← 目录 / 算法文档 · 模块一 数学基础 / 1.1 数位拆分

1.1 数位拆分

% 10/ 10 一位一位"拆开"一个整数——这是几乎所有数字类竞赛题的起点工具。

本页目录
① 为什么要"拆数字"

在 C++ 里,一个 int 类型的变量 12345 在内存里只是一段二进制数据——它并不知道自己"有 5 位",也不知道"个位是 5、十位是 4"。"数位"这个概念,是我们人类用十进制书写数字时才有的——计算机眼里只有"这个数的大小",没有"第几位是几"这种结构。

所以,如果题目要求你"求各位数字之和""判断是不是回文数""统计某个数字出现了几次",就必须先把这个整数还原成一位一位的数字。而做到这件事,只需要两个最基础的运算:% 10(取余)和 / 10(整除)。

12345 的位值分解 —— 每一位数字都乘以它的"位权"
1
×10000
万位
2
×1000
千位
3
×100
百位
4
×10
十位
5
×1
个位
12345 = 1×10000 + 2×1000 + 3×100 + 4×10 + 5×1
12345 % 10
前面 4 项全是 10 的倍数,除以 10 都余 0,只剩下个位单独留下来
= 5
12345 / 10
整除会把个位直接"切掉",剩下每一位的位权整体降一级
= 1234
这就是 % 10/ 10 好用的根本原因——十进制里所有比"个位"更高的位,对 10 取余都贡献 0;而整除 10,相当于把整个数字"右移一位",自然就甩掉了最右边那一位。
💡
类比理解:可以把整数想象成一节一节套起来的"俄罗斯套娃"——最外面(最右边)那一节是个位,剥开一节(/10)就能看到下一节(十位),每剥一节之前先看一眼这一节是什么(%10)。拆数字的过程,就是不断"剥洋葱",从最外层(个位)剥到最内层(最高位)。
② 基础写法:从右往左取出每一位

最基础的写法——一个 while 循环,每次取出当前的个位,再用整除去掉这一位,直到 n 变成 0 为止:

C++ · 拆分每一位(倒序输出)
1// 拆分每一位(倒序输出)
2while (n != 0)
3{
4 int digit = n % 10; // 取出个位
5 cout << digit << " "; // 输出当前位
6 n /= 10; // 去掉个位
7}

n = 12345 走一遍这个循环,每一步 n 都在变小,每一步都"吐出"一个数字:

n = 12345 时,while 循环的每一步
循环前的 ndigit = n % 10n /= 10 之后
12345取出 →5→ 变成1234
1234取出 →4→ 变成123
123取出 →3→ 变成12
12取出 →2→ 变成1
1取出 →1→ 变成0
n = 0,循环结束 —— 输出顺序:5 4 3 2 1
注意输出顺序是 5 4 3 2 1,正好是 12345 的倒序——这是因为每次取出来的都是"当前最右边"的那一位,而个位本来就是原数最右边的那一位,所以第一个被取出来的,正好是原数的最后一位。
⚠️
一个容易忽略的细节:如果 n = 0,循环条件 n != 0 一开始就不满足,循环体一次都不会执行——如果题目要求"输入 0 时也要输出 0",需要在循环外单独判断并处理这个特殊情况。
💡
为什么用 n != 0 而不是 n > 0因为 C++ 里负数也是可以正常拆分的——只要循环条件写成 n != 0,无论 n 是正数还是负数,/ 10 都会让它一步步逼近 0,最终精确停在 0 上。如果写成 n > 0,负数永远不满足这个条件,循环会被直接跳过、什么都拆不出来——这往往不是我们想要的效果。
n = -123 时,while (n != 0) 同样能正常拆完
循环前的 ndigit = n % 10n /= 10 之后
-123取出 →-3→ 变成-12
-12取出 →-2→ 变成-1
-1取出 →-1→ 变成0
n = 0,循环结束 —— 取出的每一位:-3 -2 -1
C++ 里整数除法是向零取整的,所以负数反复除以 10 也会一步步靠近 0,并且精确落在 0 上——这就是为什么 n != 0 同样能让负数的循环正常结束。不过要注意:取出来的每一位本身带着负号-3 而不是 3),这是因为 C++ 里负数对正数取余,结果的符号和被除数一致(-123 % 10 == -3)。如果题目只关心数字本身、不关心符号,可以在拆之前先记下符号、再用 n = abs(n) 转成正数处理,或者每次取出 digit 后用 abs(digit) 拿到不带符号的数字。
📌
最常见的应用——求各位数字之和:把"输出 digit"换成"累加 digit",就能算出一个数所有数位之和:
C++ · 求各位数字之和
1int n = 12345, sum = 0;
2while (n != 0)
3{
4 sum += n % 10; // 累加当前位,而不是输出
5 n /= 10;
6}
7// sum = 1+2+3+4+5 = 15

同样的模板还能用来数一个数有几位(每循环一次计数器加 1)、统计某个数字出现了几次(每次判断 digit 是否等于目标数字)。记住这一个 while 循环的骨架,后面绝大多数"按位处理"的题目都是在这个骨架里改一行代码。

③ 变体一:反转一个整数

基础写法只是把每一位"读出来",不会保存。如果想把读出来的数字重新拼成一个新的整数,就要用到这个变体——反转数字

C++ · ReverseNumber 反转整数
1int 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 % 10rev * 10 的作用是把 rev 现有的每一位整体向左挪一位(腾出最右边的位置),然后再把新取出来的 n % 10 填进腾出来的最右边——这正好和基础写法是反过来的操作:基础写法是"剥掉"最右边一位,这里是"新增"一位到最右边。

ReverseNumber(12345) 逐步执行过程
n(待拆分)取出的数字rev(结果累积)
1234550×10+5 = 5
123445×10+4 = 54
123354×10+3 = 543
122543×10+2 = 5432
115432×10+1 = 54321
n = 0,循环结束 —— 最终 rev = 54321
每一步 rev 在右边"接上"新取出的数字——而 n 每次拆出来的,正好是原数从右往左数的下一位。所以拼出来的 rev,数位顺序正好和原数完全相反
💡
一个意外的好处——自动去掉"多余的 0":如果原数末尾本身就有 0(比如 1200),反转之后这些 0 应该跑到结果的最前面,但"数字"是不能有前导 0 的(0021 不是一个合法的整数写法,它就是 21)。这个算法刚好自动处理好了这件事,完全不需要你额外写代码去删——往下看具体是怎么发生的。
ReverseNumber(1200) —— 末尾的 0 是怎么"消失"的
n(待拆分)取出的数字rev(结果累积)
120000×10+0 = 0
12000×10+0 = 0
1220×10+2 = 2
112×10+1 = 21
n = 0,循环结束 —— 最终 rev = 21(不是 0021!)
前两步取出的 0,乘 10 再加 0,结果一直是 0,并没有真的往 rev 里"写入"任何东西。直到取到第一个非 0 数字(这里是 2),rev 才真正开始累积数值。这就是为什么前导 0 不需要手动删除——普通整数本来就不会保留没有意义的前导 0,这是整数(int)这种数据类型自带的性质,而不是字符串。
🎯
预告:判断一个数是不是回文数(正读反读都一样,比如 12321),最直接的办法就是用 ReverseNumber 把它反转一遍,再跟原数比较是否相等——下一节我们就会用到这个工具。
④ 变体二:保留原序,替换某一位

有时候我们不想"反转"数字,只是想修改其中某一位,同时让其余各位都待在原来的位置不动——比如"把一个数里所有的 4 都换成 8"。这就需要第三种写法:

C++ · 保持原序,替换数字中所有的 4 为 8
1int n, x = 0, i = 1;
2cin >> n;
3while (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}
14cout << x;

这里多了一个变量 i,从 1 开始,每循环一次乘以 10(1 → 10 → 100 → 1000 → ...)——它的作用是记住当前取出的这一位,原本站在哪个位置上。第一次取出的是个位,i = 1;第二次取出的是十位,i = 10;以此类推。把每个数字乘上它"原本的位权" i 再累加进 x,就能让数字精确落回原来的位置,而不会像变体一那样被颠倒顺序。

把 1234 中的 4 替换成 8 —— 数字按位权"落回"原来的位置
第 1 步:取出个位 t=4 → 替换成 8,i=1,x += 8×1 = 8
·
千位
·
百位
·
十位
8
个位
第 2 步:取出十位 t=3,i=10,x += 3×10 = 30 → x = 38
·
千位
·
百位
3
十位
8
个位
第 3 步:取出百位 t=2,i=100,x += 2×100 = 200 → x = 238
·
千位
2
百位
3
十位
8
个位
第 4 步:取出千位 t=1,i=1000,x += 1×1000 = 1000 → x = 1238,n 变 0,循环结束
1
千位
2
百位
3
十位
8
个位
虽然取出数字的时间顺序是个位 → 十位 → 百位 → 千位(从右往左),但因为每一位都乘上了自己"原本的位权" i 再加进 x,所以它们各自落进的位置始终和原数一致——最终 x = 1238,正好是 1234 把 4 换成 8 之后的样子。
⚠️
和变体一最容易混淆的地方:变体一是 rev = rev * 10 + digit——乘的是累加器 rev 本身,每加一位都把之前的结果统一往左挪,所以顺序会反过来。变体二是 x += t * ii 单独累乘 10——乘的是位权 i,不是累加器,每个数字直接乘上它该在的位权后加进去,顺序当然不会变。两段代码长得有点像(都有"乘 10"的操作),但乘的对象不同,效果天差地别,一定要看清楚乘号作用在谁身上。
⑤ 三种写法对比

这三种写法的循环骨架几乎一样(都是 while(n != 0) { ... n /= 10; }),区别只在循环体里多了什么、少了什么,整理成一张表方便记忆:

写法核心语句结果顺序典型用途
基础拆分digit = n % 10(只取,不存)个位先被取出(天然倒序)逐位输出、求数字和、统计某位数字出现次数
反转数字rev = rev * 10 + digit整体顺序反转判断回文数、数字反转类题目
保序替换x += t * i;i *= 10保持原顺序不变修改/统计某一位,同时保留原数的"长相"
🎯
这套"用 % 10 取一位、用 / 10 去一位"的循环模板,是后面回文判断、进制转换、数位 DP等一大批题目的地基。三种写法的差别全部体现在"取出来的数字怎么处理"这一行上——把这三种处理方式吃透了,遇到新的"按位处理"问题时,往往只是把这一行换成新的逻辑,骨架完全不用动。