☰
Java面试22道经典算法题:从斐波那契到菱形打印的避坑指南
2026/10/14 2:07:54 网站建设 项目流程

简介:面向Java初、中高级开发者的编程面试题资源,聚焦面试中高频出现的基础编程题,涵盖斐波那契数列、素数判断、水仙花数、分解质因数等经典题目。包体仅含1个doc文档,整体大小约104KB,内容以代码示例与逐题解析为主,轻量便捷,适合笔试前或技术面试冲刺阶段反复对照练习。目前已有141人学习,可作日常备查与突击复习之用。文档对每道题均给出完整Java代码和核心思路,例如用循环代替递归计算兔子总数,利用Math.sqrt缩小整除判断范围,通过取模和除法拆解三位数以验证水仙花数,以及用while循环从最小质数2开始逐层分解质因数。这些典型题目覆盖循环结构、条件判断、算术运算及基础算法思想,能有效巩固编程基本功;通过阅读代码并尝试手写复现,可以提升现场编程的逻辑条理和临场应变能力,为这类实战考核做好更充分准备。

1. Java编程面试题:22道经典算法题,从兔子数列到菱形打印

Java编程面试题里最容易被临时考倒的,往往不是框架八股,反而是这些基础算法小题。这份资源是流传多年的一套Java面试题库,22道题从兔子的斐波那契数列一路做到菱形打印,覆盖循环、分支、数字拆分、递推和字符统计,几乎把初级笔试的高频考点过了一遍。不管你是准备校招笔试,还是给新人做入门编程摸底,这套题都是现成的训练素材。代码是典型的老式控制台写法,能跑但细节经不起推敲,逐题拆一遍正好看清出题人想考什么,也方便改出一版真正能拿给面试官看的代码。

2. 数学算法与循环结构:滚动变量、素数判断与数字拆分

这一类的题,核心是看你能不能把数学关系用最朴素的循环表达出来。面试手写代码时没有IDE提示,也没有现成工具类可查,循环边界、递推变量、取模拆数这三样基本功直接决定能不能一次写对。

2.1 斐波那契数列:滚动变量的循环写法

程序1的兔子问题本质就是斐波那契数列。原题给出的循环版本是三步滚动:

public class lianxi01 { public static void main(String[] args) { int f1 = 1, f2 = 1, f, M = 24; for (int i = 3; i <= M; i++) { f = f2; // 暂存旧值 f2 = f1 + f2; // 新值 = 前两项之和 f1 = f; // 旧值回填 System.out.println("第" + i + "个月的兔子对数: " + f2); } } }

关键就在f = f2; f2 = f1 + f2; f1 = f;这三步。如果不做暂存直接写f1 = f2; f2 = f1 + f2;,f1被覆盖后下一步算出来的就是错的。理解了滚动变量的顺序,比背递归公式重要得多。面试时如果追问时间复杂度和空间复杂度,循环版是O(n)时间、O(1)空间,递归版是O(2^n)时间,这题答到这里基本就到点上了。

实际运行还有个容易被忽略的边界:月数M改成50时int会溢出成负数。主动说出"结果超过int范围,需要换long或BigInteger"是明显的加分项。

2.2 素数判断:循环上限与标志位

程序2要求统计101到200之间有多少个素数,并输出全部。原题的判断逻辑是"用2到sqrt(n)逐个试除",这个数学思路是对的,但代码里的标志位绕了个圈。我一般会改成更直白的写法:

public class PrimeCount { public static void main(String[] args) { int count = 0; for (int i = 101; i < 200; i++) { boolean isPrime = true; for (int j = 2; j <= Math.sqrt(i); j++) { if (i % j == 0) { isPrime = false; break; } } if (isPrime) { count++; System.out.println(i); } } System.out.println("素数个数是: " + count); } }

这里改了两处。第一,标志位默认值从false改成true,只有确认被整除才置false,逻辑一眼就能看懂。第二,外层循环从i+=2改成i++。原题用步长2跳过偶数,在101到200这个区间确实没问题,但一旦把范围挪到别的区间,这种"优化"就容易翻车。面试时能把这两点改动的理由说清楚,比背答案强得多。

内层循环上限用Math.sqrt(i)是这题的核心考点。理由是:一个合数若有因子,必然有一个因子不超过它的平方根,超过平方根的那一半是配对出现的。想明白这层对称关系,就能理解为什么不用循环到i本身。

2.3 水仙花数与数字拆分

程序3的水仙花数是数字拆分里的经典题。思路很直接:把三位数的百位、十位、个位分别拆出来,判断立方和是否等于原数。

public class Narcissus { public static void main(String[] args) { for (int m = 101; m < 1000; m++) { int hundreds = m / 100; int tens = m % 100 / 10; int ones = m % 10; if (hundreds * hundreds * hundreds + tens * tens * tens + ones * ones * ones == m) { System.out.println(m + " 是一个水仙花数"); } } } }

数字拆分的三个表达式里,最容易写错的是十位。m % 100 / 10是先模100得到后两位,再除10得到十位;m / 10 % 10是先除10丢掉个位,再模10得到十位。两种写法结果一样,但前者更贴近"先取后两位"的思路。这类数字拆分题在笔试里出现频率很高,建议把取百位、十位、个位这一组写法练成肌肉记忆。

这里还有个延伸考点:如果把"三位数"改成"任意位数"求水仙花数,直接套这个写法就不够了。常见做法是循环加Math.pow或者转成字符串逐位取,能现场改出这版,说明你对题目理解不是停留在背代码。

2.4 分解质因数:while循环加最小质数

程序4要求把一个正整数分解成质因数的乘积,例如90输出90=233*5。原题用的是从2开始的候选因子k配合while循环:

public static void factor(int n) { int k = 2; while (k <= n) { if (k == n) { System.out.print(n); break; } else if (n % k == 0) { System.out.print(k + "*"); n = n / k; } else { k++; } } }

这个写法的高明之处在于,找到一个因子后不重置k。因为同一个质因数可能连续出现多次,比如90里的3出现两次,k保持不动就能在下一轮继续试除同一个数。如果n一直不能被k整除,k再递增。相比"先枚举所有质数再逐个试"的方案,这个写法少了一层循环,手写时也不容易乱。

有一个小边界值得注意:输入n为1或0时,while循环压根不进入,什么也不输出。面试时我会主动加一个if (n < 2) return;的防御判断,这种细节考官通常看在眼里。

3. 条件分支与状态计算:三元嵌套、辗转相除与区间提成

这一章的题目开始掺杂输入处理和复杂分支。面试现场写这类题,最大的风险不是不会算法,而是边界条件覆盖不全。一道分支题至少有正常值、临界值、非法值三类输入要测,能主动想到这一点,代码质量就拉开差距了。

3.1 三元运算符嵌套与成绩分级

程序5把成绩分成A/B/C三档,原题用一行三元运算符嵌套:

char grade = x >= 90 ? 'A' : x >= 60 ? 'B' : 'C';

三元运算符是右结合的,等价于x >= 90 ? 'A' : (x >= 60 ? 'B' : 'C')。这行代码简洁,但有两个隐含问题。第一个是输入范围没校验,负数和超过100的成绩都会落到'C'档;第二个是Scanner.nextInt()遇到非数字输入会抛InputMismatchException,笔试环境下如果允许,加一层try-catch或者手动校验输入范围会更稳。

我一般会建议面试时先写这种能跑通的简洁版,然后在口头补充说明"实际生产环境需要校验输入范围",这样既展示了编码速度,又展示了防御性编程意识。

3.2 最大公约数与最小公倍数

程序6求最大公约数用的是辗转相除法,也就是欧几里得算法。原题的实现先做大小交换,再循环取余,逻辑完整,但代码偏长。手写时有个简化写法:

public static int gcd(int x, int y) { while (y != 0) { int temp = y; y = x % y; x = temp; } return x; }

这个版本不需要先比较大小,因为x % y在x小于y时自然会得到x,下一轮角色就互换了,省掉交换逻辑反而更不容易写错。最小公倍数是两数乘积除以最大公约数,要注意乘积可能溢出,改成x / gcd(x, y) * y更安全。

笔试里经常有人把辗转相除和更相减损术混淆。如果用减法实现,大数会循环很多次,效率差。面试官如果追问"为什么选辗转相除",答"每次取余至少让数字规模减半,log级别收敛"就足够了。

3.3 奖金提成的分段区间

程序12是企业奖金按利润分段提成,经典的多区间if-else题。原题写了六段区间,代码很长,核心难点是区间边界不能重叠也不能遗漏。原题用的是x > 0 && x <= 10、x > 10 && x <= 20这种左开右闭写法,逻辑自洽,但中间几段把累计提成直接硬编码成了20 * 0.175这种常量,一旦提成比例调整就得重写整段。

更可维护的写法是先把各段阈值和比例存成数组,再用循环累加计算。面试时提到这种思路,比背原题加分得多。不过如果时间紧张,直接写if-else也能过,关键是每段分支的边界值要测一遍,尤其是利润正好等于10万、20万、40万这些临界点。

3.4 日期计算与闰年判断

程序14要求输入年月日判断是这一年的第几天。这种题最阴险的地方在闰年判断,平年二月28天,闰年29天。闰年条件要按顺序写:

boolean isLeap = (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);

这个顺序不能反,否则1900年这种能被100整除但不能被400整除的年份会被误判成闰年。原题用switch挨个case加月份天数,写法冗长但直白。更简洁的做法是用数组存每个月天数:

int[] daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap) daysInMonth[1] = 29; int dayOfYear = 0; for (int i = 0; i < month - 1; i++) { dayOfYear += daysInMonth[i]; } dayOfYear += day;

这个版本把数据和逻辑分离,比switch堆代码好读。面试时可以提一句"用数组代替switch,新增逻辑不用改流程",考官一般会认可。

原题还有一层bug:do-while校验里只检查了month在1到12、day在1到31,但2月30日这种非法组合仍然能通过。这个坑留到第5章展开说。

3.5 三个数排序的交换逻辑

程序15输入三个整数,要求由小到大输出。核心就是三次比较加交换,两个变量交换必须靠临时变量中转:

if (x > y) { int t = x; x = y; y = t; } if (x > z) { int t = x; x = z; z = t; } if (y > z) { int t = y; y = z; z = t; }

这三个if的顺序不能乱,先保证x最小,再让y和z排好。很多人写第一个if时忘掉临时变量,直接x = y; y = x,数据就丢了。面试手写这种题,宁可慢一点把三步写全,也不要为了省行数翻车。

提示:三个数以上的排序建议直接用Arrays.sort,但面试官大概率会追问底层原理,手写比较交换反而能展示基本功。

4. 字符处理与模式输出:ASCII边界、连加累乘与双重循环

这一章转向字符操作和图形输出,代码技巧集中在字符编码判断、累加变量更新、双重循环的边界控制上。这几类题在笔试里出现频率很高,因为它们能同时考察循环、字符串处理和格式化输出。

4.1 字符统计:toCharArray与ASCII边界

程序7统计一行字符里英文字母、空格、数字和其他字符的个数。原题的思路是对的,但代码有一个硬伤:比较的是数组名而不是数组元素。修正后应该是:

Scanner sc = new Scanner(System.in); String s = sc.nextLine(); char[] ch = s.toCharArray(); int digital = 0, character = 0, blank = 0, other = 0; for (int i = 0; i < ch.length; i++) { if (ch[i] >= '0' && ch[i] <= '9') { digital++; } else if ((ch[i] >= 'a' && ch[i] <= 'z') || (ch[i] >= 'A' && ch[i] <= 'Z')) { character++; } else if (ch[i] == ' ') { blank++; } else { other++; } }

原题里ch >= '0'这种比较在Java里连编译都过不了,因为数组对象不能和字符直接比较。另外大写字母的条件原题写的是ch > 'A' && ch <= 'Z',少了一个等于,导致大写A被漏统计。这种ASCII区间判断的边界错误非常隐蔽,面试时拿'A'、'Z'、'a'、'9'、空格各测一个字符就能全暴露。

中文字符在这个统计里会落入other分支,因为Java的char是Unicode编码,中文字符的码点远大于ASCII范围。如果有需求区分中西文,得用Character.UnicodeScript或者正则表达式,但原题场景用不上。

4.2 s=a+aa+aaa:连加式的循环变量更新

程序8求a + aa + aaa + ...的和,a由键盘输入,项数n也由键盘控制。原题用两个变量维护递推:

long a, b = 0, sum = 0; Scanner s = new Scanner(System.in); a = s.nextInt(); int n = s.nextInt(); int i = 0; while (i < n) { b = b + a; // b: 当前项 sum = sum + b; // sum: 累计和 a = a * 10; // a: 下一位权值 i++; } System.out.println(sum);

这段代码的妙处是只用了一个while循环,b表示当前项,sum累加,a不断乘10生成下一位。比如a=2,第一轮b=2,sum=2,a变成20;第二轮b=22,sum=24,a变成200;第三轮b=222,sum=246,正好对应2+22+222。

原题用long是考虑到累加会超出int范围,但项数一大long同样溢出。九个9连加就是九亿多,十项必爆int,几十项long也撑不住。真想算大数,得用BigInteger。面试时问一句"项数上限是多少",能答出这个溢出边界,说明你对数值范围有概念。

4.3 九九乘法表与菱形打印:双重循环的边界控制

程序16输出9x9乘法口诀,程序19打印菱形,这两个都是双重循环的典型题。乘法口诀的核心是内层循环上限取j <= i,保证只输出左下三角:

for (int i = 1; i < 10; i++) { for (int j = 1; j <= i; j++) { System.out.print(j + "*" + i + "=" + (j * i) + "\t"); } System.out.println(); }

原题用字符串拼接加手动补空格对齐,结果在控制台里歪歪扭扭。换成printf("%d*%d=%-4d", j, i, j*i)用左对齐宽度4,输出就整齐了。面试时顺口提一句"用printf比字符串拼接更规范",这种小细节很能体现工程习惯。

菱形打印的难点在于每一行的空格数和星号数都是动态计算的。我通常先在草稿纸上写出行号、空格数、星号数的对应表,再对应写循环。上半部分空格递减、星号递增,下半部分反过来。原题要求高和宽必须相等且为奇数,这个约束写注释里了,如果改成偶数会错位。笔试时直接在循环里写死H=7可以,但最好改成参数传入,方便验证多组尺寸。

4.4 比赛配对:约束条件下的组合输出

程序18是两个乒乓球队的配对问题,甲队a、b、c对阵乙队x、y、z,条件是a不和x比,c不和x、z比。原题用三层嵌套循环加continue跳过非法组合,本质是穷举所有配对再过滤约束。

这个题改为用约束条件直接排除,比原题的硬编码continue链更清晰。所谓匹配本质是排列组合,先列出所有可能,再逐个判断约束是否满足。面试时能指出"continue铺开写不利于扩展,建议把禁赛条件收集成集合再做过滤",这一层的抽象能力比跑出结果更有价值。原题的硬编码版本一旦条件变化,比如b也不和x比,代码就得改多处,这就是工程上说的可维护性问题。

5. 避坑:原题代码里的五处典型问题与修正方法

这套题流传多年,代码有不少从早期教材或C语言版本迁过来的痕迹,有的地方写得绕,有的地方直接是bug。我整理了自己拆题时踩过的五处典型坑,按"现象、原因、解决"三句话说明,方便你对照自己的代码排查。

5.1 素数判断的标志位绕圈

现象:程序2能跑出正确结果,但标志位b的写法很难读懂,把它挪到其他区间就不确定对不对。

原因:b初始为false,内层循环每次在"不能整除"时设true,一旦某轮整除就置false并break。碰巧在101到200范围内逻辑自洽,换成别的区间就说不准了。

解决:把标志位默认true,只有确认整除才置false。这个改动让判断逻辑和数学定义完全一致,不再依赖循环执行的先后顺序。

5.2 字符统计数组下标丢失

现象:程序7直接编译不过,报错信息指向数组和字符比较的那几行。

原因:ch是char[],原题比较时却写成了ch >= '0'这种形式,典型的从C数组版迁移到Java时漏掉了下标[i]。

解决:全部改成ch[i],同时修正大写字母判断的>为>=。改完记得用'A'这个边界字符验证一遍。

5.3 日期校验挡不住非法日期

现象:程序14里输入2月30日,程序会按正常日期计算,输出错误的天数序号。

原因:do-while循环只检查了月份在1到12、日期在1到31,没按具体月份校验天数上限,2月30日这种不存在的日期漏过去了。

解决:把天数上限和月份映射放在一起判断,先用数组拿到当月最大天数,再做区间校验。这属于防御性编程的常见套路,值得练习。

5.4 "整数"范围理解偏差

现象:程序13求"加上100后是完全平方数"的数,原题从x=1开始遍历,评论区很多人也在问为什么不是从负数开始。

原因:题目原文是"一个整数",按数学定义应包括负整数。原题代码注释里自己也承认,把整数理解成了正整数,导致漏掉了满足条件的负数。

解决:既然是面试题,按常规理解写正整数版本没问题,但在口头补充时说明"负数场景如何处理"反而能展示思维的全面性。

5.5 比赛配对硬编码条件

现象:程序18输出结果恰好是三组正确配对,但只要加一条约束条件,输出就全乱了。

原因:所有排除逻辑都用continue写死在双层循环里,条件之间没有任何抽象。如果规则一改,continue链就得重新梳理。

解决:把禁赛关系抽象成集合或布尔矩阵,再统一过滤。这个小重构既能让代码变短,也能应对规则变化,面试时主动做这种优化会给人留下好印象。

6. 面试现场怎么用:把旧题改写成能拿分的版本

这套题用来刷熟练度没问题,但直接照原样背去面试,反而容易暴露问题。我的习惯是拿到一道题先做四步改造,改造完的版本才是真正能拿给面试官看的。

第一步是方法抽取。原题逻辑全塞在main里,变量名是f1、f2、x、y这类无意义缩写。把核心逻辑抽成独立方法,例如斐波那契抽成int fib(int n),最大公约数抽成int gcd(int a, int b)。方法签名自带语义,面试官一看就知道你具备模块化思维。

第二步是边界补全。给每个方法补上输入校验和溢出保护。比如成绩分级先校验score < 0 || score > 100就抛IllegalArgumentException;斐波那契的返回值改成long或BigInteger;日期计算对2月30日直接拒绝。这些代码多写五行,但展现的是生产环境意识。

第三步是复杂度说明。每道题做完,主动说出时间和空间复杂度。例如斐波那契循环版O(n)、素数判断O(n√n)、辗转相除O(log n)。面试官问"还能不能优化",斐波那契可以提矩阵快速幂O(log n),素数判断可以提只遍历到sqrt的优化,能接住追问就稳了。

第四步是写验证代码。不要求现场写完整JUnit测试,但可以在main里多放几个断言性质的调用:

System.out.println(fib(10)); // 期望 55 System.out.println(gcd(12, 8)); // 期望 4 System.out.println(lcm(12, 8)); // 期望 24 System.out.println(isPrime(17)); // 期望 true System.out.println(isPrime(9)); // 期望 false

这几行输出比任何口头解释都有说服力。

用这套方法过完这22道题以后,我的一个习惯是每道题强制自己手写至少两版,第一版按原题思路快速跑通,第二版立即重构优化。从那以后我每次准备笔试或带新人做练习,都强制走一遍"拆题、改边界、抽方法、写断言"的流程,效果比闷头刷几十道新题好得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询