☰
面试算法题集中训练:六道经典题解析与实战技巧
2026/10/17 7:40:02 网站建设 项目流程

1. 面试算法题训练的整体思路拆解

1.1 为什么选择集中刷题而不是零散练习

很多同学准备面试时习惯今天刷一道明天刷一道,看起来每天都在学,实际上效率很低。我自己踩过这个坑,后来发现集中刷同一家公司的题目效果要好得多。原因很简单:同一家公司的面试题往往有固定的考察方向和难度梯度,集中攻克能让你在短时间内建立起对该公司的“题型直觉”。

这次我选了六道题,全部来自公开的面试题库平台。选这六道不是随便挑的,而是覆盖了算法面试中最核心的几类问题:数组操作、字符串处理、动态规划、二叉树遍历、链表操作和数学思维。这六类基本能覆盖大部分技术面试的手写代码环节。

集中训练还有一个好处是能形成对比。做完六道题之后回头看,你会发现有些技巧是通用的,比如双指针、递归拆解、状态转移。这些模式一旦内化,遇到新题也能快速找到切入点。

1.2 六道题的难度分布与考察重点

这六道题的难度大致是两易三中一难,符合大多数公司面试的梯度。前两道偏基础,主要看代码是否规范、边界条件是否考虑周全;中间三道需要一定的算法思维,考察你能否把问题抽象成经典模型;最后一道是压轴,通常需要结合多种技巧。

具体来说,第一道考察数组遍历和原地修改,第二道考察字符串的字符统计与匹配,第三道是经典的动态规划入门,第四道涉及二叉树的层序遍历变体,第五道是链表反转的进阶版,第六道是一道数学规律题。每道题我都会给出完整的解题思路、代码实现和复杂度分析。

1.3 刷题时容易忽略的三个关键点

很多人刷题只关注“能不能做出来”,但面试官看的不只是结果。我总结了三个容易被忽略但非常重要的点:

第一是代码的可读性。变量命名要清晰,不要用a、b、c这种。面试官看你的代码就像看你的简历,第一印象很重要。我习惯用有意义的英文单词,比如left、right、count、result。

第二是边界条件的处理。空数组、单元素、溢出、重复元素,这些情况你有没有考虑?我见过很多人代码逻辑没问题,但一跑测试用例就挂,就是因为没处理边界。

第三是复杂度分析的表达。做完题一定要主动说出时间复杂度和空间复杂度,并且解释为什么。这能体现你对算法本质的理解,而不是死记硬背。

提示:刷题时建议用纸笔先写一遍伪代码,再上机验证。这样能模拟面试时的白板环境,避免过度依赖编辑器的自动补全。

2. 六道题的核心细节解析与实操要点

2.1 第一题:数组中的重复元素查找

题目描述很直接:给定一个长度为n的数组,数组中的元素都在1到n之间,找出所有重复出现的元素。要求时间复杂度O(n),空间复杂度尽量低。

这道题我第一反应是用哈希表,遍历一遍把元素存入集合,遇到已存在的就加入结果。时间复杂度O(n),空间复杂度O(n)。但面试官通常会追问:能不能不用额外空间?

不用额外空间的做法是利用数组本身作为标记。因为元素范围是1到n,我们可以把每个元素的值减一作为下标,把对应位置的元素取负。如果发现某个位置已经是负数,说明这个下标对应的值已经出现过。这个方法很巧妙,但要注意恢复原数组,否则会污染输入。

def find_duplicates(nums): result = [] for num in nums: index = abs(num) - 1 if nums[index] < 0: result.append(abs(num)) else: nums[index] = -nums[index] # 恢复原数组 for i in range(len(nums)): nums[i] = abs(nums[i]) return result

这里有个细节:取绝对值是因为前面的操作可能把元素变成负数。恢复数组这一步很多人会忘,虽然不影响结果,但在实际工程中修改输入数据是大忌。

注意:这道题的前提是元素范围在1到n之间,如果范围不确定,就不能用下标标记法,老老实实用哈希表。

2.2 第二题:字符串中的第一个唯一字符

给定一个字符串,找到第一个不重复的字符并返回它的索引。如果不存在,返回-1。这道题考察的是字符统计的基本功。

最直观的做法是遍历两遍:第一遍用字典统计每个字符出现的次数,第二遍从头找第一个次数为1的字符。时间复杂度O(n),空间复杂度O(k),k是字符集大小。

def first_uniq_char(s): count = {} for ch in s: count[ch] = count.get(ch, 0) + 1 for i, ch in enumerate(s): if count[ch] == 1: return i return -1

这道题有个进阶问法:如果字符串是流式输入的,不能存储整个字符串怎么办?这时候可以用两个集合,一个记录出现过的字符,一个记录只出现一次的字符。流式处理时动态更新这两个集合,但这样就无法返回索引了,只能返回字符本身。

我在实际面试中遇到过变体:要求返回所有唯一字符的索引。这时候就不能提前返回,需要遍历完整个字符串。所以读题一定要仔细,不要看到“第一个”就条件反射地提前返回。

2.3 第三题:最大子数组和

这是动态规划的经典入门题。给定一个整数数组,找到一个具有最大和的连续子数组,返回其最大和。这道题的状态定义很关键:dp[i]表示以第i个元素结尾的最大子数组和。

状态转移方程是:dp[i] = max(dp[i-1] + nums[i], nums[i])。意思是,要么把当前元素接到前面的子数组后面,要么从当前元素重新开始。最后答案是max(dp[0], dp[1], ..., dp[n-1])。

def max_subarray(nums): if not nums: return 0 max_sum = nums[0] current_sum = nums[0] for num in nums[1:]: current_sum = max(num, current_sum + num) max_sum = max(max_sum, current_sum) return max_sum

空间优化后只需要两个变量,空间复杂度降到O(1)。这道题的关键是理解“以当前元素结尾”这个状态定义,很多人会定义成“前i个元素中的最大和”,那样转移方程会复杂很多。

提示:动态规划的题目,状态定义往往比转移方程更重要。定义对了,方程自然就出来了。

2.4 第四题:二叉树的层序遍历

层序遍历就是按层从左到右访问二叉树的所有节点。标准做法是用队列,先把根节点入队,然后循环:出队一个节点,访问它,把它的左右子节点入队。但这样只能得到一个一维数组,如果要按层分组,需要在每一轮循环中记录当前队列的大小。

from collections import deque def level_order(root): if not root: return [] result = [] queue = deque([root]) while queue: level_size = len(queue) level = [] for _ in range(level_size): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) return result

这道题的变体很多,比如锯齿形层序遍历(奇数层从左到右,偶数层从右到左),或者从底部往上层序遍历。核心逻辑不变,只是在收集结果时做一些调整。

我在面试中遇到过要求用DFS做层序遍历的,这时候需要记录每个节点的深度,把节点放入对应深度的列表中。DFS的好处是不需要队列,但递归深度可能受限于树的高度。

2.5 第五题:反转链表II

反转链表中从位置m到n的部分。这道题是基础链表反转的进阶版,难点在于处理边界和指针的衔接。

思路是:先找到第m-1个节点(pre),然后从第m个节点开始,用头插法把后面的节点逐个插到pre后面。具体操作是:记录当前节点cur和它的下一个节点next,把cur的next指向next的next,把next的next指向pre的next,把pre的next指向next。

def reverse_between(head, m, n): dummy = ListNode(0) dummy.next = head pre = dummy for _ in range(m - 1): pre = pre.next cur = pre.next for _ in range(n - m): next_node = cur.next cur.next = next_node.next next_node.next = pre.next pre.next = next_node return dummy.next

用dummy节点可以统一处理m=1的情况,这是链表题常用的技巧。头插法的关键是每次把cur后面的节点移到pre后面,移动n-m次后就完成了反转。

注意:链表题一定要画图。指针操作光靠脑子想很容易乱,画个图标注每一步的指针指向,能避免大部分错误。

2.6 第六题:数学规律题

最后一道是数学题:给定一个整数n,计算从1到n的所有整数中数字1出现的次数。这道题如果暴力遍历每个数字的每一位,时间复杂度是O(n log n),对于大n会超时。

正确的做法是逐位统计。对于每一位(个位、十位、百位...),计算这一位上1出现的次数。以个位为例,每10个数出现一次1,所以个位上1的次数是n/10 + (n%10 >= 1 ? 1 : 0)。十位上,每100个数出现10次1,以此类推。

def count_digit_one(n): count = 0 factor = 1 while factor <= n: high = n // (factor * 10) cur = (n // factor) % 10 low = n % factor if cur == 0: count += high * factor elif cur == 1: count += high * factor + low + 1 else: count += (high + 1) * factor factor *= 10 return count

这道题的关键是理解每一位的统计规律。把数字拆成高位、当前位、低位三部分,根据当前位的值分三种情况计算。这种逐位统计的思想在很多数学题中都能用到。

3. 实操过程与核心环节实现

3.1 环境准备与代码组织

我用的环境是Python 3.9,编辑器是VS Code。刷题时我习惯把每道题写成一个独立的函数,放在同一个文件里,方便对比和复习。文件结构大概是这样的:

# interview_questions.py def find_duplicates(nums): # 第一题 pass def first_uniq_char(s): # 第二题 pass # ... 其他题目

每道题下面我会写几个测试用例,用assert验证结果。这样改代码后能快速知道有没有破坏原有逻辑。

if __name__ == "__main__": assert find_duplicates([4,3,2,7,8,2,3,1]) == [2,3] assert first_uniq_char("leetcode") == 0 assert max_subarray([-2,1,-3,4,-1,2,1,-5,4]) == 6 # ... 其他测试 print("All tests passed!")

测试用例要覆盖正常情况、边界情况和异常情况。比如空数组、单元素、全部重复、全部不重复等。

3.2 每道题的调试过程记录

第一题我一开始忘了恢复原数组,测试时发现输入被修改了,加上恢复逻辑后通过。这里有个小技巧:如果不想修改原数组,可以先用切片复制一份,但那样空间复杂度就上去了。

第二题我第一版代码用了两次遍历,后来想到可以用有序字典优化,但Python 3.7之后普通字典就是有序的,所以没必要。这道题的时间瓶颈在第二遍遍历,如果字符串很长,可以考虑用数组代替字典,因为字符集是有限的。

第三题我一开始把max_sum初始化为0,结果遇到全负数的数组就返回0了,但正确答案应该是最大的那个负数。改成初始化为nums[0]后解决。这个坑很典型,动态规划的初始化一定要小心。

第四题我用了deque,因为list的pop(0)是O(n)的,deque的popleft是O(1)。这个细节在数据量大时影响很明显。

第五题我画了三张图才把指针关系理清楚。头插法的关键是每次移动后,pre.next指向新移过来的节点,cur始终指向反转段的第一个节点(移动后变成最后一个)。

第六题我推导了半小时才把三种情况的公式写对。建议在纸上列出n=123时每一位的计算过程,验证公式是否正确。

3.3 复杂度分析与优化空间

六道题的复杂度汇总如下:

题目时间复杂度空间复杂度优化空间
重复元素查找O(n)O(1)已最优
第一个唯一字符O(n)O(k)可用数组替代字典
最大子数组和O(n)O(1)已最优
层序遍历O(n)O(w)w为树的最大宽度
反转链表IIO(n)O(1)已最优
数字1的个数O(log n)O(1)已最优

大部分题目的时间复杂度已经是最优了,因为至少要遍历一遍输入。空间复杂度方面,除了层序遍历必须用队列外,其他都可以做到O(1)。

提示:面试时如果面试官问“能不能再优化”,通常是指空间复杂度。时间复杂度的优化空间一般不大,除非你用了暴力解法。

4. 常见问题与排查技巧实录

4.1 刷题过程中遇到的典型问题

问题一:代码在本地能跑,但提交后报错。这种情况通常是边界条件没处理好。比如空输入、单元素、整数溢出等。解决办法是写完代码后,手动构造几个极端用例跑一遍。

问题二:思路对了但代码写不出来。这说明你对语言的API不够熟悉。比如Python中字典的get方法、列表的切片、deque的用法。建议平时多写代码,不要只用IDE的自动补全。

问题三:做过的题过几天又忘了。这是正常的,人的记忆需要重复。我的做法是每道题做完后写一段简短的笔记,记录核心思路和易错点。复习时只看笔记,能想起来就过,想不起来再重新做。

问题四:面试时紧张导致大脑空白。这种情况可以通过模拟面试来缓解。找朋友或者用录音的方式,把解题过程说出来。多练几次就能适应。

4.2 常见问题速查表

问题现象可能原因排查方法解决方案
结果不对边界未处理检查空输入、单元素加if判断
超时复杂度过高分析循环嵌套换算法或加缓存
内存超限空间复杂度过高检查是否用了额外数组原地修改或复用输入
指针错误链表操作顺序不对画图标注指针用dummy节点简化
递归栈溢出递归深度过大检查递归终止条件改迭代或加记忆化

4.3 独家避坑技巧

第一,先写注释再写代码。把每一步要做什么用注释写出来,然后再填代码。这样逻辑清晰,也不容易漏步骤。

第二,变量命名要能自解释。比如用left、right表示左右指针,用prev、curr表示前后节点。不要用i、j、k这种,除非是循环变量。

第三,善用dummy节点。链表题中,dummy节点能统一处理头节点被修改的情况,减少边界判断。

第四,动态规划先写递归再改迭代。递归版本更直观,写出来后再改成迭代版本,能避免状态转移写错。

第五,数学题先手算几个例子。比如n=1,2,3,10,100,观察规律,再推导公式。不要一上来就写代码。

第六,做完题后主动说复杂度。这是面试的加分项,能体现你的专业素养。即使面试官没问,你也可以主动提。

注意:刷题不是越多越好,而是越精越好。一道题做三遍,比三道题各做一遍效果好得多。第一遍理解思路,第二遍不看答案写出来,第三遍优化代码和复杂度。

4.4 从面试官视角看代码质量

我后来有机会参与过几次面试,发现面试官看代码时最关注这几点:代码能不能跑通、边界有没有处理、变量命名是否清晰、有没有冗余代码、复杂度是否合理。

有个候选人的代码逻辑完全正确,但变量名全是a、b、c,面试官看完后说“这代码我不想维护”。所以命名真的很重要。

还有个候选人用了很巧妙的算法,但没处理空输入,面试官直接说“你这个代码上线会出事故”。所以边界条件不是可选项,是必选项。

最后,代码要简洁。能一行写完的不要写三行,但也不要为了简洁牺牲可读性。比如Python的列表推导式很好用,但嵌套三层就不如拆开写循环。

5. 从刷题到面试的实战转化

5.1 如何把刷题成果转化为面试表现

刷题和面试是两回事。刷题时你可以慢慢想,面试时有时间压力,还要边写边解释。我的经验是,面试前一周开始模拟面试,找几道没做过的题,限时20分钟,用白板或纸笔写。

写的时候要出声思考,把思路说出来。比如“我先看一下边界条件...这里可以用双指针...时间复杂度是O(n)”。这样面试官能跟上你的思路,即使最后没写完,也能给部分分。

另外,面试时要主动沟通。如果卡住了,不要沉默,可以说“我目前想到的是暴力解法,但复杂度太高,我在想能不能优化”。面试官通常会给你提示。

5.2 不同公司的考察偏好

虽然不能提具体公司名,但可以聊聊不同类型公司的偏好。一类公司喜欢考经典算法题,题目描述清晰,要求最优解。另一类公司喜欢结合实际场景,题目描述较长,需要你先抽象出模型。

还有一类公司会考系统设计,算法题只是其中一部分。这时候代码质量比算法巧妙更重要,因为实际工程中可维护性优先。

准备的时候可以针对目标公司的类型调整策略。如果偏工程,多练代码规范和边界处理;如果偏算法,多练动态规划和图论。

5.3 长期保持算法手感的方法

面试结束后也不要完全丢掉算法。我现在的习惯是每周做两道题,保持手感。选题时优先选没做过的,避免形成思维定式。

另外,可以参与一些开源项目或者自己写小工具,把算法用到实际场景中。比如用动态规划优化一个数据处理脚本,或者用二叉树遍历解析配置文件。这样既能巩固算法,又能积累项目经验。

最后,建议建一个自己的代码库,把做过的题和解题思路整理进去。过一段时间回头看,你会发现自己的进步,也能发现之前的不足。这个习惯我坚持了三年,受益匪浅。


我个人在实际操作中的体会是,刷题最怕的是“假努力”——看起来做了很多题,但每道题都是看答案过的,没有真正理解。真正有效的方法是:每道题先自己想15分钟,想不出来再看答案,看完答案后关掉,自己重新写一遍,写完后总结这道题的核心技巧和易错点。这个过程很慢,但效果扎实。六道题我花了整整一个周末,但收获比之前刷六十道题还大。

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

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

立即咨询