高精度减法——借位就像找邻居帮忙
较难18高精度减法:像邻居借位一样简单
计算机里的整数有大小限制(比如 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),结果就是负数。对于初学者,我们先处理非负结果,所以必须保证第一个数 >= 第二个数。判断方法:
- 长度不同:位数多的更大(比如 803 三位,56 两位,803 更大)。
- 长度相同:从最高位(数组的最后一位)开始比较,遇到第一个不同的数字,谁大谁就大。
- 全部相同:相等。
代码中的 ge 函数(greater or equal)就是做这个判断的。如果第一个数小于第二个数,我们就友好提示“暂不支持负结果”,然后退出。
去掉前导 0 —— 别让结果前面多一堆没用的 0
比如计算 100 - 99,竖式结果应该是 001,但我们要输出 1。因为倒着存,结果数组最后几位(最高位)可能有多余的 0。我们需要从后往前删除这些 0,直到只剩一位(防止结果本身就是 0)。
代码中这样处理:
while (c.size() > 1 && c.back() == 0) c.pop_back();
新手常犯的错误
- 忘记处理借位传递:如果当前位不够减,只加了 10,但忘记把
borrow设为 1,下一位就会出错。记住:借了就要记下来。 - 借位后忘了减去借出的 1:当上一个位有借位时,当前位要先减去
borrow(即上一位借出去的 1)。 - 比较大小写反了:
ge函数里return a.size() > b.size()如果写成了<,就会把大数当小数,导致减法结果错误。 - 忘记去掉前导 0:比如
100 - 99,如果不删除,结果会输出001,看起来很奇怪。 - 字符串转数字时顺序搞反:
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;
}
试试运行:输入 803 和 56,程序输出 747。输入 100 和 99,输出 1。输入 1203 和 567,输出 636。
接下来学什么?
- 如果你学会了减法,可以挑战高精度加法,原理和减法很像,只是处理进位(
carry)而不是借位。 - 如果想把负数也处理好,可以在减法前加一个符号判断:如果第一个数小,就交换它们并输出负号。
- 进一步还可以学习高精度乘法和高精度除法,它们都用到了类似的数组存储和逐位操作的思想。
记住了:借位就像找邻居帮忙,一个邻居不够就找更远的邻居,耐心一点,一定能算对!
例题精讲
在高精度减法中,当当前位被减数小于减数时,需要向高位借位,这个借位过程就像找邻居帮忙。假设我们使用的是十进制,当前位数字是3,减数是8,若向高位借1,则当前位变成多少?
在高精度减法中,向高位借位后,高位的数字会自动减少1,不需要额外处理。
下面是高精度减法函数的部分代码,数组a[]和b[]分别存储被减数和减数(低位在前),数组c[]存储结果。请在空格处填写正确的借位处理语句。
for (int i = 0; i < n; i++) {
if (a[i] < b[i]) {
___;
___;
}
c[i] = a[i] - b[i];
}在高精度减法中,如果向高位借位后发现高位已经是0,无法直接借出,应该怎么办?
在高精度减法中,借位操作完成后,被借位的高位数值减1,而当前位数值加10,这样保证了数值的正确性。