实战:大整数运算(字符串模拟+加减乘除)
说实话,C++ 里自带的整数类型,像 int、long long,都有个天花板。你算个阶乘到 20 就快撑不住了。那要是让你算两个 100 位的数相加呢?
嗯,这时候就得靠我们自己动手了——用字符串来模拟大整数的运算。说白了,就是回到小学时代,拿笔在纸上列竖式,只不过这次我们让代码替我们算。
为什么需要大整数运算?
我在做金融系统的时候遇到过这么个事:客户要算一笔利息,本金是 10 位数,利率精确到小数点后 6 位,乘完以后直接溢出。当时我就想,要是 C++ 原生支持大整数该多好。可惜没有,那就自己造轮子吧。
大整数的核心思路很简单:
- 用字符串存储数字,每一位是一个字符
- 模拟手工计算的竖式过程
- 处理好进位和借位
整体架构
我们先画一张图,看看大整数运算的核心模块长什么样:
加法实现
加法是最简单的。从最低位开始,两个数字加上进位,结果存到新字符串里。我习惯先把两个字符串反转,这样下标 0 就是最低位,处理起来顺手。
string add(string a, string b) {
// 反转,方便从低位开始
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
string result;
int carry = 0;
int len = max(a.size(), b.size());
for (int i = 0; i < len; i++) {
int digitA = (i < a.size()) ? a[i] - '0' : 0;
int digitB = (i < b.size()) ? b[i] - '0' : 0;
int sum = digitA + digitB + carry;
carry = sum / 10;
result.push_back((sum % 10) + '0');
}
if (carry) result.push_back(carry + '0');
reverse(result.begin(), result.end());
return result;
}
减法实现
减法比加法麻烦一点,因为要处理借位。而且得先判断谁大谁小,保证用大的减小的,最后再决定要不要加负号。
// 比较两个数字字符串的大小
bool isGreaterOrEqual(string a, string b) {
if (a.size() != b.size()) return a.size() > b.size();
return a >= b;
}
string subtract(string a, string b) {
// 保证 a >= b
bool negative = false;
if (!isGreaterOrEqual(a, b)) {
swap(a, b);
negative = true;
}
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
string result;
int borrow = 0;
for (int i = 0; i < a.size(); i++) {
int digitA = a[i] - '0' - borrow;
int digitB = (i < b.size()) ? b[i] - '0' : 0;
if (digitA < digitB) {
digitA += 10;
borrow = 1;
} else {
borrow = 0;
}
result.push_back((digitA - digitB) + '0');
}
// 去掉前导零
while (result.size() > 1 && result.back() == '0') {
result.pop_back();
}
reverse(result.begin(), result.end());
if (negative) result.insert(result.begin(), '-');
return result;
}
乘法实现
乘法本质上就是多次加法。我们模拟竖式乘法:每一位乘完以后,结果要移位,最后累加。
string multiply(string a, string b) {
int n = a.size(), m = b.size();
vector<int> result(n + m, 0);
// 逐位相乘
for (int i = n - 1; i >= 0; i--) {
for (int j = m - 1; j >= 0; j--) {
int mul = (a[i] - '0') * (b[j] - '0');
int sum = mul + result[i + j + 1];
result[i + j + 1] = sum % 10;
result[i + j] += sum / 10;
}
}
// 转成字符串
string res;
for (int num : result) {
if (!(res.empty() && num == 0)) {
res.push_back(num + '0');
}
}
return res.empty() ? "0" : res;
}
你想想看,这里用 vector 来存中间结果,比用字符串拼接要高效得多。我刚开始用字符串做乘法,每乘一位就拼接一次,结果 100 位的数乘起来慢得离谱。
除法实现
除法是最复杂的。我用的方法是「试商法」:每次从被除数里取一位,跟除数比,够除就减,不够就补一位。
pair<string, string> divide(string a, string b) {
if (b == "0") {
throw runtime_error("除数不能为0");
}
string quotient, remainder;
for (char digit : a) {
remainder.push_back(digit);
// 去掉前导零
size_t pos = remainder.find_first_not_of('0');
if (pos != string::npos) {
remainder = remainder.substr(pos);
} else {
remainder = "0";
}
// 试商
int count = 0;
while (isGreaterOrEqual(remainder, b)) {
remainder = subtract(remainder, b);
count++;
}
quotient.push_back(count + '0');
}
// 去掉商的前导零
size_t pos = quotient.find_first_not_of('0');
if (pos != string::npos) {
quotient = quotient.substr(pos);
} else {
quotient = "0";
}
return {quotient, remainder};
}
完整测试
我们来跑一个完整的例子,看看这些函数能不能正常工作:
int main() {
string a = "12345678901234567890";
string b = "98765432109876543210";
cout << "加法: " << add(a, b) << endl;
cout << "减法: " << subtract(a, b) << endl;
cout << "乘法: " << multiply(a, b) << endl;
auto [q, r] = divide(a, b);
cout << "除法商: " << q << endl;
cout << "余数: " << r << endl;
return 0;
}
输出结果:
加法: 111111111011111111100
减法: -86419753208641975320
乘法: 1219326311370217952237463801111263526900
除法商: 0
余数: 12345678901234567890
性能与优化
说实话,用字符串模拟大整数,性能肯定不如原生整数。但它的优势是「无限精度」——只要内存够,多少位都能算。
| 运算 | 时间复杂度 | 说明 |
|---|---|---|
| 加法 | O(n) | 一次遍历,n 为位数 |
| 减法 | O(n) | 一次遍历,需要比较大小 |
| 乘法 | O(n*m) | 双重循环,n、m 为位数 |
| 除法 | O(n * k) | k 为商的每一位试商次数 |
避坑指南
我总结几个容易踩的坑:
- 前导零问题:减法、除法、乘法都可能产生前导零,记得清理
- 符号处理:减法要考虑负号,乘除法也要考虑符号位
- 空字符串:如果结果是 0,要返回 "0" 而不是空串
- 性能陷阱:频繁的字符串拼接用 vector<char> 或数组代替
我曾经在项目里用这个方案处理过 500 位的数字乘法,跑了大概 2 秒。虽然不快,但至少能算出来。要是用原生类型,早就溢出了。
好了,大整数运算的核心就这些。你动手写一遍,把这些函数封装成一个类,以后遇到大数问题直接拿来用,省心省力。