高精度对于有些语言来说都有成型的库函数供程序员使用,但是其中对于算法的考量来说,我们还是希望重新感受一下实现的过程。
以c++为例,对于一般的 int,long,或者 long long 类型的能够满足大多数场景,但对于成百上千位的大数来说这些就无能为力了。
本文参考相关资料整理了加减乘除的大数算法小题目。 回顾一下最基本的运算规则用程序如何实现。
大数加法
给定两个正整数,不含前导0, 计算他们的和。
整数长度范围 [1, 100000]
实现思路也比较直接,我们无非是要按位相加,时刻记住进位补数即可,根本在于我们用一个数组的结构来存储数的每一位。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main() {
string a, b; cin >> a >> b;
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
//string res = "";
vector<int> res;
int t = 0;
for (size_t i = 0, j = 0; i < a.size() || j < b.size(); i++, j++) {
if (i < a.size()) t += a[i] - '0';
if (j < b.size()) t += b[j] - '0';
//res = to_string(t % 10) + res;
res.push_back(t % 10);
t = t / 10;
}
if (t > 0) res.push_back(t);
for (int i = res.size() - 1; i >= 0; i--) cout << res[i];
//if(t > 0) res = to_string(t) + res;
//cout << res;
return 0;
}
从注释中可以看出我原来用的是字符串拼接和存储中间结果,实际的效果比当前时间耗时高一个数量级,因此是不可取的,相信可以猜到字符串拼接存在大量的空间重分配和拷贝操作,具体原因可能到细究一下 c++ 的string 实现的考量,这个问题其实不简单,希望有机会能写写。
大数减法
给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。
与加法不同的是,减法考虑的是从高位借位。尤其最后可能会出现前导0,要处理一下。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
bool cmp1(string a, string b) {
if (a.size() != b.size()) return a.size() < b.size();
return a < b;
}
vector<int> sub(string a, string b) {
vector<int> res;
int borrow = 0;
for (int i = 0, j = 0; i < a.size() || j < b.size(); i++, j++) {
int t = a[i] - '0' - borrow;
if (j < b.size()) t = t - (b[j] - '0');
if (t < 0) {
borrow = 1;
t = t + 10;
} else borrow = 0;
res.push_back(t);
}
return res;
}
int main() {
string a, b; cin >> a >> b;
bool sw = 0;
if (cmp1(a, b)){
swap(a, b);
sw = true;
}
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
vector<int> res = sub(a, b);
if (sw) cout << "-";
// 处理前导0
int i = res.size() - 1;
while(res[i] == 0 && i > 0)i--;
while(i >= 0) cout << res[i--];
return 0;
}
大数乘法(大数乘小数)
给定两个非负整数(不含前导 0) A 和 B,请你计算 A x B 的值
A 的长度 [1, 100000]
B 的大小 【0, 10000】
思路是,把小数B乘A的每一位,然后加起来,因为每次数位高一位,所以错位加,从A的低位到高位,每次加起来。
可以确定的是每次的乘法运算都是小数,每次有且仅有一结果位可以确认。
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
string a; int b;
cin >> a >> b;
int lastSeg = 0;
vector<int> res;
reverse(a.begin(), a.end());
for (int i = 0; i < a.size(); i++) {
int t = b * (a[i] - '0') + lastSeg;
res.push_back(t % 10);
lastSeg = t / 10;
}
while(lastSeg > 0) {
res.push_back(lastSeg%10);
lastSeg /= 10;
}
int i = res.size() - 1;while(res[i] == 0 && i > 0) i--;
while (i >= 0) cout << res[i--];
return 0;
}
大数除法(大数除小数)
除法采用试除法,每次从高位开始,除以小数,可以除就得一位商,不可补0,剩余增加一位后继续以此类推。最后不能除的是余数。
最后记得处理前导0.
#include<iostream>
#include<vector>
using namespace std;
int main() {
string a; int b; cin >> a >> b;
// 试除法
vector<int> dv;
int md = 0;
int obj = 0;
for (int i = 0; i < a.size(); i++) {
obj = obj * 10 + a[i] - '0';
if (obj < b) {
dv.push_back(0);
continue;
}
dv.push_back(obj/b);
obj = obj % b;
}
md = obj;
int i = 0; while(dv[i] == 0 && i < dv.size() - 1) i++;
while(i < dv.size()) cout << dv[i++];
cout << endl;
cout << md;
return 0;
}
不足
当然考察一个成熟的工业级实现是另一个话题了,拿string来说,不同的程序组件实现都会有很多细节的考量,例如 redis的SDS,mysql的字符串实现。工程实践使用还是要多翻一下源代码,以便写出更加健壮优雅的程序实现。
参考文献
[1] https://www.acwing.com/
[2]《c++ 标准程序库》Nicolai M.Josuttis , 候捷,孟岩 译