第30章 字符串与数字互转(大数处理):大数加法、大数乘法

各位同学,今天咱们聊点硬核的——大数处理。

你可能会问:C语言里不是有int、long long吗?为什么还要搞什么大数?

我当年刚入行时也这么想。直到有一次做金融系统,需要计算一个100位的整数乘法。int?long long?全炸了。那会儿我才意识到:语言内置的类型,根本扛不住真实世界的需求

说白了,大数处理就是用字符串或数组来模拟数学运算。今天咱们就讲两个最基础的操作:大数加法大数乘法


30.1 大数加法:从小学竖式说起

大数加法的思路,其实就是你小学学过的竖式加法。

两个数从低位到高位逐位相加,逢十进一。只不过现在数字太大,我们得用字符串存。

我习惯用逆序存储。什么意思?比如数字"12345",我存成字符数组时,让低位在索引0的位置:

原始数字: "12345"
逆序存储: ['5','4','3','2','1']

为什么这么干?因为加法是从低位开始的。逆序存储后,索引0就是最低位,循环起来特别顺手。

来看代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 大数加法:返回 a + b 的字符串结果
char* big_add(const char* a, const char* b) {
    int len_a = strlen(a);
    int len_b = strlen(b);
    int max_len = (len_a > len_b ? len_a : len_b) + 1; // 多留一位给进位
    char* result = (char*)malloc(max_len + 1);
    if (!result) return NULL;

    int i = len_a - 1, j = len_b - 1, k = 0;
    int carry = 0;

    // 从低位到高位逐位相加
    while (i >= 0 || j >= 0 || carry) {
        int digit_a = (i >= 0) ? a[i--] - '0' : 0;
        int digit_b = (j >= 0) ? b[j--] - '0' : 0;
        int sum = digit_a + digit_b + carry;
        result[k++] = (sum % 10) + '0';
        carry = sum / 10;
    }

    result[k] = '\0';

    // 翻转结果(因为我们是从低位往高位存的)
    for (int left = 0, right = k - 1; left < right; left++, right--) {
        char tmp = result[left];
        result[left] = result[right];
        result[right] = tmp;
    }

    return result;
}

这段代码我用了很多年。核心就三点:

  • 从后往前遍历两个字符串
  • 处理进位,carry变量是关键
  • 最后翻转,因为结果也是逆序存的
小技巧: 我习惯在循环条件里直接写 while (i >= 0 || j >= 0 || carry),这样即使两个数都遍历完了,只要还有进位,循环就不会停。省得最后再单独处理一次进位。

你可能会问:如果两个数都是0呢?

嗯,这个代码会返回"0"。因为循环一开始carry=0,两个digit都是0,sum=0,结果存了个'0',然后carry=0,循环结束。没问题。


30.2 大数乘法:别被复杂度吓到

大数乘法比加法复杂一些。但别怕,思路还是那个思路——模拟竖式乘法

你还记得小学怎么算多位数乘法吗?

比如 123 × 45:

      1 2 3
   ×    4 5
   --------
      6 1 5   (123 × 5)
    4 9 2     (123 × 4,左移一位)
   --------
    5 5 3 5   (相加)

大数乘法就是把这个过程用代码实现。我个人的做法是:

  1. 先逐位相乘,结果存到一个临时数组里(不处理进位)
  2. 最后统一处理进位

这样逻辑更清晰,不容易出错。

来看代码:

char* big_multiply(const char* a, const char* b) {
    int len_a = strlen(a);
    int len_b = strlen(b);
    int total_len = len_a + len_b;
    int* temp = (int*)calloc(total_len, sizeof(int));
    if (!temp) return NULL;

    // 逐位相乘,结果累加到对应位置
    for (int i = len_a - 1; i >= 0; i--) {
        for (int j = len_b - 1; j >= 0; j--) {
            int digit_a = a[i] - '0';
            int digit_b = b[j] - '0';
            int pos = (len_a - 1 - i) + (len_b - 1 - j);
            temp[pos] += digit_a * digit_b;
        }
    }

    // 统一处理进位
    int carry = 0;
    for (int i = 0; i < total_len; i++) {
        int sum = temp[i] + carry;
        temp[i] = sum % 10;
        carry = sum / 10;
    }

    // 找到最高有效位
    int last = total_len - 1;
    while (last > 0 && temp[last] == 0) last--;

    // 转成字符串
    char* result = (char*)malloc(last + 2);
    if (!result) {
        free(temp);
        return NULL;
    }

    for (int i = 0; i <= last; i++) {
        result[i] = temp[last - i] + '0';
    }
    result[last + 1] = '\0';

    free(temp);
    return result;
}
注意: 这里我用了一个int数组做临时存储,而不是char数组。为什么?因为两个个位数相乘最大是81,加上进位可能超过127。用char存会溢出。我曾经在这个坑里栽过,调试了半天才发现是类型问题。

这段代码的时间复杂度是O(n×m),n和m分别是两个数的位数。对于几百位的数,完全够用。

但如果你要处理上万位的数,那就得考虑更高效的算法了,比如Karatsuba算法或FFT。不过那是进阶内容,咱们今天先打好基础。


30.3 知识结构图:大数处理的核心逻辑

下面这张图,是我自己总结的大数处理知识结构。你看一眼,心里就有谱了。

大数处理核心知识结构 大数处理 大数加法 大数乘法 逆序存储 逐位相加+进位 逐位相乘累加 统一处理进位 核心思想:模拟竖式运算 + 字符串/数组存储

从图上你能看到,大数加法和乘法都基于同一个思想:模拟竖式运算。区别在于加法是一次遍历,乘法是两层循环。


30.4 避坑指南:我踩过的那些坑

做这行十几年,我在大数处理上栽过不少跟头。挑几个典型的说说:

坑1:忘记处理前导零

比如"000" + "0" 应该返回 "0",而不是 "000"。我在乘法代码里用了一个while循环跳过前导零,这个习惯一定要养成。

坑2:内存泄漏

每次malloc都要记得free。我早期写代码时经常忘记释放临时数组,导致程序跑着跑着内存就爆了。现在我的习惯是:谁分配,谁释放。函数内部malloc的,要么在函数内释放,要么在文档里明确告诉调用者要free。

坑3:字符和数字的转换

字符'0'到'9'转成数字0到9,要减去'0'。反过来要加上'0'。这个太基础了,但越基础的东西越容易在忙乱中出错。我见过有人写 int digit = a[i] - 48;,虽然也能跑,但可读性差。用'0'更清晰。

坑4:进位处理不彻底

加法里如果最后还有进位,一定要记得加上。乘法里如果某一位累加后超过10,进位要一直传递下去。我见过一个bug,就是进位只处理了一次,结果算出来少了一位。


30.5 实战经验:什么时候用大数?

你可能觉得大数处理离自己很远。其实不然。

我参与过一个区块链钱包项目,里面涉及以太坊的wei单位转换。1 ether = 10^18 wei,这个数字已经超过long long的范围了。所有金额计算都得用大数。

还有一个科学计算器的项目,用户输入"12345678901234567890 × 98765432109876543210",普通计算器直接溢出,但我们的计算器能给出精确结果。靠的就是大数乘法。

所以,别觉得大数处理是屠龙之术。它其实是个很实用的技能。


30.6 总结

今天咱们讲了:

  • 大数加法:逆序存储,逐位相加,处理进位
  • 大数乘法:两层循环逐位相乘,统一处理进位
  • 避坑指南:前导零、内存泄漏、类型转换、进位传递

这两个算法是基础中的基础。掌握了它们,你就能处理任意长度的整数运算了。

最后送你一句话:算法不难,难的是细节。写代码时多想想边界情况,多测试几个极端例子,你的代码就会越来越稳。


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