
聊到一个Java新手最绕不开的话题数值计算题。不管你是准备蓝桥杯入门、应付面试手撕代码还是刚学完循环和数组想找点像样的练习你迟早都会撞上这一批题。其实这类题根本不偏门它考察的就是你最该掌握的基础语法组合数据类型、运算符、分支循环、方法封装再加一点简单的数学思维。我见过太多人一上来就背框架、背Spring注解结果连“水仙花数”都得想半天这其实说明基础还欠着火候。这篇博文我会把6个最经典的数值计算问题从头到尾过一遍包括思路拆解、完整代码、易错点以及我在实际刷题和带新人过程中反复遇到的那些坑。适合刚学完Java基础语法的人用来巩固也适合准备面试或蓝桥杯的读者拿来做题感训练。1. 为什么选这6个问题一份面向应用的题单设计1.1 数值计算题如何串起Java基础语法很多人觉得数值计算题太“小学数学”没有难度也没有含金量。但我的看法正好相反这类题目能把Java基础语法中最容易出现隐性错误的地方全部暴露出来。你写一个条件判断很容易但写一个覆盖边界、考虑溢出、还能兼顾性能的条件判断就不容易了。比如数据类型选int还是long决定了斐波那契数列算到第几项就会“莫名其妙变成负数”循环边界写i n还是i n决定了你找因子的时候会不会漏掉最后的平方数取模和整除的顺序搞错数字拆分出来的结果就是乱的。这些细节是你在学语法阶段不太会在意、但一写代码就会真实遇到的。数值计算还有个好处它不需要任何框架依赖不需要配置环境一个类一个main方法就能跑。这让你可以把全部注意力放在逻辑本身。我常说这类题是检验“你是否真的理解了编程基础”的试金石。你背得再多八股文不如亲手把六个数值题从零写一遍。1.2 六道题的知识点覆盖对照这六个问题不是随便挑的它们基本覆盖了数值计算领域最常出现的思维模型。先看一个总览表格题目核心考点涉及语法难度常见出现场景水仙花数数字按位拆分、自幂数判断循环、取模、整除入门课堂作业、蓝桥杯基础题素数判断因子遍历、数学优化循环、条件判断、break入门面试热身、算法训练最大公约数与最小公倍数辗转相除法、递归思想while循环、递归、取模基础面试题、工程算法斐波那契数列递推与递归对比、溢出数组、循环、方法调用基础面试、动态规划启蒙回文数判断数字反转、溢出防护循环、数学运算中等LeetCode、蓝桥杯完数判断因子配对、计算优化循环、累加、取模中等数学兴趣题、竞赛入门从表格能看出来这组题的难度是阶梯式上升的。前两个只需要单层循环加简单判断中间两个开始引入“递推”和“数学算法优化”的思想最后两个则更考验边界处理和性能意识。把这六个问题啃下来你后面接触字符串处理、数组操作、动态规划时都会有一种熟悉感因为本质上还是在做同一件事通过细致的条件控制把一个数学定义翻译成计算机逻辑。1.3 建议的练习顺序与学习方法我给的建议是严格按照表格顺序练先水仙花、再素数、然后公约数、斐波那契、回文数、完数。因为水仙花数对语法要求最低你只需要会拆数字就能写出来能快速建立信心。素数和公约数开始强迫你思考“循环要不要走到头”这是性能意识的起点。斐波那契数列开始讨论递归的成本让你意识到一个看似正确的解法可能极其低效。回文数则第一次让你考虑“数据溢出”这是工程视角的敲门砖。完数结合了因子遍历和配对优化相当于一次小型综合训练。学习方法上我的建议很直接先别看我的代码。每道题先自己画一个输入输出样例比如手算判断153是不是水仙花数、7是不是素数然后在纸上写伪代码。伪代码能跑通再落到Java代码。遇到卡住超过20分钟再参考答案。这个过程逼着你把问题抽象成步骤也正是面试手撕代码时真正考察的能力。2. 写题之前先把这4个基础问题搞明白2.1 数据类型的取值范围与溢出风险Java中int类型是32位有符号整数取值范围是-2147483648到2147483647也就是大约负21亿到正21亿。很多新手对这个范围没有概念直到斐波那契数列算到第46项左右时突然输出一个负数才一脸懵地来问我是不是代码写错了。其实代码没错是int类型装不下了数值溢出后按照二进制补码变成了负数。这类问题的通用解法有三种换用long扩大范围、使用BigInteger支持任意大整数、或者从一开始就设计好取模运算比如题目要求输出F(n) % 1000000007时你只在递推过程中保留余数这样int就够用了。在数值计算题里最怕的不是溢出本身而是你根本没有意识到它会溢出。写水仙花数时153 * 153 * 153还好但如果你把次方计算写成循环累乘在更大范围的自幂数题里就可能翻车。我也见过有人用Math.pow()那个方法返回的是double精度会随数值增大而丢失。所以确定要精确整数运算时尽量用乘法自己算或者明确使用整数类型做强制转换。2.2 数字拆分的唯一姿势除法配合取模数值计算题的核心操作之一就是“把一个多位数拆成单个数字”。方法其实只有一种反复用% 10取最后一位用/ 10去掉最后一位。比如对123执行123 % 10得到3执行123 / 10得到12再对12执行% 10得到2对1执行% 10得到1整个循环结束后你就拿到了每一位数字。这个思路在水仙花数、回文数、数字统计类题目里会反复出现有必要熟练到形成肌肉记忆。这里要注意一个问题负数取模在Java里会得到负余数。比如-123 % 10的结果是-3不是3。所以如果你要对负数做数字拆分一定先取绝对值或者单独写一个负数判断分支。很多新手在回文数判断里输入-121结果发现逻辑混乱就是因为没有提前处理负数。我的习惯是涉及数字拆分的函数开头先判断正负号提前归一然后统一处理正数逻辑。2.3 循环边界和break/continue的细节数值计算题里循环边界几乎是最高频的bug来源。比如判断一个数n是否为素数你写for (int i 2; i n; i)能跑但没有必要遍历到n - 1优化一下到i * i n就够。这时候边界是还是就非常关键写成会漏判i * i n的情况比如n 49你只检查到i 7就不会触发i 7时余数为0的条件然后错误地认为49是素数。break在循环里往往配合“一旦找到就提前结束”的语义。素数判断里只要发现一个因子就可以break跳出没必要继续往后试。continue则适合跳过某些不符合条件的情况比如循环累加时跳过偶数。这两个关键字本身不难难的是判断“到底该不该跳出”。我的建议是写循环之前先在注释里写清楚循环的目的是“找第一个满足条件的值”还是“统计所有满足条件的值”目的不同跳出逻辑完全不同。2.4 估算复杂度几层循环决定性能数值题看着简单数据量一大就会暴露性能问题。假设题目要求统计1到1000000之间有多少个素数你写两层循环外层遍历每个数内层从2遍历到这个数本身那么总操作次数是百万量级乘以平均几十万量级的除法最后结果就是十亿甚至万亿级别的运算程序跑完人都能睡一觉。这就是为什么我总强调写出能跑的代码只是第一步你还要大概估算一下复杂度。复杂度估算不需要特别高深你只需要数循环层数。一层循环数据量在百万级通常没问题两层循环数据量过万就要警惕三层循环超过一千就可能卡顿。应对思路无非两种把内层循环的计算量降下来比如素数判断只到sqrt(n)或者用空间换时间比如用数组缓存中间结果。后面的题目我会反复用到这个原则。3. 六个经典问题逐一拆解思路、代码、易错点3.1 水仙花数三位数自幂数的判断题目描述很简单一个三位数其各位数字的立方和等于它本身。典型例子是153因为1^3 5^3 3^3 153。要求输出所有满足条件的三位数。思路是遍历100到999对每个数字拆出个位、十位、百位然后求立方和。关键点是“拆数字”的循环写法。对153来说先num % 10得到个位3num / 10得到15再对15做同样的操作。这样写的好处是代码通用以后改成一个通用判断方法处理任意位数都不需要大改。public class NarcissisticNumber { public static boolean isNarcissistic(int num) { int original num; int sum 0; while (num 0) { int digit num % 10; sum digit * digit * digit; num / 10; } return sum original; } public static void main(String[] args) { for (int i 100; i 999; i) { if (isNarcissistic(i)) { System.out.println(i); } } } }这里有两个易错点。第一循环边界写成i 999如果你写成i 999就会漏掉999本身影响输出结果。第二sum digit * digit * digit这里直接连乘不要用Math.pow(digit, 3)因为Math.pow返回double和int做比较或累加时容易引入精度问题。输出结果是三个153、370、371、407我见过很多人漏掉407就是因为边界条件写错。3.2 素数判断从暴力试探到开方优化素数又称质数指大于1且只能被1和自身整除的正整数。最直观逻辑是假设要判断n就从2一直试到n - 1如果都没整除说明是素数。这个写法完全没有错但性能差。优化思路来自一个简单数学事实如果n有一个大于sqrt(n)的因子那它必然对应一个小于sqrt(n)的因子所以只要检查到sqrt(n)即可。public static boolean isPrime(int n) { if (n 2) { return false; } if (n 2) { return true; } if (n % 2 0) { return false; } for (int i 3; i * i n; i 2) { if (n % i 0) { return false; } } return true; }这段代码把判断2的倍数单独拿出来了循环从3开始且步长为2跳过所有偶数既简化了逻辑又减少了一半运算量。这里要特别注意n 2这个分支我见过不少人只写n 2为false结果忘了2是素数然后特别尴尬地发现isPrime(2)返回false。另一个边界是n 1它既不是素数也不是合数所以第一步就要排除掉。3.3 最大公约数与最小公倍数辗转相除法落地最大公约数GCD最经典的算法是辗转相除法也叫欧几里得算法。核心公式是gcd(a, b) gcd(b, a % b)不断用较小数对较大数取模直到余数为0最后一次的非零除数就是最大公约数。比如求gcd(48, 18)48 % 18 12接着18 % 12 6然后12 % 6 0所以最大公约数是6。用迭代写起来很简洁public static int gcd(int a, int b) { while (b ! 0) { int temp b; b a % b; a temp; } return a; }最小公倍数LCM的求法建立在GCD之上公式是a * b / gcd(a, b)。不过有一个工程细节最好先除再乘也就是写成a / gcd(a, b) * b而不是a * b / gcd(a, b)。因为a * b很容易超出int范围比如两个数都是10万乘积是100亿已经溢出。先除再乘能有效降低溢出风险。public static int lcm(int a, int b) { return a / gcd(a, b) * b; }这道题的变形考法也不少比如求三个数的最大公约数就可以先求前两个数的gcd再用结果和第三个数求一次gcd。本质上还是一层套一层没有太多新知识点但递归思想会在这里第一次出现。如果你用递归写gcd函数体只需要一句话return b 0 ? a : gcd(b, a % b);很漂亮但对初学者来说迭代版本反而更容易理解建议先掌握迭代再回来看递归。3.4 斐波那契数列递归陷阱与递推优化斐波那契数列的定义是F(0) 0, F(1) 1, F(n) F(n - 1) F(n - 2)。很多教材拿它当递归的入门例子代码很简洁public static int fib(int n) { if (n 1) { return n; } return fib(n - 1) fib(n - 2); }但直接这样写有个大坑重复计算极其严重。算fib(5)时要算fib(4)和fib(3)而fib(4)又需要fib(3)和fib(2)你会发现fib(3)被算了两次规模越大重复越夸张时间复杂度接近指数级。实测fib(45)就可能慢到你怀疑电脑坏了。所以我强烈建议初学者用循环递推而不是递归。public static long fib(int n) { if (n 1) { return n; } long prev 0; long curr 1; for (int i 2; i n; i) { long next prev curr; prev curr; curr next; } return curr; }这里我用long而不是int是因为斐波那契增长太快int很快就装不下了。前46项左右还在int范围内第47项就溢出为负数。用long也只是把“爆掉”的时间往后推了推最多支持到大概第92项。如果题目要求更大的项基本都会让你对某个大质数取模那就在每次相加后立刻%保持数值在安全范围内。3.5 回文数判断反转数字与溢出防护回文数的定义是正着读和倒着读都一样比如12321、101。一个简单的做法是把整个数字反转然后比较反转后是否和原数相等。比如123反转成321不相等所以不是回文。做法上利用x x * 10 num % 10循环就能完成反转。但这里有个隐患如果原始数很长反转过程中可能超过int上限。更聪明的做法是只反转一半。比如12321后半部分反转后是12而前半部分是123两者不相等但由于原数有奇数位中间的数字3不需要参与比较。判断条件可以设计为x reversed || x reversed / 10其中reversed / 10处理的是奇数位的情况。同时为了防止末尾是0的边界比如10反转一半时会出问题所以在开头先排除负数以及末尾为0且本身不是0的数。public static boolean isPalindrome(int x) { if (x 0 || (x % 10 0 x ! 0)) { return false; } int reversed 0; while (x reversed) { reversed reversed * 10 x % 10; x / 10; } return x reversed || x reversed / 10; }这段代码最精妙之处在于不需要处理溢出因为我们只反转一半反转后的数字不会比原数大太多。如果实在理解不了“反转一半”先用最朴素的全反转版本写出正确结果再逐步优化也是完全可以的。面试时候让自己能讲清楚每次循环中x和reversed的变化比背代码有效得多。3.6 完数判断因子遍历与平方根优化完数完全数指一个数恰好等于它的所有真因子之和真因子是不包括自身的正因子。比如6 1 2 328 1 2 4 7 14。初学者第一反应是遍历从1到n - 1的所有数能整除就累加。这个逻辑正确但效率差。优化思路和素数判断类似因子是成对出现的。如果i是n的因子那么n / i也是因子。所以我们只需要遍历到Math.sqrt(n)然后把i和n / i都加进去。要注意的是如果i恰好等于n / i比如n 16时i 4这个因子只能加一次不能重复累加。public static boolean isPerfect(int n) { if (n 2) { return false; } int sum 1; for (int i 2; i * i n; i) { if (n % i 0) { sum i; if (i ! n / i) { sum n / i; } } } return sum n; }这里的sum初始化为1因为任何大于1的整数都一定有真因子1。然后从2开始找利用因子配对特性一次性加两个。这个写法如果没想明白“配对”的含义很容易把平方因子重复加导致判断错误。比如n 16因子配对是(2, 8)和(4, 4)后者只算一次结果才是正确的1 2 4 8 15不等于16所以16不是完数。如果把4加两次就会得出错误的“16是完数”的结论。4. 调试实录这些坑我几乎每次都能看到4.1 Math.pow的double陷阱与精度丢失写完水仙花数后我收到过好几次类似的问题“代码明明照着写的为什么153判断不出来”一看代码里写的是Math.pow(num % 10, 3)。Math.pow返回的是doubleMath.pow(5, 3)的精确值虽然是125.0但在浮点数运算里double和int直接比较时有时会出现精度差异比如某个结果被表示为124.99999999999999累加后和原始整数比较就不相等。虽然三位数的场景里不一定每次都会出错但这是隐患数据范围一大就会放大。我的建议非常粗暴数字 * 数字 * 数字不香吗基础数值题里次方运算基本都是小整数次方直接连乘最可靠、可读性也高。只有在指数特别大、确实需要Math.pow的场景才用它并且做转换时要明确用(int)强转强转是截断而不是四舍五入这一点也经常被忽略。4.2 边界条件导致漏算循环范围与末尾数字边界条件问题主要集中两类。第一类是循环范围写错。水仙花数要求三位数有人写100到999也有人写100到1000后者虽然多循环一次但1000 % 10得到0不影响结果属于无害错误。但如果你把范围写成99到999就会把99也算进去99的各位立方和是1458并不是99所以虽然多算了一些数字但结果可能碰巧没被输出。真正危险的是把最大边界写成i 999这会漏掉999导致结果不完整。第二类边界是末尾数字为0的情况主要出现在回文数判断。输入10如果代码不单独排除x % 10 0 x ! 0的情况反转后会得到1然后错误地把10判断为回文数。类似的还有100、120等等。这类测试用例往往不会出现在教科书里但考官和判题系统最爱塞这种陷阱。4.3 结果不对时的三种排查套路遇到结果不对不要直接盯着代码发呆。我调试数值题基本会用三板斧。第一板斧是打印中间值。比如做水仙花数判断时在每个循环里打印当前数字、个位、十位、百位和累加和一眼就能看到哪一步开始和预期不符。第二板斧是固定输入把循环改成单测单个值比如传入153调用isNarcissistic()如果返回false就把函数单步调试看每一步的变量值。第三板斧是反向验证手算一遍答案比如手算判断153确实是1 125 27 153如果代码说它不是那问题一定出在代码逻辑或类型转换上。这三板斧本质上都在做一件事缩小问题范围。永远不要指望盯着代码用肉眼看出所有错误运行时打印和单测是最快的方式。我甚至在本地养成了每个函数都顺手写一个main方法测试边界的习惯比如测试isPrime(1)、isPrime(2)、isPrime(4)、isPrime(9)四个用例一跑边界几乎全暴露。4.4 面试与比赛中的常见变形考法基础题很少以原题形态直接出现在高端局里它一定会加个变形。比如水仙花数改成“统计从1到n之间所有自幂数并输出”就变成了一道区间统计题这时你需要在主循环里调用isNarcissistic(i)而且要注意自幂数不再局限于三位有四位玫瑰数、五位五角星数等判断方法必须通用化。素数更是重灾区判断区间素数、输出前n个素数、把一个数分解成质因数、判断孪生素数每一种都是在isPrime基础上做文章。回文数的变形也很多比如判断一个字符串是否是回文把链表反转后判断是否回文或者判断一个数在二进制表示下是否是回文。这些题目换汤不换药核心都是“对称性”判断。斐波那契数列的变形最常见的就是爬楼梯问题每次可以爬1阶或2阶爬到n阶有多少种方法答案就是F(n1)。完数则会变成找亲密数对即两个数各自的真因子和互为对方。所以不要小看这6个基础题它们就像数学里的公式推导变形无穷但根子只有一个。5. 组合实战一道“回文质数”区间统计题5.1 题目描述与考点拆解基础题练完之后我建议你做一道组合题来检验综合能力。这里给一个很经典的蓝桥杯风格题目给定区间[a, b]其中1 a b 1000000请输出这个区间内所有既是素数又是回文数的整数每个数占一行按从小到大顺序输出。这道题把前面学的两个核心判断函数isPrime和isPalindrome串起来了。它的难点不在于单个判断而在于你可能写出一个逻辑完全正确但超时的版本。比如你从a循环到b每个数分别调用两个判断而isPrime内部又是从2循环到sqrt(n)当b 1000000时总运算量虽然不至于爆炸但也有一定压力。这也是比赛题最常见的考察点基础逻辑要熟优化思路也要有。5.2 模块化实现isPrime与isPalindrome编写代码时我的习惯是拆函数判断素数一个函数、判断回文一个函数、主循环一个函数。这样每个函数都可以单独测试出现问题也不用在几百行代码里大海捞针。最终代码可以复用前面已经写好的判断方法先把正确性跑通。public class PalindromePrime { public static boolean isPrime(int n) { if (n 2) { return false; } if (n 2) { return true; } if (n % 2 0) { return false; } for (int i 3; i * i n; i 2) { if (n % i 0) { return false; } } return true; } public static boolean isPalindrome(int x) { if (x 0 || (x % 10 0 x ! 0)) { return false; } int reversed 0; while (x reversed) { reversed reversed * 10 x % 10; x / 10; } return x reversed || x reversed / 10; } public static void main(String[] args) { int a 1; int b 1000; for (int i a; i b; i) { if (isPrime(i) isPalindrome(i)) { System.out.println(i); } } } }注意main方法里的判断条件我写的是isPrime(i) isPalindrome(i)。这样写有个小优化因为isPrime通常比isPalindrome计算量小先判断素数如果不是素数就不需要进一步判断回文了能省下不少运算。这也体现了一个编码细节是短路运算前面的条件为false时后面的函数根本不会执行。5.3 完整代码与结果验证把a 1、b 1000输入后预期输出是2 3 5 7 11 101 131 151 181 191 313 353 373 383 727 757 787 797 919 929你可以把这组结果当成一个基准测试。我在实际运行时发现很多人第一次输出的结果里少了2因为isPrime(2)的边界写错了还有人漏掉了11因为回文判断里没有处理好奇数位数字的情况。如果你运行后能得到完全一致的输出说明这两个函数都写对了。再把边界改成a 1000、b 1000000区间大了以后程序也能在短时间内跑完这就说明性能基本达标。5.4 复杂度优化方向从判定到筛法如果题目把范围再扩大比如[1, 100000000]直接每次调用isPrime就有点吃力了。这时候可以先把素数一次性筛出来再判断回文。最常见的做法是埃氏筛准备一个布尔数组默认所有数都是素数从2开始把每个素数的倍数全部标记为合数。筛完以后isPrime从一个函数调用退化为一次数组查询复杂度从O(sqrt(n))降到了O(1)。这个思路就是典型的“空间换时间”。boolean[] isPrime new boolean[b 1]; Arrays.fill(isPrime, true); isPrime[0] false; isPrime[1] false; for (int i 2; i * i b; i) { if (isPrime[i]) { for (int j i * i; j b; j i) { isPrime[j] false; } } }在面试或比赛中你可以先给出函数判断版本让代码清晰可读再在追问下拿出筛法优化版本。这样做反而能展示你的思考层次基础能写优化也会。单纯一上来就写筛法反而容易因为细节太多而出错。我个人在带新人时也反复强调先正确再高效最后再考虑代码的行数最小化。最后分享一点实际感受这6个题每次刷的感觉都不一样。第一次是在学语法第二次是在练边界第三次你会发现自己在思考“这个循环能不能提前退出”“这个函数能不能通用化”。这些都是程序员成长的必经阶段。我建议你拿到代码后试着把三道题改成“区间统计版”比如输出1到n之间所有水仙花数统计1到n之间所有完数的个数。语法没变换个问法又是一套全新的训练这种扩展练习比反复刷同一道题要有效得多。