带二级的学生时,我经常遇到一个现象:讲到循环、分支、数组,孩子们都能跟上;一碰到“把一个整数拆成一位一位的”这种题,教室里就安静了。后来我总结了一句话——拆数字不是一道题,而是一组模板,你只要把“怎么拆”和“拆完干什么”两件事分开想,这个题型基本就通关了。这篇文章就是把我给备考学生整理的东西写出来:从最基础的数位分离,到反转、回文、数位和、水仙花数、计数排序这些高频变形,再把我踩过和看过学生踩过的边界坑一次说清。不管你是准备GESP二级的考生,还是带孩子备考的家长或老师,照着这篇文章练,数字拆分相关的大题小题都能稳住。
1. “拆数字”为什么会成为GESP二级的必考题型
1.1 从考纲看拆数字的考点定位
GESP二级考纲覆盖的核心内容其实高度集中在几个点上:顺序结构、分支结构、循环结构、一维数组的简单应用。你把这四个关键词放在一起看,拆数字几乎就是为它们量身定做的综合题。一个整数要拆开,你得用循环;每一次循环里要取余数、做除法,这是数学运算和赋值操作;拆出来之后往往要判断大小、奇偶、是否相等,这又落到分支结构上;如果要把拆出的每一位存起来、统计次数、排序拼数,那就直接升级到一维数组的应用了。
换句话说,拆数字是二级考纲里极少数能把“循环+分支+数组”三个模块串联到一个题里的题型。出题人自然也喜欢用它做大题的骨架,因为一道题就能把一整个学期的基础知识都考到。我常和学生说,二级能不能稳过,很多时候就看你对拆数字这套动作熟不熟。它不像贪心、递归那样需要“灵光一闪”,它靠的是肌肉记忆和细节意识,练到位了就是送分题。
1.2 拆数字题在考卷上的常见变身
很多学生觉得拆数字难,是因为每次见到的题好像长得都不一样。我把备考这几年刷过的题目做了一次归类,发现脱掉外壳之后,核心动作都一样。
- 数位分离题:输入一个整数,输出每一位数字,可能是从低到高,也可能是从高到低。
- 数位和题:输入一个整数,输出它的各位数字之和,或者统计1到n之间有多少个数满足数位和的条件。
- 回文数题:判断一个整数从左往右读和从右往左读是否一样,本质就是“拆出来再做比较”。
- 水仙花数题:判断一个三位数的每一位立方和是否等于它本身。
- 数字出现次数题:统计某个数字在给定整数里出现了多少次,或者找出现次数最多的数字。
- 数字重组题:把一个整数的每一位拆出来,排序后组成最大数或最小数。
- 位数统计题:输入一个整数,输出它是几位数,或者输出某一位上的数字。
这些题,拆的动作都是同一个模板,区别只在拆完之后那段代码。可惜不少学生在第一步“拆”上就卡住了,后面自然什么都连不起来。我建议你先把第二章的基础模板练到闭着眼睛写出来,再去做后面的变形题。
2. 数位分离:三套基础模板与适用边界
2.1 模板一:除十取模动态循环
这是拆数字最核心、最通用的模板,也是我要求学生必须能默写出来的第一段代码。
int n; cin >> n; while (n > 0) { int d = n % 10; // 这里处理数字 d n /= 10; }原理其实很简单:十进制整数除以10的余数就是它的个位,除以10之后的商则是把个位“扔掉”后剩下的部分。循环执行下去,每次取的就是当前最低位,循环结束时n就变成了0。这个模板天然是从低位到高位逐个取出数字的。
我用一个例子带学生走一遍:n=1234。第一次循环,n%10得到4,n/=10后变成123;第二次循环,n%10得到3,n变成12;第三次得到2,n变成1;第四次得到1,n变成0,循环结束。整个过程一共取出4、3、2、1,正好是1234从低位到高位的每一位。
这段代码里有三个细节需要特别注意。
第一,n=0时不进入循环,也就是说这段代码默认输入是正整数。如果题目允许输入0,就要改用do-while结构,我在第五章会专门讲。
第二,变量n在这里被修改了,循环结束后你手里只有拆出来的每一位数字,原来的数已经没了。如果你后面还需要用到原始数值,一定要在循环前另外存一份副本。
第三,循环里取出来的d就是当前想处理的那一位,你可以直接打印、累加、判断、存进数组,怎么处理都行,不影响循环本身。
2.2 模板二:字符串转换法
有些题目天生适合用字符串而不是整数来处理数字。比如题目说“输入一个四位数,每位之间可能包含前导零”,你用整数读入,0012自动变成了12,前导零的信息就丢了。这时候用字符串读入,每一位字符天然就是数字的一位。
string s; cin >> s; for (int i = 0; i < (int)s.size(); i++) { int d = s[i] - '0'; // 处理数字 d }核心一行是d = s[i] - '0'。C++里字符'0'到'9'在ASCII码表中是连续排列的,字符'3'减去字符'0',得到的正好是整型3。这种写法我在上课的时候会反复强调:千万别直接写s[i] = (int)s[i],那样取出来的是ASCII码,不是数字本身。
字符串模板的最大优势是能原样保留每一位,包括前导零和超长整数。判断一个超长整数是不是回文数、输出每位数字时要求位数对齐,这种题用它就很省力。缺点就是如果你拆完还得做加法、乘法之类的数值运算,就要先把每一位重新组合成整数或者做高精度处理,多一层转来转去的功夫。平时练习时,不要因为“循环取模更高级”就排斥字符串法,工具没有高低,合适才是硬道理。
2.3 模板三:固定位数直接拆解
当题目明说了“输入一个三位整数”或者“输入两个二位数”时,还有个更快更直观的写法:直接用整除和取模定位某一位。以三位数n为例:
int bai = n / 100; // 百位 int shi = n / 10 % 10; // 十位 int ge = n % 10; // 个位这种方式不需要写循环,一眼就能看出哪一位对应哪个变量,代码也最短。水仙花数、判断三个数字能否组成三角形这类“位数固定”的题目,用它会很顺手。缺点是它完全锁死了位数,题目一旦没有明说输入范围,或者输入可能是两位数也可能是四位数,这套代码就会直接错掉。
所以我的建议是:模板一和模板二必须熟练掌握,模板三可以在确认位数固定之后放心使用,但不建议在考场上一上来就默认用固定位数拆法。先看清题目里有没有“三位整数”“四位数卡号”这类明确限定,再选工具。
3. 高频变形一网打尽:反转、回文、数位和
3.1 数字反转模板与前导零陷阱
数字反转是拆数字最自然的延伸:从低位往高位拆的过程中,把每一位“拼”到另一个数上,拼出来的数自然就是原数的逆序。
int rev = 0; while (n > 0) { rev = rev * 10 + n % 10; n /= 10; }这段代码的逻辑值得细品。rev一开始是0,第一次循环取出n的个位拼到rev上;下一次循环,rev先乘以10,给新的一位腾出个位的位置,然后加上当前取出的数字。以n=123为例:第一轮,rev=010+3=3;第二轮,rev=310+2=32;第三轮,rev=32*10+1=321。循环结束后rev就是321,完美反转。
但在GESP二级的练习和考试中,有一个很多人忽略的前提:题目到底期望怎么处理前导零。比如n=1200,按这个模板跑,rev的结果是21,因为原数末尾的两个0在反转时被当成最高位的0,自然消亡了。而如果题目想要的是“0021”这样的四位数结果,这个模板就不够。这时候需要回到字符串法,反转之后手动补零,或者根据原位数用输出格式控制来补。
更进一步,很多反转题不是只让你反转一个数,而是让你判断“一个数反转后与原数是否相等”,这就是回文数问题。反转模板练熟,这类题就迎刃而解。
3.2 回文数判断:反转比较法
回文数的定义是正着读和倒着读一样,比如121、12321、5665。判断回文数最直接的办法就是反转后比较原数和反转数是否相等。
int n; cin >> n; int original = n; int rev = 0; while (n > 0) { rev = rev * 10 + n % 10; n /= 10; } if (rev == original) { cout << "Yes" << endl; } else { cout << "No" << endl; }这里有一个我在学生作业里见得最多的低级错误:写完反转循环就直接拿n和rev比较。可这时候n已经被循环改成0了,比较结果永远是false。所以一定要在一开始设一个副本,比如原来的original,比较时用它。
另外还有一道常见变式:只判断回文但不要求你输出反转后的数。这种情况下就不需要真的把rev求出来,直接双指针比较字符串也行。
string s; cin >> s; int i = 0, j = (int)s.size() - 1; bool isPal = true; while (i < j) { if (s[i] != s[j]) { isPal = false; break; } i++; j--; } if (isPal) cout << "Yes" << endl; else cout << "No" << endl;两种写法都可以,但我建议备考二级的同学优先掌握反转比较法,因为反转本身也是一道常考题,你练一遍就同时覆盖了两个考点。字符串双指针适合题目已经用字符串读入的场景,比如中间夹杂了字母符号的情况很少出现在二级,但作为补充思路也可以用。
3.3 数位和与筛选类题目
数位和就是把拆出来的每一位累加:
int sum = 0; while (n > 0) { sum += n % 10; n /= 10; } cout << sum << endl;单看这一段,似乎比反转还简单。但真正有区分度的是它的“筛选类”变式,比如这道经典题:输入一个n,输出1到n之间数位和为偶数的数有多少个。这类题把枚举和拆数字组合在了一起,难点立刻从“会不会拆”变成了“拆的时候别破坏循环变量”。
正确的写法是这样的:
int n; cin >> n; int ans = 0; for (int i = 1; i <= n; i++) { int t = i; int sum = 0; while (t > 0) { sum += t % 10; t /= 10; } if (sum % 2 == 0) ans++; } cout << ans << endl;关键一步是int t = i;。很多学生图省事,直接while (i > 0),拆着拆着把i拆成了0,结果外层循环直接提前结束。这个bug非常隐蔽,因为单看一次循环结果好像没问题,但整个程序的输出就会比预期少一大截。我建议你每次做“对每个数都拆一遍”的题,都条件反射地先复制一份临时变量再去拆。
筛选类题还能继续扩展:统计m到n之间有多少个回文数、统计各位数字互不相同的三位数有多少个,本质都是“枚举+拆数字+条件判断”,模板不变。
4. 组合拳:拆完数字之后还能干什么
4.1 水仙花数与特殊数判断
水仙花数是一个经典到不能再经典的二级题:一个三位数,它等于各位数字的立方和,比如153 = 1^3 + 5^3 + 3^3。解法就是对每个三位数拆出百位、十位、个位再做判断。
for (int i = 100; i <= 999; i++) { int a = i / 100; int b = i / 10 % 10; int c = i % 10; if (a*a*a + b*b*b + c*c*c == i) { cout << i << " "; } }这里用固定位数模板很合适,因为题目限定了三位数。但如果你把它改成“四叶玫瑰数”(四位数各位四次方和),固定位数模板就得扩写成四个变量。有的同学会想到用循环模板处理任意位数,这也能做:先拆位,依次乘方累加,再判断是否等于原数。不过二级更常考的是三位水仙花,把固定拆法写熟就够。
这类“特殊数判断”远不止水仙花。完全数、自恋数、快乐数,拆数字都是它们的底层动作。二级阶段出现频率最高的是水仙花,但也有学校自编模拟题会考“各位数字平方和”,套路一模一样。你只要记住:拆完再算,算完再比。
4.2 统计每位数字出现次数
另一个高频组合是“拆数字+计数数组”。举例来说,输入一个整数n,统计各位中数字0到9的出现次数,输出哪个数字出现得最多。
int cnt[10] = {0}; int n; cin >> n; while (n > 0) { cnt[n % 10]++; n /= 10; }循环结束后,cnt[d]就是数字d在n中出现的次数。这个技巧最漂亮的点在于:数字只有0到9这十种可能,数组下标天然就是数字本身,不需要再做任何映射。拆数字、存次数,一行代码搞定一件事。
基于这个计数数组,你能立刻解决很多题:找出出现次数最多的数字、检查是否存在重复出现的数字、删掉重复数字后还剩几个不同数字。二级大题里“统计一个长整数里每个数字出现的次数”就是直接用这个模板。需要注意的是循环结束后n被清成了0,如果后面还要用到n,记得存副本。另外题目如果要求输出“出现次数最多的数字是哪一个”,当多个数字次数并列时,题目一般会规定输出最小的或者最大的,这种细节一定要读题时圈出来。
4.3 借助数组对拆出的数字排序
把拆出来的每一位数字存进数组再排序,是非常经典的二级进阶思路。比如:输入一个整数n(可能是个位数也可能是多位数),把它的每一位拆出来后,重新组合成一个最大的数。
int a[10], len = 0; int n; cin >> n; while (n > 0) { a[len++] = n % 10; n /= 10; } // 按从大到小排序 for (int i = 0; i < len - 1; i++) { for (int j = i + 1; j < len; j++) { if (a[i] < a[j]) { int t = a[i]; a[i] = a[j]; a[j] = t; } } } // 拼接成最大数 int res = 0; for (int i = 0; i < len; i++) { res = res * 10 + a[i]; } cout << res << endl;这里有几个容易踩的细节。第一,二级还没强制要求掌握快速排序,用双重循环冒泡或选择完全够用,代码还更好背。第二,如果要求组成最小数,按升序排完后直接拼,但当一个数字是0开头时(比如拆出了0和1),排出来是0、1,拼出来是1,这其实等价于把0放在前面但最终被当成前导零吞掉。若题目要求“组成一个最小的n位数,且0不能放在首位”,那就要先把最小的非零数字放到第一位,再拼剩下的数字。这种变形在二级里属于偏难的压轴题,你能看懂原理、会画步骤图就很好了。
5. 考场失分重灾区与避坑清单
5.1 n=0这个“隐形杀手”
很多孩子背模板背得很熟,结果一遇到0还是翻车。问题就藏在最普通的while (n > 0)里。如果输入n=0,循环体一次都不执行,程序相当于什么都没做。
0到底算不算“一位数”?严格说数学上0也有它自己的数位表示。二级题目如果输入范围写了“非负整数”而没有排除0,你就要处理。最简单的方案是把循环改成do-while:
int n; cin >> n; int sum = 0; do { sum += n % 10; n /= 10; } while (n > 0);这个结构保证了至少执行一次循环,n=0时也能拆出数字0。缺点是在n为正数时,它和while(n>0)的结果完全一致,所以干脆把你能控制的模板都统一用do-while,少一个边界就要少一分风险。不过要注意:如果你需要在循环里对n做分支判断,比如判断第一位是不是0,do-while也会多执行那一次,比如n=1000拆到最后的1之后还要再进来一次取到0,这时就要看题目具体需求了。
我的习惯是写while模板时单独加一行if (n == 0) { ... }先处理,再进入循环。两种方案二选一都行,关键是别让它成为你没考虑过的盲区。
5.2 负数进来怎么办
GESP二级的大多数题目都会写“输入一个正整数”,但我遇到过不少自制模拟卷和练习平台,会在输入范围里写“整数”,然后悄悄塞一个负数。你要是按照正整数模板直接写while(n>0),负数的每一位都拆不出来。
处理负数的方法非常粗暴:拆数字前取绝对值。
int n; cin >> n; if (n < 0) n = -n; while (n > 0) { // 正常拆位 }注意C++里负数取余的结果可能是负数,比如-123 % 10的结果是-3。所以必须先转成正数再拆,顺序不能反。另外,有些回文数题会规定负数的负号忽略,直接判断数字部分是否为回文,那也要先取绝对值。考场上一旦发现题目没说“正整数”,就条件反射地加上这个处理,基本就不会因为符号丢分。
5.3 反转溢出与long long
反转模板里rev = rev * 10 + n % 10;,看起来人畜无害,但有一个隐藏爆点:溢出的风险。C++的int类型最大值是2147483647,约21亿。一个十位数一旦反转,很可能就超过这个上限。比如2147483647反转过来是7463847412,直接爆掉。
GESP二级的输入值一般不会设计到这种极限,但“拆数字之后拼一个更大数”的场景,比如5.1里那种重组最大数,也有可能在边界数据上溢出。我的建议很实际:凡是题目没明确说n不超过10^9的,反转和拼接的变量一律开long long,哪怕输入是int也能正常读入,输出用cout也无需额外处理格式。
long long n; cin >> n; long long rev = 0; while (n > 0) { rev = rev * 10 + n % 10; n /= 10; }这个习惯一旦养成了,后面学高精度、学递推,都不会在类型上翻车。另外要注意,long long整除法在正数范围内行为正常,但如果你直接用abs(n)函数,头文件要包含<cstdlib>或<cmath>,二级考场有的环境默认头文件不全,用流水账写法自己取绝对值更稳。
5.4 别用循环变量直接拆位
这个问题我在3.3里已经点过,但值得再单独拎出来,因为它实在太频繁了。它的典型错误代码长这样:
for (int i = 1; i <= n; i++) { // 想统计1~n里所有数的数位和 while (i > 0) { sum += i % 10; i /= 10; } }你跑一遍就会发现,外层循环到了i=12的时候,里面把i拆成0了,外层循环直接跳到13,中间所有带循环的枚举全乱套。英文里管这叫loop variable mutation,中文就是“循环变量被修改”。解决办法永远只有一个:拆位之前先复制临时变量。
for (int i = 1; i <= n; i++) { int t = i; while (t > 0) { sum += t % 10; t /= 10; } }这个错几乎每个班都会有人犯,而且犯过一次之后如果不注意,下次还会犯。我建议你在练习阶段就把“所有要拆位的数据,先另存一份”写成一行注释,放在循环开头,形成固定习惯。
6. 备考练习建议与提升路线
6.1 怎么练才高效
拆数字这个专题,最忌讳的就是“看懂了但不练”。你要在考试里做到不假思索地写出模板,那就要练到肌肉记忆。我给学生定的目标是:三套基础模板能默写;反转、回文、数位和三个变形能默写;n=0、负数、原数备份这三个坑能自动规避。
每天的练习量不用多,拆数字相关的题目做2到3道就够了,关键是做完之后要总结。建议你准备一个错题本,专门记录“因为边界条件出错”的题。判断一个数是否为回文数,输入附带了0结尾的样例吗?判断水仙花数,输出格式是空格分隔还是换行分隔?重组最大数,题目要求输出整数还是保留前导零的字符串?这些细节比算法本身更能拉开分数差距。
还有一个我特别推荐的做法:每道题写完,都自己造几个边界测试数据跑一遍。比如测试0、测试1、测试最大输入值、测试全是同一个数字的数、测试末尾带很多0的数。这个习惯不需要额外花多少时间,但对考试稳定性的提升非常明显。我见过太多学生样例全过、考试全挂,就是因为不会自己构造测试点。
6.2 从二级到三级的分水岭
练完这些模板,你再去刷二级真题和模拟卷,会发现拆数字类的题目难度其实就停留在“模板熟练度”和“细节意识”两个维度上。真正的分水岭是三级要开始的高精度加减法、简单贪心、递归初步:这些内容里拆数字依然会被频繁调用,但不再是主角。比如高精度计算里,你要把一个很大的数按位拆开存到数组里;贪心的数字构造题,你要拆位、排序、再拼回。现在把这些模板练扎实,以后学三级内容会轻松很多。
我给备考二级的孩子最后一句忠告永远是:别急着刷难题,先把拆数字这种“基础中的基础”焊死。考场上真正让你丢分的,往往不是最后那两道压轴题,而是你以为自己早就掌握、实际漏洞百出的第一道大题。把模板背熟、边界摸透,二级的数字题就是你最稳的拿分项。