用 % 10 和 / 10 一位一位"拆开"一个整数——这是几乎所有数字类竞赛题的起点工具。
在 C++ 里,一个 int 类型的变量 12345 在内存里只是一段二进制数据——它并不知道自己"有 5 位",也不知道"个位是 5、十位是 4"。"数位"这个概念,是我们人类用十进制书写数字时才有的——计算机眼里只有"这个数的大小",没有"第几位是几"这种结构。
所以,如果题目要求你"求各位数字之和""判断是不是回文数""统计某个数字出现了几次",就必须先把这个整数还原成一位一位的数字。而做到这件事,只需要两个最基础的运算:% 10(取余)和 / 10(整除)。
% 10 和 / 10 好用的根本原因——十进制里所有比"个位"更高的位,对 10 取余都贡献 0;而整除 10,相当于把整个数字"右移一位",自然就甩掉了最右边那一位。/10)就能看到下一节(十位),每剥一节之前先看一眼这一节是什么(%10)。拆数字的过程,就是不断"剥洋葱",从最外层(个位)剥到最内层(最高位)。最基础的写法——一个 while 循环,每次取出当前的个位,再用整除去掉这一位,直到 n 变成 0 为止:
| 1 | // 拆分每一位(倒序输出) |
| 2 | while (n != 0) |
| 3 | { |
| 4 | int digit = n % 10; // 取出个位 |
| 5 | cout << digit << " "; // 输出当前位 |
| 6 | n /= 10; // 去掉个位 |
| 7 | } |
用 n = 12345 走一遍这个循环,每一步 n 都在变小,每一步都"吐出"一个数字:
| 循环前的 n | digit = n % 10 | n /= 10 之后 | ||
|---|---|---|---|---|
| 12345 | 取出 → | 5 | → 变成 | 1234 |
| 1234 | 取出 → | 4 | → 变成 | 123 |
| 123 | 取出 → | 3 | → 变成 | 12 |
| 12 | 取出 → | 2 | → 变成 | 1 |
| 1 | 取出 → | 1 | → 变成 | 0 |
| n = 0,循环结束 —— 输出顺序:5 4 3 2 1 | ||||
n = 0,循环条件 n != 0 一开始就不满足,循环体一次都不会执行——如果题目要求"输入 0 时也要输出 0",需要在循环外单独判断并处理这个特殊情况。n != 0 而不是 n > 0?因为 C++ 里负数也是可以正常拆分的——只要循环条件写成 n != 0,无论 n 是正数还是负数,/ 10 都会让它一步步逼近 0,最终精确停在 0 上。如果写成 n > 0,负数永远不满足这个条件,循环会被直接跳过、什么都拆不出来——这往往不是我们想要的效果。| 循环前的 n | digit = n % 10 | n /= 10 之后 | ||
|---|---|---|---|---|
| -123 | 取出 → | -3 | → 变成 | -12 |
| -12 | 取出 → | -2 | → 变成 | -1 |
| -1 | 取出 → | -1 | → 变成 | 0 |
| n = 0,循环结束 —— 取出的每一位:-3 -2 -1 | ||||
n != 0 同样能让负数的循环正常结束。不过要注意:取出来的每一位本身带着负号(-3 而不是 3),这是因为 C++ 里负数对正数取余,结果的符号和被除数一致(-123 % 10 == -3)。如果题目只关心数字本身、不关心符号,可以在拆之前先记下符号、再用 n = abs(n) 转成正数处理,或者每次取出 digit 后用 abs(digit) 拿到不带符号的数字。| 1 | int n = 12345, sum = 0; |
| 2 | while (n != 0) |
| 3 | { |
| 4 | sum += n % 10; // 累加当前位,而不是输出 |
| 5 | n /= 10; |
| 6 | } |
| 7 | // sum = 1+2+3+4+5 = 15 |
同样的模板还能用来数一个数有几位(每循环一次计数器加 1)、统计某个数字出现了几次(每次判断 digit 是否等于目标数字)。记住这一个 while 循环的骨架,后面绝大多数"按位处理"的题目都是在这个骨架里改一行代码。
基础写法只是把每一位"读出来",不会保存。如果想把读出来的数字重新拼成一个新的整数,就要用到这个变体——反转数字:
| 1 | int ReverseNumber(int n) |
| 2 | { |
| 3 | int rev = 0; |
| 4 | while (n != 0) { |
| 5 | rev = rev * 10 + n % 10; // 把之前的反转结果左移一位,加上新个位 |
| 6 | n /= 10; |
| 7 | } |
| 8 | return rev; |
| 9 | } |
| 10 | // ReverseNumber(12345) → 54321 |
关键就在第 5 行这一句:rev = rev * 10 + n % 10。rev * 10 的作用是把 rev 现有的每一位整体向左挪一位(腾出最右边的位置),然后再把新取出来的 n % 10 填进腾出来的最右边——这正好和基础写法是反过来的操作:基础写法是"剥掉"最右边一位,这里是"新增"一位到最右边。
| n(待拆分) | 取出的数字 | rev(结果累积) | ||
|---|---|---|---|---|
| 12345 | → | 5 | → | 0×10+5 = 5 |
| 1234 | → | 4 | → | 5×10+4 = 54 |
| 123 | → | 3 | → | 54×10+3 = 543 |
| 12 | → | 2 | → | 543×10+2 = 5432 |
| 1 | → | 1 | → | 5432×10+1 = 54321 |
| n = 0,循环结束 —— 最终 rev = 54321 | ||||
rev 在右边"接上"新取出的数字——而 n 每次拆出来的,正好是原数从右往左数的下一位。所以拼出来的 rev,数位顺序正好和原数完全相反。1200),反转之后这些 0 应该跑到结果的最前面,但"数字"是不能有前导 0 的(0021 不是一个合法的整数写法,它就是 21)。这个算法刚好自动处理好了这件事,完全不需要你额外写代码去删——往下看具体是怎么发生的。| n(待拆分) | 取出的数字 | rev(结果累积) | ||
|---|---|---|---|---|
| 1200 | → | 0 | → | 0×10+0 = 0 |
| 120 | → | 0 | → | 0×10+0 = 0 |
| 12 | → | 2 | → | 0×10+2 = 2 |
| 1 | → | 1 | → | 2×10+1 = 21 |
| n = 0,循环结束 —— 最终 rev = 21(不是 0021!) | ||||
rev 里"写入"任何东西。直到取到第一个非 0 数字(这里是 2),rev 才真正开始累积数值。这就是为什么前导 0 不需要手动删除——普通整数本来就不会保留没有意义的前导 0,这是整数(int)这种数据类型自带的性质,而不是字符串。ReverseNumber 把它反转一遍,再跟原数比较是否相等——下一节我们就会用到这个工具。有时候我们不想"反转"数字,只是想修改其中某一位,同时让其余各位都待在原来的位置不动——比如"把一个数里所有的 4 都换成 8"。这就需要第三种写法:
| 1 | int n, x = 0, i = 1; |
| 2 | cin >> n; |
| 3 | while (n != 0) |
| 4 | { |
| 5 | int t = n % 10; |
| 6 | if(t == 4) |
| 7 | { |
| 8 | t = 8; // 将原始数字中的所有4替换为8 |
| 9 | } |
| 10 | x += t * i; |
| 11 | n /= 10; |
| 12 | i *= 10; |
| 13 | } |
| 14 | cout << x; |
这里多了一个变量 i,从 1 开始,每循环一次乘以 10(1 → 10 → 100 → 1000 → ...)——它的作用是记住当前取出的这一位,原本站在哪个位置上。第一次取出的是个位,i = 1;第二次取出的是十位,i = 10;以此类推。把每个数字乘上它"原本的位权" i 再累加进 x,就能让数字精确落回原来的位置,而不会像变体一那样被颠倒顺序。
i 再加进 x,所以它们各自落进的位置始终和原数一致——最终 x = 1238,正好是 1234 把 4 换成 8 之后的样子。rev = rev * 10 + digit——乘的是累加器 rev 本身,每加一位都把之前的结果统一往左挪,所以顺序会反过来。变体二是 x += t * i,i 单独累乘 10——乘的是位权 i,不是累加器,每个数字直接乘上它该在的位权后加进去,顺序当然不会变。两段代码长得有点像(都有"乘 10"的操作),但乘的对象不同,效果天差地别,一定要看清楚乘号作用在谁身上。这三种写法的循环骨架几乎一样(都是 while(n != 0) { ... n /= 10; }),区别只在循环体里多了什么、少了什么,整理成一张表方便记忆:
| 写法 | 核心语句 | 结果顺序 | 典型用途 |
|---|---|---|---|
| 基础拆分 | digit = n % 10(只取,不存) | 个位先被取出(天然倒序) | 逐位输出、求数字和、统计某位数字出现次数 |
| 反转数字 | rev = rev * 10 + digit | 整体顺序反转 | 判断回文数、数字反转类题目 |
| 保序替换 | x += t * i;i *= 10 | 保持原顺序不变 | 修改/统计某一位,同时保留原数的"长相" |
% 10 取一位、用 / 10 去一位"的循环模板,是后面回文判断、进制转换、数位 DP等一大批题目的地基。三种写法的差别全部体现在"取出来的数字怎么处理"这一行上——把这三种处理方式吃透了,遇到新的"按位处理"问题时,往往只是把这一行换成新的逻辑,骨架完全不用动。