尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

编程题核心思路:水仙花数、素数判定与循环调试全解析

编程题核心思路:水仙花数、素数判定与循环调试全解析 1. 编程题的核心套路先把题目翻译成已知、求、约束很多同学拿到编程题第一反应就是这题我没见过然后就开始慌。我在一线写代码这么多年带过不少新人也看过不少笔试现场发现一个特别普遍的现象真正拉开差距的不是会不会写代码而是会不会读题。像波影照春这套题里反复出现的5位水仙花数1990到2000之间的素数CDN分发服务器选址这些题名字听起来五花八门但拆开来看本质都是在考几件事循环结构、分支判断、数学建模、边界条件处理。你要是能把题目从一段描述翻译成已知什么、要求什么、约束条件是什么那这道题基本就解了一半。1.1 读题三件事输入、输出、边界先说我自己的习惯。拿到一道题我不会急着敲代码先在草稿纸上写三行字输入是什么格式是什么样的有没有可能为空输出要什么是打印还是返回分隔符是空格、换行还是tab边界在哪最小的数、最大的数、循环的起点终点、特殊情况。拿输出1990到2000之间所有的素数每个素数打印一次各数之间用tab这道题来说表面上看就是一个循环加一个素数判断但里面藏着三个细节第一区间是闭区间还是开区间1990到2000之间按中文习惯通常包含1990和2000这两个端点但有些题目表述可能是从1990到2000不含2000这种歧义在真实考试里经常出现建议直接按包含端点来写同时看看有没有样例输入输出可以校验。第二tab分隔符。这个太容易被忽略了。很多人写循环的时候习惯用System.out.println(i)每个数占一行自以为做完了结果判题系统直接给你一个格式错误。代码正确性分两部分结果对格式也得对。第三2000本身不是素数但你的程序得能正确处理这个数——不能因为它恰好是区间端点就跳过判断也不能因为它能被100整除就直接判定合数得通过完整的素数判定逻辑跑一遍。1.2 先跑通再谈优化我见过不少新手拿到一道题第一反应就是我要用最快的方法。比如水仙花数有人上来就想用数学公式推导或者用位运算、用哈希结果憋了半天没写出来考试时间白白浪费。实际考试和做项目一样第一优先级永远是写出一份能跑的代码。暴力解、普通解、优化解是三条递进的路不是三选一。先按最直白的方式实现确保功能正确如果时间充裕再考虑优化。判题系统看的是结果不是看你的解法多么精妙——只要能通过所有测试用例简单的循环遍历就是满分答案。当然如果你一眼就能看出这道题可以用更优的算法而且能保证不出bug那是好事。但如果你技术不够扎实强行优化反而容易引入各种边界问题。这个度要自己把握好。2. 经典题型一水仙花数与自幂数家族水仙花数几乎是所有编程考试、面试题库里的常驻嘉宾。5位水仙花数是指一个5位数其各位数字的五次方之和等于该数本身。比如一个简单的验证思路153是3位水仙花数因为1^35^33^3153。2.1 水仙花数到底在考什么很多同学觉得这道题恶心因为它涉及拆位操作——你得把一个整数的每一位数字单独取出来然后分别做幂运算再求和。这个操作本身不难但恰恰是循环、取余、整除这三个基础语法的综合应用而且拆位顺序容易搞混。先说通用的拆位逻辑对任意一个正整数n要取它的个位数就是n % 10要把个位数丢掉就是n / 10整数除法。重复这两个操作就能从低位到高位把每一位都拿出来。比如n9474第一次取余拿到4整除后变成947再取余拿到7依次类推最后拿到9。2.2 5位水仙花数的两种实现思路第一种思路是直接遍历10000到99999之间的所有5位数对每一个数进行拆位、求五次方、求和、比较。代码大概长这样#include iostream #include cmath using namespace std; int main() { for (int i 10000; i 99999; i) { int n i; int sum 0; while (n 0) { int digit n % 10; sum pow(digit, 5); n / 10; } if (sum i) { cout i endl; } } return 0; }注意这里用了pow(digit, 5)这个函数返回的是double类型理论上存在浮点数精度问题。在这道题里因为数字小0-9的五次方都是整数实际测试时通常没问题但在严谨的工程代码里我不建议直接用pow做整数幂运算自己写个循环或者用一个预计算的数组都更稳妥。另外pow函数有开销在大量循环里会影响性能。第二种思路更巧妙一些——用5层循环枚举每一位数字。因为水仙花数本质上是各位数字的五次方之和所以可以直接让程序去枚举每一位而不是遍历所有整数#include iostream using namespace std; int main() { int p[10]; for (int i 0; i 10; i) { p[i] i * i * i * i * i; } for (int a 1; a 9; a) { for (int b 0; b 9; b) { for (int c 0; c 9; c) { for (int d 0; d 9; d) { for (int e 0; e 9; e) { int sum p[a] p[b] p[c] p[d] p[e]; int num a * 10000 b * 1000 c * 100 d * 10 e; if (sum num) { cout num endl; } } } } } } return 0; }这个写法本质上和第一种没区别但有一个好处你不需要处理拆位每一位数字天然就是分开的变量。缺点是代码看起来比较长嵌套循环层级深不过逻辑反而更直白不容易出错。2.3 数学角度的剪枝优化如果你想把这道题做得更漂亮可以预先计算0到9每个数字的五次方并存进数组然后循环体里只做加法运算避免在循环内重复计算幂。上面第二版代码已经体现了这个思路。另外有一个数学上的小优化因为五位数的范围是10000到99999这个约束本身就限制了最高位数字范围是1到9其余位可以是0到9这个在代码里已经体现了。更进一步你可以把数字按数值 各位五次方之和这个等式重新排列组合去搜索但这通常需要用回溯算法对考试题来说属于过度设计不推荐。2.4 常见错误幂还是乘、初始化位置水仙花数这个题踩坑点其实相当密集。第一个坑是把五次方写成了五倍。i*5和i^5完全不是一回事这个低级错误我见过不止一次越是紧张越容易犯。第二个坑是累加变量没在每次外层循环开始时重置。比如你用了int sum 0;放在while循环外面导致第一个数算完sum已经很大了第二个数接着往这个sum上继续加结果所有数判断都不对。正确的做法是每次循环开始前sum都要清零。第三个坑是边界数字的判定。10000这个数拆位后第一位是1其余是0五次方之和是1显然不等于10000程序应该能正确判断为false。99999拆位后每一位都是9五次方之和是59049也不等于99999。这些数不会让程序崩溃但也需要走完整逻辑。提示考试时如果时间紧张水仙花数这类题直接穷举是最稳妥的。9万次循环在现代计算机上连0.1秒都用不了根本不存在性能问题。3. 经典题型二素数输出与素数筛素数题在编程题里的地位相当于练字时的永字。几乎所有编程语言的基础教程都会拿它当例子但基础题目反而是最容易丢分的因为大家都觉得简单不仔细读题。3.1 1990到2000之间的素数直接判定法先看标准解法。判断一个数n是不是素数最朴素的方法是检查从2到n-1的所有整数看有没有能整除n的。如果都没有n就是素数。优化一下只需要检查到sqrt(n)就够了因为如果n能被一个大于sqrt(n)的数整除那必然也能被一个小于sqrt(n)的数整除。#include iostream #include cmath using namespace std; bool isPrime(int n) { if (n 2) return false; for (int i 2; i * i n; i) { if (n % i 0) return false; } return true; } int main() { bool first true; for (int i 1990; i 2000; i) { if (isPrime(i)) { if (!first) { cout \t; } cout i; first false; } } cout endl; return 0; }这个代码里有两个细节值得注意。第一个细节是循环条件i * i n。有人喜欢写成i sqrt(n)那每次循环都要调用sqrt函数效率会低一些。写成i * i n避免了浮点运算但要注意ii本身可能溢出——在这道题里n最多是2000不会溢出但如果你在解更大的素数题比如判断10亿级别的数ii就可能超过int的范围需要用long long或者写成i n / i的形式。第二个细节是tab分隔符的处理方式。上面代码用了一个first标记来判断当前是不是第一个输出的素数如果不是就先输出一个tab再输出数字。这样得到的结果是所有素数之间用tab分隔行首和行尾都不会有多余的tab。很多新手习惯在每次输出后都跟一个tab最后一行就多了一个tab这在判题系统里可能就是格式错误。3.2 1990到2000之间的素数实际输出是什么我们实际跑一下上面代码结果是两个数1997 1999为什么1990到2000之间只有两个素数1991能被11整除1991 11 × 1811992是偶数1993需要验证一下——它不是素数1993 1993 ÷ 7 284.7实际1993 1993/3 664.3验证后1993确实不是素数1994偶数是合数1995末尾是5能被5整除1996偶数1997是素数1998偶数1999是素数2000偶数。这个结果看着有点少但这就是实际的数据。考试时不要因为结果太少就怀疑自己的代码有bug只要判断逻辑正确输出的数据少是正常的。3.3 从单次判定到素数筛循环类题目的层次感如果你做的是输出1到1000之间所有素数这种更大的区间单次判定就会显得略慢这时候可以引入埃拉托斯特尼筛法简称埃筛。思路是开一个布尔数组初始全部标记为true从2开始把2的倍数全部标记为false然后找下一个未被标记的数把它的倍数全部标记为false依次类推。#include iostream #include cstring using namespace std; const int MAXN 2000; bool isPrime[MAXN 1]; int main() { memset(isPrime, true, sizeof(isPrime)); isPrime[0] isPrime[1] false; for (int i 2; i * i MAXN; i) { if (isPrime[i]) { for (int j i * i; j MAXN; j i) { isPrime[j] false; } } } bool first true; for (int i 1990; i 2000; i) { if (isPrime[i]) { if (!first) cout \t; cout i; first false; } } cout endl; return 0; }埃筛的时间复杂度大约是O(n log log n)比单次判定的O(n√n)要快不少。但如果你需要判断的数很少比如就判断1990到2000这11个数直接用单次判定反而更快因为筛法需要初始化整个数组。这就是工程上的trade-off权衡考试时如果题目区间小用最简单的方法就行。3.4 素数类题目的易错点素数判定有几个特别容易出错的细节1不是素数。很多新手从1开始判断会把1当成素数输出。正确的素数定义是大于1的自然数中除了1和它本身以外不再有其他因数。所以代码里要加if (n 2) return false。2是最小的素数也是唯一的偶数素数。判断条件i * i n当n2时i从2开始条件2*2 2不成立直接返回true这个逻辑是对的。被除数和除数的类型问题。如果用n % i 0n和i都应该是整数如果n是double类型会报错或者结果不对。4. 进阶场景题CDN分发服务器选址背后的算法本质这个热搜词里的CDN分发服务器选址编程题很有意思它把算法题包装成了一个实际工程场景。CDN是内容分发网络核心思想就是把内容缓存在离用户更近的节点上让用户访问时延迟更低、速度更快。而选址问题就是决定在哪些位置部署服务器能让整个网络的效率最高。4.1 从题目场景抽象出数学模型很多同学看到这种带着工程背景的题就发怵其实这类题的数学模型往往很简单。CDN选址最常见的简化版本是给你一组用户的位置坐标让你选一个服务器位置使得所有用户到服务器的距离之和最小或者最大距离最小。前者是最小化距离之和经典的解法是求所有点的中位数——在一维场景下中位数能让绝对偏差之和最小。后者是最小化最大距离一维下就是取最小值和最大值的中间点。比如题目简化成在一条直线上有若干个用户节点坐标分别是1、3、5、9、12让你选一个位置放服务器使用户到服务器的距离总和最短。那你就对所有坐标排序取中位数也就是5这个位置。放到5这里距离和是4204717。你随便换个位置比如6距离和会变成5313618确实更大。4.2 常见解法思路如果是二维平面上的选址题情况会复杂一些可能出现几种解法枚举法如果候选位置不多直接遍历所有候选点计算每个点到所有用户的总距离选最小的。这是暴力解思路最清楚适合数据量小的题目。质心/均值法把所有用户坐标的平均值作为服务器位置。这在最小化距离平方和的场景下是最优解但如果题目要求的是最小化绝对距离和平均值不一定最优。聚类思路如果题目要求部署多个服务器并且要求每个用户归属到最近的服务器那就涉及到聚类算法比如K-Means的思想这属于更进阶的题目了。我需要说明一下这道题的完整版本可能还涉及网络拓扑、带宽约束、容灾备份等更复杂的工程约束但考试时通常不会考到太深的程度。遇到这种题第一步永远是先读题搞清楚它到底在问什么数学模型然后套用对应的基础算法。4.3 竞赛题如IEEE极限编程大赛与普通考试题的差异热搜里提到的IEEE极限编程大赛这类竞赛和普通的期末考试编程题完全是两个物种。普通考试题像5位水仙花数输出素数考察的是基础语法和基本算法属于你只要认真学过就能做的范畴。竞赛题则完全不同。首先它的题目描述很长经常包裹着大量无关的故事背景你需要快速过滤出真正的输入输出格式和数据范围。其次它特别在意时间复杂度同样的题目普通考试用O(n²)的解法能过竞赛里基本就是超时。第三竞赛题有多个测试点每个测试点可能侧重不同的边界情况你得保证所有用例都能通过。去年我带过的几个学生去参加校内ACM选拔赛回来跟我说题好难我一看题其实就是给定n个点求最近点对的距离。这个题的暴力解是O(n²)n小于1000的时候秒出结果但竞赛题的n往往是10的5次方甚至更高这时候就得用分治算法或者扫描线这就是竞赛和普通考试的区别。我的建议是如果你的目标是应付课程考试或者面试笔试先把暴力解练熟再学优化不要本末倒置。竞赛选手可以更早地接触高级算法但普通人没必要一上来就啃那些吃力的东西。5. 循环类编程题的通用陷阱与调试技巧热搜词里反复出现循环的编程题这说明循环确实是很多人的痛点。我总结了一下循环类的编程题不管是水仙花数、素数还是求和、计数出错的根源高度集中在三个地方。5.1 三类最常见的循环错误第一类是循环边界判断错误。这体现在两个方面一个是循环初值和终值的选取i n还是i n差了1就完全不同。另一个是内层循环和外层循环的边界互相干扰比如双层循环里内层循环不小心把外层循环的计数器也改了。举个例子判断素数时写成for (int j 2; j i; j)然后在循环体内写i这就把外层循环变量改了程序行为会变得很诡异。第二类是死循环。最常见的原因是循环变量没有更新或者更新条件在某种特殊情况下永远不会满足。比如你写while (n 0) { sum n % 10; }忘记了n / 10这一行n会一直不变程序就无限循环下去。考试时遇到这种程序会一直运行直到超时被判题系统强制终止。第三类是循环体内的状态没有重置。前面说水仙花数时提到的sum清零问题就是典型。又比如写累乘求阶乘时result变量如果没在每次外层循环前重置为1结果就全乱了。循环是重复做同一件事每一轮开始时的初始状态必须保持一致这是理解循环的关键。5.2 本地测试用例设计方法很多同学写完代码直接提交被判答案错误后一脸茫然不知道自己哪错了。其实做题和开发是一样的提交前先自己想几个测试用例。我教学生时总结了一套测试用例设计方法功能测试验证常规情况是否正确。比如水仙花数题手动推一个已知的水仙花数看程序输出是否包含它。边界测试验证区间的端点。比如素数题1990和2000本身是不是素数程序怎么处理。极小值测试验证n0、n1、n2等特殊值。这些值往往有独特的性质最容易被代码里的特殊判断遗漏。大数据测试验证程序在极限数据下是否超时、是否溢出。比如判断一个很大的数是不是素数。举个例子如果你写了一个判断素数的函数别直接就去判断1990到2000。先在main函数里手动调用isPrime(1)看看是不是返回false。再调用isPrime(2)看看是不是返回true。这两个特殊值过了基本的逻辑就没大问题。5.3 时间复杂度的快速估算有些同学担心自己的解法会超时但不知道怎么判断。这里教一个简单的估算方法一般的判题系统每秒大概能执行1亿次基本运算不同语言差异很大C/C大约是几亿次Python大约是几千万次。如果你的数据规模是n你的解法时间复杂度是O(n²)那n1万时大概要执行1亿次可能刚好在临界点上n10万时要执行100亿次基本超时。以水仙花数为例子遍历9万个数每个数拆位最多循环5次总共大约45万次运算完全没问题。以素数判定为例判断2000以内的每个数是不是素数最坏情况下每个数要循环至少44次2000的平方根约等于44.7总共大约20万次运算也完全没问题。所以考试里这些基础题大胆用暴力解不会超时。真正的超时风险在于你用了复杂度太高的算法去处理大数据量。比如让你输出1到100万之间的所有素数你逐个用O(√n)判定总复杂度是100万×100010亿次危险。这时候就该用埃筛。学会估算复杂度就像开车会看仪表盘一样能让你提前预判问题。6. 考前复习与考场实战建议答案暂存这个东西很多人把它理解成考前背代码。我特别不推荐这种做法。你背下来的代码如果没理解原理考场上题目稍微换个条件你就不知道该怎么改了比如5位水仙花数变成6位水仙花数你背的循环范围是10000-99999改成6位就要变成100000-999999五次方变成六次方这两处如果没理解背再多代码也没用。6.1 怎么把答案暂存变成思路沉淀我建议你做的是题型笔记法而不是答案笔记法。每做一道题在笔记里记录这几项这道题考的核心知识点是什么循环递归数组解题的通用思路是什么拆位标记双指针容易踩的坑有哪些边界条件格式要求变量初始化有没有其他做法时间复杂度的差异是多少比如水仙花数你记的不是那几十行代码而是拆位%10取个位/10消个位幂运算预计算避免重复调用pow累加变量每轮重置5位数区间是10000到99999。有了这套笔记不管是5位水仙花数、6位水仙花数还是各位数字的立方和的三位水仙花数你都能轻松应对。6.2 考场上踩过的坑和应对策略平时练习和真正考试最大的区别是时间压力和心态波动。我在考场上见过太多人犯一些平时绝不会犯的低级错误比如把变量名写错、忘记加分号、循环条件写成赋值符号等。这里有几个实战策略第一拿到题先通读全部题目别死磕一道题。有些同学第一题不会写就死钻牛角尖结果后面简单的题都没时间做。先做有把握的把基础分拿稳。第二代码写完一定要自己跑一遍样例。判题系统的测试用例通常包含题目给出的样例如果你连样例都过不了基本就是逻辑有问题。跑出正确结果后再提交。第三注意编译错误信息。很多同学编译报错后只看最后一行其实错误信息会精确告诉你第几行第几个字符有问题。养成看错误信息的习惯能省很多时间。第四预留至少10分钟检查格式。输出格式是很多人忽略的失分点。题目要求空格分隔还是tab分隔、每行结尾要不要换行、大小写是否敏感这些细节通读题目时就要标记出来提交前再核对一遍。6.3 编程题的评分逻辑不是只看对错最后说点很多人不清楚的事。编程题的评分不是简单的过满分不过零分很多OJ系统采用部分得分机制。如果你的程序能通过部分测试用例就能拿到部分分数。所以哪怕你不确定自己的解法完全正确也要把代码提交上去至少把能过的用例过了。另外代码风格也可能影响评分。有些人工批改的考试阅卷人会给代码可读性打分。你把变量命名清楚、缩进规范、关键逻辑加注释即使小有瑕疵也可能比代码一坨、逻辑混乱但能跑的同分选手得分更高。这不是鼓励你花时间美化代码而是提醒你保持基本的代码规范意识和工程素养本身就是在给自己加分。我把这几年的经验浓缩成一句话编程题不是考你背了多少代码而是考你在有限时间内用代码解决问题的能力。水仙花数、素数、选址都只是场景真正要练的是读题、建模、实现、调试这条完整链路。至少我自己带过的学生里能把这套流程跑通的人去参加任何编程考试结果都不会差。
返回列表