公司动态
C++数位分解实战:从两位数含2问题掌握循环与条件判断
1. 问题引入与核心思路最近在带学生刷题的时候碰到一个挺有意思的基础题题目要求是“请输出所有的2位数中含有数字2的整数”。乍一看这题简单得有点过分不就是从10到99遍历一遍看看哪个数里包含‘2’嘛。但恰恰是这种看似简单的题目最能考验一个程序员尤其是初学者对问题本质的理解、代码的严谨性以及思维的发散性。很多同学拿到手可能三五行循环就写完了但有没有想过除了最直接的暴力枚举有没有更“优雅”或者更“高效”的解法这道题背后其实藏着循环控制、整数数位分解、条件判断优化等多个C基础知识点是检验基本功的一块很好的试金石。我们今天不满足于仅仅给出答案而是要深挖一下。我会带你从最朴素的思路开始一步步推导看看如何写出既正确又高效的代码。同时我们也会探讨一些常见的“坑”比如输出格式、遍历边界、判断逻辑等这些细节往往是决定代码质量的关键。无论你是正在准备信息学奥赛的选手还是刚开始学习C的新手相信通过这个具体的例子都能对循环和条件判断有更深刻的认识。2. 问题分析与解法设计2.1 问题重述与范围界定首先我们把题目翻译成更精确的编程语言描述我们需要找出并输出所有在区间 [10, 99] 内的整数并且这些整数的十进制表示中至少有一位十位或个位上的数字是2。这里有几个关键点需要明确范围是所有的“两位数”即从10开始到99结束包含10和99。这一点必须清晰因为循环的起始和终止条件就基于此。“含有数字2”的定义指的是十进制表示下的数字2。需要检查这个两位数的十位和个位。输出要求题目说“请输出所有的...”通常意味着每个符合条件的数占一行或者以空格分隔输出一行。为了清晰和通用我们采用每行输出一个数的格式。2.2 核心算法思路拆解解决这个问题的核心在于如何判断一个两位数是否包含数字‘2’。这本质上是一个“数位分离”和“条件匹配”的问题。对于两位数num我们可以通过数学运算快速得到其十位和个位十位tens_digit num / 10在C中整数除法会截断小数部分个位units_digit num % 10取模运算得到除以10的余数得到十位和个位数字后判断条件就很简单了tens_digit 2或units_digit 2。只要其中一个条件为真这个数就符合要求。基于这个判断方法最直接、最容易理解的算法就是遍历法用一个循环让变量num从10开始一直增加到99。对于每一个num计算其十位和个位。判断十位或个位是否等于2。如果等于则输出这个num。这个思路清晰明了时间复杂度是 O(N)其中 N90从10到99共90个数对于这个问题规模效率完全足够。接下来我们就用代码来实现它并深入每一个细节。3. 基础实现与代码详解3.1 完整代码实现标准解法我们先给出最标准、最易读的解法代码并附上详细的注释。#include iostream using namespace std; int main() { // 循环遍历所有两位数即从10到99包含 for (int num 10; num 99; num) { // 分解出十位数字通过整数除法 int tens_digit num / 10; // 分解出个位数字通过取模运算 int units_digit num % 10; // 判断条件十位或个位中至少有一个是2 if (tens_digit 2 || units_digit 2) { // 如果符合条件则输出该数字 cout num endl; } } return 0; }3.2 代码逐行解析与原理#include iostream和using namespace std;这是C标准输入输出流的头文件cout和endl都定义在其中。using namespace std;是为了避免每次使用cout,endl都要写std::cout的麻烦。在小型程序或竞赛中常用但在大型项目中建议显式使用std::以避免命名冲突。for (int num 10; num 99; num)这是循环的核心。int num 10是初始化将循环变量num设置为第一个两位数10。num 99是循环继续的条件。只要num小于或等于99循环就会执行。这里必须用而不是否则会漏掉数字99。这是一个新手常犯的边界错误。num是每次循环结束后执行的表达式将num的值增加1。这里使用前置自增 (num) 和后置自增 (num) 在这个简单场景下效果一样但前置自增理论上效率稍高对于内置类型无差别对于复杂对象有区别是一种良好的编程习惯。数位分解int tens_digit num / 10;C中两个整数相除结果仍是整数并且是向零取整直接截断小数部分。对于两位数除以10正好得到其十位数字。例如45 / 10的结果是4。int units_digit num % 10;%是取模运算符得到的是除法后的余数。一个数除以10的余数自然就是它的个位数字。例如45 % 10的结果是5。条件判断if (tens_digit 2 || units_digit 2)是相等比较运算符。||是逻辑或运算符。只要tens_digit 2和units_digit 2这两个条件中有一个为真true整个表达式就为真。这个判断逻辑直接对应了题目要求“含有数字2”即十位或个位任意一位是2即可。输出cout num endl;cout是标准输出流对象是流插入运算符将后面的内容输出到控制台。endl是操纵符作用是插入一个换行符并刷新输出缓冲区。这里使用endl确保每个数字输出在独立的一行。如果希望所有数字输出在同一行用空格隔开可以写成cout num ” “;循环结束后再输出一个endl。return 0;在main函数中return 0;通常表示程序正常结束。这是C/C程序的一个约定俗成的习惯。注意在在线判题系统OJ中有时对输出格式要求非常严格。如果题目要求“每个数字一行”那么使用endl或”\n”是正确的。如果要求“一行输出空格分隔”则需调整输出方式。务必仔细阅读题目的输入输出描述。3.3 运行结果验证运行上述程序控制台会依次输出以下数字12 20 21 22 23 24 25 26 27 28 29 32 42 52 62 72 82 92你可以手动验证一下从10到99之间所有十位或个位是2的数确实都包含在这个列表里了一个不多一个不少。4. 方案优化与思维拓展虽然上面的解法已经完美解决了问题但作为程序员我们应该有追求更优解和更广思路的习惯。下面我们来探讨几种不同的思路和潜在的优化点。4.1 优化判断逻辑在基础解法中我们对每个数都进行了除法、取模和两次比较。有没有可能减少一些操作呢对于这个问题由于数值范围很小优化带来的性能提升微乎其微但作为一种思维训练很有价值。思路一预先计算直接判断我们注意到所有十位是2的数构成了一个等差数列20, 21, 22, ..., 29。所有个位是2的数也构成了一个等差数列12, 32, 42, ..., 92。但22重复了。所以我们可以直接输出这两个序列。这完全避免了循环和计算。#include iostream using namespace std; int main() { // 输出十位是2的数20 ~ 29 for (int i 20; i 29; i) { cout i endl; } // 输出个位是2的数12, 32, 42, ..., 92 (跳过22因为已输出) for (int i 12; i 92; i 10) { if (i ! 22) { // 22已经在第一个循环输出过了 cout i endl; } } return 0; }这种方法在本题中甚至比遍历法代码更短但它严重依赖于问题的特殊性两位数、数字2。如果题目改成“含有数字3的三位数”这种方法就需要重新推导序列通用性很差。而遍历法则可以轻松应对这种变化只需修改循环范围和判断数字即可。思路二合并判断减少变量在基础解法中我们使用了两个临时变量tens_digit和units_digit。其实我们可以将判断条件直接写在if语句里省去这两个变量。for (int num 10; num 99; num) { if (num / 10 2 || num % 10 2) { cout num endl; } }代码更简洁了。但这里有一个细微的性能考量同样对于本题可忽略在if条件中num / 10和num % 10会被计算两次如果第一个条件为真由于逻辑或||的短路特性num % 10可能不会被计算。而在基础解法中每个数只计算一次除法和一次取模。在追求极致性能的场合基础解法可能略优因为现代编译器通常很难优化掉这种在条件表达式中的重复计算。不过代码简洁性和可读性往往是更重要的。4.2 通用化改造原题是“含有数字2的两位数”。我们如何将代码改造成一个通用的函数用于找出任意区间内含有任意指定数字的数呢我们可以设计一个函数bool containsDigit(int number, int digit)它判断整数number的十进制表示中是否包含数字digit0-9。然后在主函数中调用它。这里的关键在于number可能不止两位我们需要一个循环来逐位检查。#include iostream using namespace std; // 判断一个正整数num中是否包含数字digit (0-9) bool containsDigit(int num, int digit) { // 处理数字0的特殊情况如果num本身就是0那么只有当digit也是0时才返回true if (num 0) { return digit 0; } // 注意需要处理负数可以先取绝对值。这里假设题目都是非负数。 int n abs(num); // 引入cstdlib或直接处理负数逻辑 while (n 0) { if (n % 10 digit) { // 检查当前个位 return true; } n / 10; // 去掉个位继续检查下一位 } return false; // 所有位都检查完了没找到 } int main() { int start 10; int end 99; int targetDigit 2; for (int num start; num end; num) { if (containsDigit(num, targetDigit)) { cout num endl; } } return 0; }这个通用函数containsDigit使用了while循环来逐位分解整数。n % 10获取当前个位n / 10将数字右移一位相当于去掉个位。这样无论num是几位数这个函数都能正确判断。这是一个非常重要的数位分解模板在很多算法题中都会用到。4.3 算法效率分析时间复杂度与空间复杂度对于基础遍历法时间复杂度O(N)其中 N 是需要遍历的数的个数。本题中 N90常数时间非常快。空间复杂度O(1)只使用了几个固定的整型变量与输入规模无关。对于通用函数法时间复杂度O(N * M)其中 N 是区间内数的个数M 是每个数的平均位数对于两位数M2。整体仍然是线性复杂度。空间复杂度O(1)。所以对于本题以及大多数类似的小规模问题这些算法在效率上没有本质区别。选择哪种方法更多取决于代码的清晰度、可维护性和可扩展性。5. 常见错误与深度避坑指南在实际编写和调试过程中尤其是初学者很容易掉进一些陷阱。下面我总结几个最常见的错误并解释为什么错以及如何避免。5.1 循环边界错误错误示例1for (int num 10; num 99; num)错误原因循环条件是num 99当num等于99时条件为假循环终止。因此数字99不会被遍历到如果99符合条件本题中99不含2所以不影响结果但逻辑错误就会被漏掉。正确做法对于包含上限的区间遍历必须使用。即num 99。错误示例2for (int num 0; num 99; num)错误原因起始值设为0这会把0到9的一位数也包含进来不符合题目“两位数”的要求。虽然这些数不含2不影响最终输出集合但做了无用的循环迭代并且没有严格遵循题意。正确做法起始值应为10。避坑心得在写循环时一定要在纸上或心里明确循环变量的初始值、终止条件和更新步长。对于遍历一个闭区间[a, b]标准的for循环写法是for (int i a; i b; i)。务必确认a和b是否正确对应题目的范围。5.2 数位分解与判断逻辑错误错误示例错误处理十位和个位// 错误写法混淆了十位和个位的计算方法 int tens_digit num % 10; // 这实际上得到的是个位 int units_digit num / 10; // 这实际上得到的是十位 if (tens_digit 2 || units_digit 2) { // 判断逻辑因此全乱 cout num endl; }错误原因将取模和除法运算用反了。num % 10是个位num / 10是十位。变量名tens_digit和units_digit与它们实际存储的值不匹配导致逻辑混乱。如果坚持用这个判断程序会错误地将所有个位是2的数当成十位是2的数输出反之亦然结果完全错误。正确做法牢记对于整数nn / 10是去掉个位得到高位部分n % 10是得到个位。给变量起一个见名知意的名字并在写代码时保持清醒。错误示例错误使用逻辑与if (tens_digit 2 units_digit 2) { // 错误这是要求十位和个位同时为2 cout num endl; }错误原因题目要求是“含有数字2”即十位或个位是2。使用逻辑与意味着必须十位和个位都是2这样只会输出数字22而漏掉了12, 20, 21, 23...等一大批正确答案。正确做法理解清楚“含有”意味着“至少有一个”应该使用逻辑或||。5.3 输出格式与平台适配问题问题输出没有换行或换行符使用不当在有些在线判题系统中如果要求“每个答案一行”而你输出了空格分隔的一行可能会被判为“格式错误”。同样如果要求“空格分隔一行”而你每行输出一个也可能错误。解决方案仔细阅读题目描述中的“输出格式”部分。使用cout num endl;或cout num “\n”;可以实现换行输出。如果需要空格分隔一行可以这样写bool first true; // 标记是否是第一个输出的数 for (int num 10; num 99; num) { if (tens_digit 2 || units_digit 2) { if (!first) { cout ” “; // 不是第一个数先输出一个空格 } cout num; first false; } } cout endl; // 最后输出一个换行符结束本行或者更简洁地用一个变量记录输出状态string separator “”; // 初始为空 for (int num 10; num 99; num) { if (tens_digit 2 || units_digit 2) { cout separator num; separator ” “; // 输出第一个数后分隔符改为空格 } } cout endl;问题在Windows和Linux/Unix系统上换行符的差异endl会输出一个换行符并刷新缓冲区它输出的换行符是平台相关的Windows上是\r\nLinux上是\n。在线判题系统通常运行在Linux环境下所以直接使用\n也是完全正确的并且有时效率略高于endl因为endl会强制刷新缓冲区。个人习惯在算法竞赛中我通常使用cout num “\n”;因为它更直接且避免了不必要的缓冲区刷新。但在需要立即显示输出的交互式调试中endl的强制刷新功能有时很有用。6. 举一反三与相关练习掌握了这个“数位判断”的核心技能你可以轻松解决一大批类似的问题。下面我列举几个变种题你可以尝试自己实现巩固所学。6.1 变种题目示例输出所有三位数中含有数字7的整数。思路循环范围变为[100, 999]。判断条件需要检查百位、十位、个位。百位num / 100十位(num / 10) % 10或num % 100 / 10个位num % 10。输出1~n之间所有能被3整除且至少有一位是5的整数。思路循环[1, n]。两个条件num % 3 0并且(containsDigit(num, 5)为真)。这里就需要用到我们上面写的通用containsDigit函数了。求所有水仙花数一个三位数其各位数字的立方和等于该数本身。思路循环[100, 999]。分解出百位a、十位b、个位c。判断a*a*a b*b*b c*c*c num。这是数位分解的经典应用。计算一个整数中某个数字出现的次数。思路使用while循环分解整数在循环中用if判断当前位是否等于目标数字并用一个计数器累加。6.2 核心技巧总结通过这道题我们可以提炼出几个在编程中非常基础且重要的技巧整数数位分解使用% 10获取个位使用/ 10去掉个位通过循环可以处理任意位数的整数。这是处理数字相关问题的基石。循环边界控制务必清楚你的循环变量从哪里开始到哪里结束步长是多少。特别是处理包含/不包含端点时要小心使用和。条件逻辑清晰理解“与”和“或”||的区别。A B要求两者同时满足A || B要求至少满足一个。从特殊到一般先解决具体问题两位数数字2再思考如何抽象成通用函数任意数任意数字。这种思维是提高代码复用性和解决问题能力的关键。测试与验证即使题目再简单也要用几个典型的、边界的情况测试你的程序。例如本题可以手动列出所有含2的两位数与程序输出对比。也可以测试一下不含2的数如11是否被错误输出。这道“简单”的题目就像一面镜子能照出一个程序员对基础掌握的扎实程度。下次再遇到类似问题希望你不仅能快速写出代码更能清晰地解释代码背后的每一处设计考量。编程的路上正是由这些扎实的基本功一步步堆砌起解决复杂问题的能力。