CC++ & Algorithm

高精度减法——借位就像找邻居帮忙

较难18
语言版本:C++Python(暂无)
概述:模拟手算减法的借位过程,实现超大整数的减法运算。

高精度减法:像邻居借位一样简单

计算机里的整数有大小限制(比如 int 最大约 21 亿),但现实中我们可能需要计算几百位的数字——比如统计全国中小学生零花钱总和、计算天文数字的差值。高精度减法就是用来解决这个问题的:它把数字拆成一位一位,像我们手算竖式那样,从个位开始逐位相减,不够减就向高位借 1 当 10。我们先学会让大数减小数,结果是非负的,这样不容易晕。

为什么要把数字倒着存?

平时写竖式,个位在最右边;但在数组里,我们习惯从下标 0 开始。如果正着存(最高位在 0),那么做加法减法时,最高位对齐就很难处理(因为两个数长度不同)。所以我们倒着存:个位在数组的第 0 个位置

比如:

  • 803 → 倒着存:a = [3, 0, 8](下标0是3,下标1是0,下标2是8)
  • 56 → 倒着存:b = [6, 5](下标0是6,下标1是5)

这样一来,从下标 0 开始逐位操作,就相当于从个位开始计算,非常方便。

借位就像找邻居帮忙

你还记得手算减法竖式的过程吗?比如算 803 - 56

   8 0 3
-    5 6
---------

个位:3 不够减 6,向十位借 1,但十位是 0,所以继续向百位借。百位的 8 借出 1 变成 7,十位得到 10(因为借的是 1 个百 = 10 个十),然后十位再借 1 个十给个位,十位变成 9,个位变成 10+3=13。13 - 6 = 7。 十位:9 - 5 = 4。 百位:7 - 0 = 7。 结果:747。

在代码里,我们用 borrow(借位)变量来模拟这个过程:

  • 一开始 borrow = 0(没借过)。
  • 对每一位,先算出 diff = 当前被减数的数字 - 减数的数字 - borrow
  • 如果 diff < 0,说明不够减,需要向高位借 1,于是 diff += 10,并且把 borrow 设为 1。
  • 否则,borrow 设为 0。
  • diff 存入结果数组。

注意:如果某个位置减数没有数字(比如 56 只有两位,处理百位时减数看作 0),我们就用 0 代替。

生活中的例子:零花钱差额

假设小明有 1203 元零花钱,小红有 567 元。小明比小红多多少?

   1 2 0 3
-     5 6 7
------------

按照竖式:

  • 个位:3 - 7 不够,向十位借,十位是 0,再向百位借,百位是 2,借 1 变成 1,十位变成 10,再借给个位 1,十位变 9,个位变 13,13-7=6。
  • 十位:9 - 6 = 3。
  • 百位:1(原来的2被借走了1) - 5 不够,向千位借,千位 1 变 0,百位加 10 变 11,11-5=6。
  • 千位:0 - 0 = 0。 结果:636。

程序会输出 636,表示小明多 636 元。

必须提前判断谁大谁小

高精度减法如果出现负数(例如用 56 减 803),结果就是负数。对于初学者,我们先处理非负结果,所以必须保证第一个数 >= 第二个数。判断方法:

  1. 长度不同:位数多的更大(比如 803 三位,56 两位,803 更大)。
  2. 长度相同:从最高位(数组的最后一位)开始比较,遇到第一个不同的数字,谁大谁就大。
  3. 全部相同:相等。

代码中的 ge 函数(greater or equal)就是做这个判断的。如果第一个数小于第二个数,我们就友好提示“暂不支持负结果”,然后退出。

去掉前导 0 —— 别让结果前面多一堆没用的 0

比如计算 100 - 99,竖式结果应该是 001,但我们要输出 1。因为倒着存,结果数组最后几位(最高位)可能有多余的 0。我们需要从后往前删除这些 0,直到只剩一位(防止结果本身就是 0)。

代码中这样处理:

while (c.size() > 1 && c.back() == 0) c.pop_back();

新手常犯的错误

  1. 忘记处理借位传递:如果当前位不够减,只加了 10,但忘记把 borrow 设为 1,下一位就会出错。记住:借了就要记下来。
  2. 借位后忘了减去借出的 1:当上一个位有借位时,当前位要先减去 borrow(即上一位借出去的 1)。
  3. 比较大小写反了ge 函数里 return a.size() > b.size() 如果写成了 <,就会把大数当小数,导致减法结果错误。
  4. 忘记去掉前导 0:比如 100 - 99,如果不删除,结果会输出 001,看起来很奇怪。
  5. 字符串转数字时顺序搞反a.push_back(s1[i] - '0') 一定要从字符串的最后一个字符开始循环,否则数组顺序就反了。

完整可运行的代码(附详细注释)

下面的程序实现了两个大整数的减法(保证结果非负)。你可以输入两个很大的数,看看它能不能正确算出减法。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

// 比较两个vector表示的数,返回 true 如果 a >= b
bool ge(vector<int> &a, vector<int> &b) {
    if (a.size() != b.size()) return a.size() > b.size();     // 位数多的一定大
    for (int i = a.size() - 1; i >= 0; i--) {                 // 从最高位开始比
        if (a[i] != b[i]) return a[i] > b[i];
    }
    return true; // 全部相等,认为 a >= b
}

// 高精度减法:计算 a - b,要求 a >= b
vector<int> sub(vector<int> &a, vector<int> &b) {
    vector<int> c;           // 存放结果(倒着)
    int borrow = 0;          // 借位标记,0表示没借,1表示借过
    for (int i = 0; i < a.size(); i++) {
        int va = a[i];                     // 当前被减数的这一位
        int vb = (i < b.size()) ? b[i] : 0; // 减数的这一位,如果b没有这一位就当0
        int diff = va - vb - borrow;       // 计算差值,先减去借位
        if (diff < 0) {                    // 不够减,需要借位
            diff += 10;                    // 借1当10
            borrow = 1;                    // 标记借位,下一位要减1
        } else {
            borrow = 0;                    // 够减,不用借位
        }
        c.push_back(diff);                 // 把当前位结果放到数组里
    }
    // 去掉前导0,但至少要保留一位(结果是0的情况)
    while (c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}

int main() {
    string s1, s2;                     // 用字符串读入两个大数
    cin >> s1 >> s2;
    vector<int> a, b;                  // 准备两个数组存数字(倒着)
    // 把字符串转成数字数组,个位放在下标0
    for (int i = s1.size() - 1; i >= 0; i--) a.push_back(s1[i] - '0');
    for (int i = s2.size() - 1; i >= 0; i--) b.push_back(s2[i] - '0');

    if (!ge(a, b)) {                   // 确保第一个数不小于第二个数
        cout << "第一个数小于第二个数,暂不支持负结果" << endl;
        return 0;
    }
    vector<int> c = sub(a, b);         // 做减法
    for (int i = c.size() - 1; i >= 0; i--) cout << c[i];   // 倒着输出结果
    cout << endl;
    return 0;
}

试试运行:输入 80356,程序输出 747。输入 10099,输出 1。输入 1203567,输出 636

接下来学什么?

  • 如果你学会了减法,可以挑战高精度加法,原理和减法很像,只是处理进位(carry)而不是借位。
  • 如果想把负数也处理好,可以在减法前加一个符号判断:如果第一个数小,就交换它们并输出负号。
  • 进一步还可以学习高精度乘法高精度除法,它们都用到了类似的数组存储和逐位操作的思想。

记住了:借位就像找邻居帮忙,一个邻居不够就找更远的邻居,耐心一点,一定能算对!

例题精讲

1单选题

在高精度减法中,当当前位被减数小于减数时,需要向高位借位,这个借位过程就像找邻居帮忙。假设我们使用的是十进制,当前位数字是3,减数是8,若向高位借1,则当前位变成多少?

A2
B4
C13
D18
2判断题

在高精度减法中,向高位借位后,高位的数字会自动减少1,不需要额外处理。

3填空题
下面是高精度减法函数的部分代码,数组a[]和b[]分别存储被减数和减数(低位在前),数组c[]存储结果。请在空格处填写正确的借位处理语句。

for (int i = 0; i < n; i++) {
    if (a[i] < b[i]) {
        ___;
        ___;
    }
    c[i] = a[i] - b[i];
}
4单选题

在高精度减法中,如果向高位借位后发现高位已经是0,无法直接借出,应该怎么办?

A向更高位借位
B将高位变为9并继续向更高位借位
C直接放弃计算
D将结果当前位设为0
5判断题

在高精度减法中,借位操作完成后,被借位的高位数值减1,而当前位数值加10,这样保证了数值的正确性。