实战:大整数运算(字符串模拟+加减乘除)

说实话,C++ 里自带的整数类型,像 int、long long,都有个天花板。你算个阶乘到 20 就快撑不住了。那要是让你算两个 100 位的数相加呢?

嗯,这时候就得靠我们自己动手了——用字符串来模拟大整数的运算。说白了,就是回到小学时代,拿笔在纸上列竖式,只不过这次我们让代码替我们算。

为什么需要大整数运算?

我在做金融系统的时候遇到过这么个事:客户要算一笔利息,本金是 10 位数,利率精确到小数点后 6 位,乘完以后直接溢出。当时我就想,要是 C++ 原生支持大整数该多好。可惜没有,那就自己造轮子吧。

大整数的核心思路很简单:

  • 用字符串存储数字,每一位是一个字符
  • 模拟手工计算的竖式过程
  • 处理好进位和借位

整体架构

我们先画一张图,看看大整数运算的核心模块长什么样:

大整数运算核心模块 字符串输入(大整数A、B) 选择运算:+ - × ÷ 加法:逐位相加+进位 减法:逐位相减+借位 乘法:逐位乘+累加 除法:试商+减法 输出结果(字符串)

加法实现

加法是最简单的。从最低位开始,两个数字加上进位,结果存到新字符串里。我习惯先把两个字符串反转,这样下标 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;
}
注意:去掉前导零这步很容易忘。我曾经调试了半天,发现结果总是多一串零,就是因为忘了 pop_back。另外,如果结果是 0,要保留一个 '0',不能全删光。

乘法实现

乘法本质上就是多次加法。我们模拟竖式乘法:每一位乘完以后,结果要移位,最后累加。

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 为商的每一位试商次数
优化思路:如果追求性能,可以用 Karatsuba 乘法(分治思想,O(n^1.585)),或者用 FFT(快速傅里叶变换,O(n log n))。不过对于 99% 的场景,上面的朴素实现已经够用了。

避坑指南

我总结几个容易踩的坑:

  • 前导零问题:减法、除法、乘法都可能产生前导零,记得清理
  • 符号处理:减法要考虑负号,乘除法也要考虑符号位
  • 空字符串:如果结果是 0,要返回 "0" 而不是空串
  • 性能陷阱:频繁的字符串拼接用 vector<char> 或数组代替

我曾经在项目里用这个方案处理过 500 位的数字乘法,跑了大概 2 秒。虽然不快,但至少能算出来。要是用原生类型,早就溢出了。

好了,大整数运算的核心就这些。你动手写一遍,把这些函数封装成一个类,以后遇到大数问题直接拿来用,省心省力。

公众号:蓝海资料掘金营,微信deep3321