当计算机不会除法时,我们怎么教它算 123456789 ÷ 123?
先抛一个反直觉的事实:CPU 的算术逻辑单元里,其实并没有一条“除法指令”是天生就高效的。整数除法在所有基础运算里是最慢的——它本质上是一个“猜 + 减 + 修正”的迭代过程。硬件层面如此,算法层面也一样。
所以当你需要算一个 100 位的大整数除以一个普通 long long 时,别指望有什么魔法。最靠谱的方法,就是回到小学三年级:列竖式,逐位试商。
竖式的本质:把“除法”降维成“减法”
我们来看 123456789 ÷ 123 的竖式是怎么走的。关键在于,你从来不需要一步算出“123456789 里面有几个 123”。你只需要每次盯住一小段数字,问一个很弱的问题:
当前这个数,够减几次 123?
- 取前三位
123,够减 1 次,商 1,余 0。 - 拉下
4,得到4,不够减,商 0,余 4。 - 拉下
5,得到45,不够减,商 0,余 45。 - 拉下
6,得到456,够减 3 次(456 - 369 = 87),商 3,余 87。 - ……
每一步的商最多是 9,因为如果够减 10 次,说明你上一步取的位数不够,应该多取一位。这个性质非常重要——它保证了“试商”这个动作的代价是常数级的。
用代码表达这个“拉下一位”的动作,就一行:
remainder = remainder * 10 + (ch - '0');
这行代码是整个算法的灵魂。它把“把下一位数字接在余数后面”这个竖式动作,翻译成了乘 10 加个位。然后:
int q = remainder / b; // 这一位的商
remainder = remainder % b; // 新的余数
注意,这里 remainder 始终小于 b,所以 remainder * 10 + 新数字 最大也就是 10b 左右,不会溢出(前提是 b 本身在 long long 范围内且不接近上限)。
一道题正好戳中这个关键点
有一道题问:用减法模拟长除法时,确定商的某一位的正确方法是什么?选项里有一个很诱人的错误答案——“直接用被除数剩余部分除以除数”。这在高精度场景下是循环论证:我们之所以要用减法模拟,就是因为被除数太大,没法直接做除法。如果 remainder 本身就是一个几百位的大数,remainder / b 这个操作你打算怎么实现?还不是得回到减法。
所以正确的做法是:反复减去除数,每减一次计数加一,直到剩余部分小于除数。这个计数就是当前位的商。由于商位只能是 0~9,循环最多跑 10 次,效率完全可接受。
这也解释了为什么高精度除法的时间复杂度大致是 O(n × 9),n 是被除数的位数。每一步的试商代价是常数。
除数为 0:一个不能省的检查
另一道判断题说“用减法模拟长除法时,不需要考虑除数为 0 的情况”。这个说法错得很典型。
有人觉得:我又没写 / 或 %,只是反复减,怎么会出问题?问题恰恰出在“反复减”上。如果除数是 0,那么 remainder 永远不会小于除数(因为任何非负数都不小于 0),循环条件永远为真,程序直接死循环。这不是“数学上无意义”这么轻描淡写,而是实打实的运行时灾难。
所以无论你用哪种方式实现除法——直接运算符也好,减法模拟也好——除数检查都是第一道防线:
if (b == 0) {
cout << "Error: division by zero" << endl;
return 1;
}
这不是防御性编程的教条,而是算法逻辑本身的必然要求。
前导零:一个容易被忽略的边界
逐位算完之后,商是以 vector<int> 的形式按高位到低位存储的。比如 99 ÷ 100,每一位的商都是 0,得到 [0, 0]。如果你直接输出,会打印 00,这显然不对。
处理方式是从头扫描,跳过所有前导零,但要注意:如果跳完之后发现没有数字了,说明商就是 0,必须输出一个 0,而不是什么都不输出。
int start = 0;
while (start < quotient.size() && quotient[start] == 0) start++;
if (start == quotient.size()) cout << 0;
else for (int i = start; i < quotient.size(); i++) cout << quotient[i];
这个边界在竞赛里是常见的失分点,因为它只在“被除数小于除数”时触发,而很多人的测试用例恰好没覆盖到这种情况。
这套思路能走多远
减法模拟长除法只解决了“高精度除以低精度”的场景。如果除数本身也是高精度数,你就不能再用 remainder / b 和 remainder % b 了,得把试商也变成减法循环,或者用二分法加速。但核心思想没变:把除法拆成逐位的减法和移位。
理解了这一点,你就理解了为什么计算机做除法比做加法慢一个数量级——它本质上是在用迭代逼近一个本来“应该一步到位”的运算。
下次遇到大数除法,别慌。想想竖式,想想那个 remainder * 10 + digit,你会发现小学数学比你想象的要深刻得多。
关于作者
我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。
这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。
如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)