大整数运算算法(C)

引言

本文的算法大量参考《计算机程序设计艺术》(The Art of Computer Programming)的算法,代码部分大量参考 Java 的 BigInteger 库。

为了便于理解,文中的代码为无符号的大整数,有符号的大整数可以在此基础上进行进一步封装。代码在效率上还有些许提升空间,并且不做非法输入的检查。

本文包含的算法有 大数比较大数加法大数减法大数乘法大数除法大数与字符串转化

本文的代码已经开源至我个人的仓库:https://gitee.com/oldprincess/bint

一、大整数结构

1. 大整数表示

对于一个整数,可以有多种表示方法,例如二进制、十进制、十六进制等等。定义符号 b 表示基底,二进制时 b=2,十进制时 b=10,那么一个整数 n 可以表示成

n=i=0kaibi,a[0,b1]

例如十进制数 (1234)10 可记为 1×103+2×102+3×10+4,十六进制数 (AB)16 可记为 10×16+11

在大数库中,采用 b=232 作为基底,一是因为此时系数项 a 的取值范围为 [0,2321],刚好是一个 32 位无符号整数所表示的范围,可以有效地利用内存;二是因为现代计算机大多是 64 位系统,32 位无符号整数的运算结果能够被 64 位无符号整数表示,能够方便地获取运算时进位的数值。

在具体程序实现时,使用一段内存来表示大整数,存储整数表示时的系数 ai。此处采取大端存储的方式,即数据高位在高地址,低位在低地址。例如 (a2a1a0)b 中,a2 的存储地址要高于 a0 的地址。
这么做的优势在于,在后续整数长度扩大时,可以方便地扩充内存,而保留原始的数据内容不变。

2. 相关代码

// bint.h #include <stdint.h> #define BINT_SIZE 256 typedef struct { uint32_t value[BINT_SIZE]; // 内存,初始化为0 int32_t dsize; // 数据长度 } BINT; // 大整数结构 typedef struct { BINT q; // 商 BINT r; // 余数 } BINT_DIV_T; // 除法返回值 #define BINT_NEW() \ { .value = {0}, .dsize = 0 }

二. 大整数算法

1. 大整数比大小

对于两个 b 进制大整数 u=(unun1u0)bv=(vmvm1v0)b,比较大小算法为

  • ifn>m
  • return 1// u 大于 v
  • else ifn<m
  • return -1// u 小于 v
  • else
  • fori=n0
  • ifui>vi
  • return 1// u 大于 v
  • ifui<vi
  • return -1// u 小于 v
  • return 0// u 等于 v
/** * @brief bint_cmp 大整数比大小 * @return 1(u>v), -1(u<v), 0(u=v) */ int bint_cmp(BINT u, BINT v) { if (u.dsize > v.dsize) { // u 长度大于 v return 1; } else if (u.dsize < v.dsize) { // u 长度小于 v return -1; } else { // u 和 v 长度相等 for (int i = u.dsize; i >= 0; i--) { if (u.value[i] > v.value[i]) { return 1; } else if (u.value[i] < v.value[i]) { return -1; } } } return 0; }

2. 大整数加法

对于两个 b 进制大整数 u=(unun1u0)bv=(vnvn1v0)b,它们加法结果为 r=(rn+1rnrn1r0)br=u+v 算法如下

  • carry0// 初始化进位
  • fori=0n// 从最低位遍历至最高位
  • ri(ui+vi+carry)modb// 取结果
  • carry(ui+vi+carry)/b// 取进位
  • rn+1carry

上述算法即普通的竖式加法

索引 n+1 n n-1 ... 0
0 un un1 ... u0
+ 0 vn vn1 ... v0
rn+1 rn rn1 ... r0
BINT bint_add(BINT u, BINT v) { BINT r = BINT_NEW(); int32_t maxlen = max(u.dsize, v.dsize); uint32_t carry = 0; // 进位 uint64_t tmp = 0; //临时值 for (int32_t i = 0; i < maxlen; i++) { tmp = (uint64_t)(u.value[i]) + (uint64_t)(v.value[i]) + carry; r.value[i] = (uint32_t)(tmp & UINT32_MAX); carry = (uint32_t)(tmp >> 32); } r.dsize = maxlen; // 处理结果长度 if (carry > 0) { r.value[maxlen] = carry; r.dsize++; } return r; }

3. 大整数减法

对于两个 b 进制大整数 u=(unun1u0)bv=(vnvn1v0)b,它们减法结果为 r=(rnrn1r0)br=uv(u>v) 算法如下

  • borrow0//初始化借位
  • fori=0n// 从最低位遍历至最高位
  • ifuiviborrow<0// 需要借位
  • riuiviborrow+b
  • borrow1// 设置借位
  • else
  • riuiviborrow
  • borrow0

上述算法即普通的竖式减法

索引 n n-1 ... 0
un un1 ... u0
- vn vn1 ... v0
rn rn1 ... r0
BINT bint_sub(BINT u, BINT v) { BINT r = BINT_NEW(); int32_t maxlen = max(u.dsize, v.dsize); uint32_t borrow = 0; // 借位 uint64_t tmp = 0; //临时值 for (int32_t i = 0; i < maxlen; i++) { tmp = (uint64_t)(u.value[i]) - (uint64_t)(v.value[i]) - borrow; r.value[i] = (uint32_t)(tmp & UINT32_MAX); // 存在借位 if (tmp >> 32 != 0) { borrow = 1; } else { borrow = 0; } } r.dsize = maxlen; // 处理结果长度 while (r.dsize > 0 && r.value[r.dsize - 1] == 0) { r.dsize--; } return r; }

4. 大整数乘法

对于两个 b 进制大整数 u=(unun1u0)bv=(vmvm1v0)b,它们乘法结果为 r=(rn+mrm+n1rm+n2r0)br=u×v 算法如下

  • r0
  • fori=0m// 遍历v
  • carry0// 初始化进位
  • forj=0n// 遍历 u
  • tmpri+j+uj×vi+carry
  • ri+jtmpmodb
  • carrytmp/b
  • ri+ncarry

上述算法即普通的竖式乘法

索引 ... n ... m ... 0
un ... um ... u0
× vm ... v0
... rn ... rm ... r0

伪代码中的算法可以表达成公式

(rn+mr0)b=i=0m(unu0)b×vi×bi

BINT bint_mul(BINT u, BINT v) { BINT r = BINT_NEW(); uint64_t tmp; uint32_t carry; for (int32_t i = 0; i < v.dsize; i++) { carry = 0; uint32_t val = v.value[i]; for (int32_t j = 0; j < u.dsize; j++) { tmp = (uint64_t)(r.value[i + j]) + (uint64_t)(u.value[j]) * val + carry; r.value[i + j] = (uint32_t)(tmp & UINT32_MAX); carry = (uint32_t)(tmp >> 32); } r.value[i + u.dsize] = carry; } r.dsize = u.dsize + v.dsize; // 处理结果长度 while (r.dsize > 0 && r.value[r.dsize - 1] == 0) { r.dsize--; } return r; }

对于大数乘法,上述只是一个比较普通的算法,在 Java BigInteger 库中还采用了分治的算法,例如 Karatsuba 或者 Toom-Cook。而且对于平方运算,还有更为高效的算法。

5. 乘法与除法(大整数与普通整数运算)

为了方面后续代码的实现,需要用到如下两个函数

(1) 乘和加

给定 b 进制大整数 (unun1u0)bb 进制整数 xy,计算 (rn+1rnr0)b=u×x+y 的算法如下

  • r0
  • carryy// 初始化进位
  • fori=0n// 遍历 u
  • tmpui×x+carry
  • ritmpmodb
  • carrytmp/b
  • rn+1carry
/** * @return u * mul_val + add_val */ static BINT bint_muladd(BINT u, uint32_t mul_val, uint32_t add_val) { BINT r = BINT_NEW(); uint64_t tmp; uint32_t carry = add_val; for (int32_t i = 0; i < u.dsize; i++) { tmp = (uint64_t)(u.value[i]) * mul_val + carry; r.value[i] = (uint32_t)(tmp & UINT32_MAX); carry = (uint32_t)(tmp >> 32); } r.dsize = u.dsize; if (carry > 0) { r.value[r.dsize] = carry; r.dsize++; } return r; }

(2) 除法

给定 b 进制大整数 (unun1u0)bb 进制整数 d,计算 (qnqn1q0)b=u÷d,余数为 b 进制整数 r 的算法如下

  • r0// 初始化余数
  • fori=n0// 遍历 u
  • tmp(r×b+ui)
  • qitmpmodd
  • rtmp/d

上述即普通的竖式除法,从最高位开始除,记录当前位的商和余数,直至最低位

/** * @return u / div, 余数为 r */ static BINT bint_div_u32(uint32_t* r, BINT u, uint32_t div) { BINT q = BINT_NEW(); uint64_t tmp; //临时值 uint64_t rem = 0; //余数 // 从最高位一直除至最低位 for (int32_t i = u.dsize - 1; i >= 0; i--) { tmp = (rem << 32) | (uint64_t)(u.value[i]); q.value[i] = (uint32_t)(tmp / div); rem = tmp % (uint64_t)div; } if (r != NULL) { *r = (uint32_t)rem; } q.dsize = u.dsize; while (q.dsize > 0 && q.value[q.dsize - 1] == 0) { q.dsize--; } return q; }

6. 字符串转化

(1) 大整数转字符串

b 进制大整数 u=(unun1u0)b 转化为 radix 进制的字符串 s 算法如下

  • i0
  • whileu0
  • remumodradix
  • uu÷radix
  • sichr(rem)// 存储余数对应的字符数值
  • ii+1
  • s逆序(s)
void bint_tostr(BINT u, char* s, int radix) { // 对0特殊判断 if (u.dsize == 0) { s[0] = '0'; s[1] = '\0'; return; } // 除 uint32_t r = 0; char* cur = s; while (u.dsize > 0) { u = bint_div_u32(&r, u, radix); if (r >= 0 && r <= 9) { // 0~9 直接转化为数字 *cur = '0' + (char)r; } else { // 大于9的需要转为为字母a~z *cur = 'a' + (char)r - 10; } cur++; } *cur = '\0'; // 逆序 int32_t sLen = (int32_t)((size_t)cur - (size_t)s); for (int32_t i = 0; i < sLen / 2; i++) { char t = s[i]; s[i] = s[sLen - 1 - i]; s[sLen - 1 - i] = t; } }

(2) 字符串转大整数

radix 进制字符串 s 转化为 b 进制大整数 u=(unun1u0)b 算法如下

  • u0
  • fori=0s.len1
  • uu×radix+int(si)

为了有效地利用运算模块,可以将 k 个字符合并为一组(这也是Java BigInteger库所采用的策略),即

  • u0
  • i0
  • whilei<s.len
  • uu×radix+int(sisi+k1)
  • ii+k
BINT bint_fromstr(char* s, int radix) { // 对于 radix进制,以 parse_span[radix] 个字符为单位进行转化 static int8_t parse_span[] = {0, 0, 30, 19, 15, 13, 11, 11, 10, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5}; uint32_t add_val; uint32_t mul_val; BINT r = BINT_NEW(); // 进制转换 while (*s != '\0') { // 初始化变量 add_val = 0; mul_val = 1; // 进制转换,parse_span[radix] 个字符为一组 for (int8_t i = 0; i < parse_span[radix]; i++) { uint32_t tmp = 0; char c = *s; // End of String if (c == '\0') { break; } // 字符转整数 if ('0' <= c && c <= '9') { // 0~9 tmp = c - '0'; } else if ('A' <= c && c <= 'A' + radix - 10 - 1) { // A~Z tmp = c - 'A' + 10; } else if ('a' <= c && c <= 'a' + radix - 10 - 1) { // a~z tmp = c - 'a' + 10; } mul_val = mul_val * radix; // 更新乘数 add_val = add_val * radix + tmp; // 更新加数 s++; } r = bint_muladd(r, mul_val, add_val); } return r; }

7. 除法

除法算法基于《计算机程序设计艺术》书中提到的一些定理,具体证明建议去阅读书籍

(1) 定理1

对于两个 b 进制大整数 u=(unun1u0)bv=(vn1vn2v0)b,它们除法结果为 q

通过竖式除法可知,若 (unun1u1)b<(vn1vn2v0)b,那么 q 的长度为 1

q^=min(unb+un1vn1,b1)

如果有 vn1b/2,那么 q^2qq^

(2) 定理2

对于定理1中的 u,v,q^,令 r^=(unb+un1)modvn1

测试 q^=bq^vn2>br^+un2,如果是,则 q^ 减1,r^ 加上 vn1,如果 r^<b 则重复此测试

通过上述测试可以高速确定 q^q 大 1 的大多数情况,且消除 q^q 大 2 的所有情况

(3) 除法算法

对于两个 b 进制大整数 u=(unun1u0)bv=(vmvm1v0)b,它们除法结果为 q=(qnmqnm1q0)b,余数为 r=(rmrm1r0)b

在《计算机程序设计艺术》中,除法算法分为 D1 ~ D8 共 8 步,下面的伪代码按照书中给的算法顺序

  • // === D1 规格化 ===
  • db/(vm+1)//为了让vmb/2
  • uu×d
  • vv×d
  • uu.len0// 将 u 的最高位的下一位置0
  • // === D2 初始化 j ===
  • forj=u.lenv.len0
  • // === D3 计算 q ===
  • q^(uj+mb+uj+m1)/vm
  • ifq^=0
  • qj0
  • continue// 跳过本轮
  • ifq^b
  • q^b1
  • r^(uj+mb+uj+m1)vm×q^
  • // 测试q^
  • whileq^×vm1>r^×b+uj+m2
  • q^q^1// 更新q^
  • r^r^+vm// 更新r^
  • ifr^b
  • break// 跳出while循环
  • // === D4 乘和减 ===
  • uuq×(v×bj)
  • // === D5 测试余数 ===
  • ifu<0
  • // === D6 往回加 ===
  • uu+(v×bj)
  • q^q^1
  • qjq^
  • // === D7 对 j 进行循环 ===
  • // === D8 逆规格化 ===
  • ru÷d

(4) 除法代码

代码对算法中的 db/(vm+1) 进行了修改,通过计算 vm 的最高有效位来决定 d 的值,对于 uu×d 操作可采用大整数的移位运算实现,这在二进制计算机中具备更高效的效率(为了便于理解,下方代码中并未使用移位,而是采用了较为低效的乘法操作)

static int significant_bit(uint32_t n) { int bit = 0; // 判断最高有效位 while (n != 0) { n >>= 1; bit++; } return 32 - bit; } BINT_DIV_T bint_div(BINT u, BINT v) { BINT_DIV_T res = {BINT_NEW(), BINT_NEW()}; // 除数为0 if (v.dsize == 0) { // 直接返回 return res; } // 除数长度为 1(选择更高效的算法) if (v.dsize == 1) { uint32_t divisor = v.value[0]; //除数 uint32_t rem = 0; //余数 res.q = bint_div_u32(&rem, u, divisor); res.r.value[0] = rem; if (rem > 0) { res.r.dsize++; } return res; } // Knuth 除法 // D1 规格化 int d = significant_bit(v.value[v.dsize - 1]); u = bint_muladd(u, 1 << d, 0); v = bint_muladd(v, 1 << d, 0); u.value[u.dsize] = 0; // 为了方便 D2 的循环迭代 // D2 初始化 j int32_t div_len = v.dsize; uint32_t div_h = v.value[div_len - 1]; uint32_t div_l = v.value[div_len - 2]; uint64_t base = (uint64_t)UINT32_MAX + 1; // 2^32 for (int32_t j = u.dsize - div_len; j >= 0; j--) { // D3 计算qhat uint64_t qhat, rhat; uint64_t uh = (uint64_t)(u.value[j + div_len]); uint64_t ul = (uint64_t)(u.value[j + div_len - 1]); uint64_t ul2 = (uint64_t)(u.value[j + div_len - 2]); qhat = (uh * base + ul) / (uint64_t)div_h; if (qhat > UINT32_MAX) { // 防止计算出的qhat过大 qhat = UINT32_MAX; } rhat = (uh * base + ul) - (uint64_t)div_h * qhat; if (qhat == 0) { // 商为0,跳过本轮 res.q.value[j] = 0; continue; } while (qhat * (uint64_t)div_l > base * rhat + ul2) { // 调整qhat qhat--; rhat += div_h; if (rhat >= base) { break; } } // D4 乘和减 uint64_t tmp = 0; //临时值 uint64_t borrow = 0; // u的借位 uint64_t carry = 0; // div的进位 for (int32_t i = 0; i < div_len; i++) { // 计算乘法 div*qhat uint64_t t = qhat * (uint64_t)(v.value[i]) + carry; carry = t >> 32; // 计算减法 u - div*qhat tmp = (uint64_t)(u.value[j + i]) - (t & UINT32_MAX) - borrow; borrow = (tmp >> 32) ? 1 : 0; // 赋值 u.value[j + i] = (uint32_t)(tmp & UINT32_MAX); } if (borrow != 0 || carry != 0) { tmp = (uint64_t)(u.value[j + div_len]) - carry - borrow; borrow = (tmp >> 32) ? 1 : 0; u.value[j + div_len] = (uint32_t)(tmp & UINT32_MAX); } // D5 测试余数 res.q.value[j] = (borrow == 0) ? (uint32_t)qhat : (uint32_t)qhat - 1; // D6 往回加 if (borrow != 0) { uint64_t carry = 0; // 加法进位 for (int32_t i = 0; i < div_len; i++) { tmp = (uint64_t)u.value[j + i] + (uint64_t)(v.value[i]) + carry; u.value[j + i] = (uint32_t)(tmp & UINT32_MAX); carry = tmp >> 32; } if (carry != 0) { tmp = (uint64_t)u.value[j + div_len] + carry; u.value[j + div_len] = (uint32_t)(tmp & UINT32_MAX); // 将之后的进位丢弃,以抵消D4中的借位 } } } // D7 对 j 进行循环 res.q.dsize = u.dsize - div_len + 1; while (res.q.dsize > 0 && res.q.value[res.q.dsize - 1] == 0) { res.q.dsize--; } res.r = bint_div_u32(NULL, u, 1 << d); return res; }

8. 代码运行结果

上述代码的调用方式大致如下

#include <stdio.h> #include <stdlib.h> #include "bint.h" int main() { BINT a, b, c; BINT_DIV_T d; char sbuffer[1024], sbuffer2[1024]; // load from str a = bint_fromstr("1234567123456712345671234567", 10); b = bint_fromstr("654321654321654321654321", 10); c = bint_add(a, b); // a + b bint_tostr(c, sbuffer, 10); // a+b=1235221445111033999992888888 printf("a+b=%s\n", sbuffer); c = bint_sub(a, b); // a - b bint_tostr(c, sbuffer, 10); // a-b=1233912801802390691349580246 printf("a-b=%s\n", sbuffer); c = bint_mul(a, b); // a * b bint_tostr(c, sbuffer, 10); // a*b=807804002591322070054017119327931540612061880114007 printf("a*b=%s\n", sbuffer); d = bint_div(a, b); // a / b bint_tostr(d.q, sbuffer, 10); bint_tostr(d.r, sbuffer2, 10); // a/b=1886...516483406072295031185161 printf("a/b=%s...%s\n", sbuffer, sbuffer2); bint_tostr(a, sbuffer, 10); // to str bint_tostr(a, sbuffer2, 16); // a=1234567123456712345671234567(10) // a=3fd35c1ddd60c78fbb0f407(16) printf("a=%s(10)\na=%s(16)\n", sbuffer, sbuffer2); // load from hex str a = bint_fromstr("3fd35c1ddd60c78fbb0f407", 16); bint_tostr(a, sbuffer, 10); // a=1234567123456712345671234567(10) printf("a=%s(10)\n", sbuffer); return 0; }

三、 其它

上述给出的代码其实还有较大的优化空间

  • 为了便于理解并没有使用动态内存分配,故当数据长度超过数组范围时就会发生溢出
  • 存在更为高效的乘法算法(分治),对于平方还能进一步优化
  • 没有设计左右移位的算法,在乘或除2的幂次方数据时,采用移位高效得多得多
  • 没有进行非法输入检测
  • 函数传参可以使用指针,而不是直接传递结构体。在Openssl中,为了降低运算时临时数据内存分配的开销,额外设置了一个用于存储上下文CTX的结构

参考:《计算机程序设计艺术》,Donald E. Knuth


__EOF__

本文作者kentle
本文链接https://www.cnblogs.com/kentle/p/16180799.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   kentle  阅读(1356)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示