引言
本文的算法大量参考《计算机程序设计艺术》(The Art of Computer Programming)的算法,代码部分大量参考 Java 的 BigInteger 库。
为了便于理解,文中的代码为无符号的大整数,有符号的大整数可以在此基础上进行进一步封装。代码在效率上还有些许提升空间,并且不做非法输入的检查。
本文包含的算法有 大数比较,大数加法,大数减法,大数乘法,大数除法,大数与字符串转化
本文的代码已经开源至我个人的仓库:https://gitee.com/oldprincess/bint
一、大整数结构
1. 大整数表示
对于一个整数,可以有多种表示方法,例如二进制、十进制、十六进制等等。定义符号 b 表示基底,二进制时 b=2,十进制时 b=10,那么一个整数 n 可以表示成
n=k∑i=0aibi,a∈[0,b−1]
例如十进制数 (1234)10 可记为 1×103+2×102+3×10+4,十六进制数 (AB)16 可记为 10×16+11
在大数库中,采用 b=232 作为基底,一是因为此时系数项 a 的取值范围为 [0,232−1],刚好是一个 32 位无符号整数所表示的范围,可以有效地利用内存;二是因为现代计算机大多是 64 位系统,32 位无符号整数的运算结果能够被 64 位无符号整数表示,能够方便地获取运算时进位的数值。
在具体程序实现时,使用一段内存来表示大整数,存储整数表示时的系数 ai。此处采取大端存储的方式,即数据高位在高地址,低位在低地址。例如 (a2a1a0)b 中,a2 的存储地址要高于 a0 的地址。
这么做的优势在于,在后续整数长度扩大时,可以方便地扩充内存,而保留原始的数据内容不变。
2. 相关代码
二. 大整数算法
1. 大整数比大小
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vmvm−1⋯v0)b,比较大小算法为
- ifn>m
- return 1// u 大于 v
- else ifn<m
- return -1// u 小于 v
- else
- fori=n→0
- ifui>vi
- return 1// u 大于 v
- ifui<vi
- return -1// u 小于 v
- return 0// u 等于 v
2. 大整数加法
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vnvn−1⋯v0)b,它们加法结果为 r=(rn+1rnrn−1⋯r0)b,r=u+v 算法如下
- carry←0// 初始化进位
- fori=0→n// 从最低位遍历至最高位
- ri←(ui+vi+carry)modb// 取结果
- carry←⌊(ui+vi+carry)/b⌋// 取进位
- rn+1←carry
上述算法即普通的竖式加法
索引 |
n+1 |
n |
n-1 |
... |
0 |
|
0 |
un |
un−1 |
... |
u0 |
+ |
0 |
vn |
vn−1 |
... |
v0 |
|
rn+1 |
rn |
rn−1 |
... |
r0 |
3. 大整数减法
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vnvn−1⋯v0)b,它们减法结果为 r=(rnrn−1⋯r0)b,r=u−v(u>v) 算法如下
- borrow←0//初始化借位
- fori=0→n// 从最低位遍历至最高位
- ifui−vi−borrow<0// 需要借位
- ri←ui−vi−borrow+b
- borrow←1// 设置借位
- else
- ri←ui−vi−borrow
- borrow←0
上述算法即普通的竖式减法
索引 |
n |
n-1 |
... |
0 |
|
un |
un−1 |
... |
u0 |
- |
vn |
vn−1 |
... |
v0 |
|
rn |
rn−1 |
... |
r0 |
4. 大整数乘法
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vmvm−1⋯v0)b,它们乘法结果为 r=(rn+mrm+n−1rm+n−2⋯r0)b,r=u×v 算法如下
- r←0
- fori=0→m// 遍历v
- carry←0// 初始化进位
- forj=0→n// 遍历 u
- tmp←ri+j+uj×vi+carry
- ri+j←tmpmodb
- carry←⌊tmp/b⌋
- ri+n←carry
上述算法即普通的竖式乘法
索引 |
... |
n |
... |
m |
... |
0 |
|
|
un |
... |
um |
... |
u0 |
× |
|
|
|
vm |
... |
v0 |
|
... |
rn |
... |
rm |
... |
r0 |
伪代码中的算法可以表达成公式
(rn+m⋯r0)b=m∑i=0(un⋯u0)b×vi×bi
对于大数乘法,上述只是一个比较普通的算法,在 Java BigInteger 库中还采用了分治的算法,例如 Karatsuba 或者 Toom-Cook。而且对于平方运算,还有更为高效的算法。
5. 乘法与除法(大整数与普通整数运算)
为了方面后续代码的实现,需要用到如下两个函数
(1) 乘和加
给定 b 进制大整数 (unun−1⋯u0)b,b 进制整数 x 和 y,计算 (rn+1rn⋯r0)b=u×x+y 的算法如下
- r←0
- carry←y// 初始化进位
- fori=0→n// 遍历 u
- tmp←ui×x+carry
- ri←tmpmodb
- carry←⌊tmp/b⌋
- rn+1←carry
(2) 除法
给定 b 进制大整数 (unun−1⋯u0)b,b 进制整数 d,计算 (qnqn−1⋯q0)b=u÷d,余数为 b 进制整数 r 的算法如下
- r←0// 初始化余数
- fori=n→0// 遍历 u
- tmp←(r×b+ui)
- qi←tmpmodd
- r←⌊tmp/d⌋
上述即普通的竖式除法,从最高位开始除,记录当前位的商和余数,直至最低位
6. 字符串转化
(1) 大整数转字符串
将 b 进制大整数 u=(unun−1⋯u0)b 转化为 radix 进制的字符串 s 算法如下
- i←0
- whileu≠0
- rem←umodradix
- u←⌊u÷radix⌋
- si←chr(rem)// 存储余数对应的字符数值
- i←i+1
- s←逆序(s)
(2) 字符串转大整数
将 radix 进制字符串 s 转化为 b 进制大整数 u=(unun−1⋯u0)b 算法如下
- u←0
- fori=0→s.len−1
- u←u×radix+int(si)
为了有效地利用运算模块,可以将 k 个字符合并为一组(这也是Java BigInteger库所采用的策略),即
- u←0
- i←0
- whilei<s.len
- u←u×radix+int(si⋯si+k−1)
- i←i+k
7. 除法
除法算法基于《计算机程序设计艺术》书中提到的一些定理,具体证明建议去阅读书籍
(1) 定理1
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vn−1vn−2⋯v0)b,它们除法结果为 q
通过竖式除法可知,若 (unun−1⋯u1)b<(vn−1vn−2⋯v0)b,那么 q 的长度为 1
记
^q=min(⌊unb+un−1vn−1⌋,b−1)
如果有 vn−1≥⌊b/2⌋,那么 ^q−2≤q≤^q
(2) 定理2
对于定理1中的 u,v,^q,令 ^r=(unb+un−1)modvn−1
测试 ^q=b 或 ^qvn−2>b^r+un−2,如果是,则 ^q 减1,^r 加上 vn−1,如果 ^r<b 则重复此测试
通过上述测试可以高速确定 ^q 比 q 大 1 的大多数情况,且消除 ^q 比 q 大 2 的所有情况
(3) 除法算法
对于两个 b 进制大整数 u=(unun−1⋯u0)b 和 v=(vmvm−1⋯v0)b,它们除法结果为 q=(qn−mqn−m−1⋯q0)b,余数为 r=(rmrm−1⋯r0)b
在《计算机程序设计艺术》中,除法算法分为 D1 ~ D8 共 8 步,下面的伪代码按照书中给的算法顺序
- // === D1 规格化 ===
- d←⌊b/(vm+1)⌋//为了让vm≥⌊b/2⌋
- u←u×d
- v←v×d
- uu.len←0// 将 u 的最高位的下一位置0
- // === D2 初始化 j ===
- forj=u.len−v.len→0
- // === D3 计算 q ===
- ^q←⌊(uj+mb+uj+m−1)/vm⌋
- if^q=0
- qj←0
- continue// 跳过本轮
- if^q≥b
- ^q←b−1
- ^r←(uj+mb+uj+m−1)−vm×^q
- // 测试^q
- while^q×vm−1>^r×b+uj+m−2
- ^q←^q−1// 更新^q
- ^r←^r+vm// 更新^r
- if^r≥b
- break// 跳出while循环
- // === D4 乘和减 ===
- u←u−q×(v×bj)
- // === D5 测试余数 ===
- ifu<0
- // === D6 往回加 ===
- u←u+(v×bj)
- ^q←^q−1
- qj←^q
- // === D7 对 j 进行循环 ===
- // === D8 逆规格化 ===
- r←u÷d
(4) 除法代码
代码对算法中的 d←⌊b/(vm+1)⌋ 进行了修改,通过计算 vm 的最高有效位来决定 d 的值,对于 u←u×d 操作可采用大整数的移位运算实现,这在二进制计算机中具备更高效的效率(为了便于理解,下方代码中并未使用移位,而是采用了较为低效的乘法操作)
8. 代码运行结果
上述代码的调用方式大致如下
三、 其它
上述给出的代码其实还有较大的优化空间
- 为了便于理解并没有使用动态内存分配,故当数据长度超过数组范围时就会发生溢出
- 存在更为高效的乘法算法(分治),对于平方还能进一步优化
- 没有设计左右移位的算法,在乘或除2的幂次方数据时,采用移位高效得多得多
- 没有进行非法输入检测
- 函数传参可以使用指针,而不是直接传递结构体。在Openssl中,为了降低运算时临时数据内存分配的开销,额外设置了一个用于存储上下文CTX的结构
参考:《计算机程序设计艺术》,Donald E. Knuth
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示