数据结构基础:先打好地基再盖楼
聊到算法题,很多人一上来就闷头刷LeetCode,结果刷了三个月还是找不到工作。为啥?因为地基没打牢。
数组、链表、栈、队列、哈希表、树、图,这七个数据结构是算法题里出现频率最高的”常客”。你得先明白它们各自的特性和适用场景,才能在选择解题思路时快速判断。
先说数组。数组是最简单的数据结构,但面试里经常出现一些变种题,比如”两数之和”这种经典入门题,看似简单,实际上考察的是你对哈希表的理解:
# 暴力解法 O(n^2)
def twoSum(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
# 哈希表优化 O(n)
def twoSum(nums, target):
hash_map = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hash_map:
return [hash_map[complement], i]
hash_map[num] = i
return []
这道题看似简单,但它背后涉及的核心思想是空间换时间,这个思想在后续很多算法里都会用到。
再说链表。链表在面试里出现频率极高,”反转链表”、”判断环”、”合并两个有序链表”这些题几乎是必考的。链表题的核心是指针操作,画图是最好的解题方式:
# 反转链表经典题
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverseList(head):
prev = None
curr = head
while curr:
next_temp = curr.next # 先保存下一个节点
curr.next = prev # 反转指针
prev = curr # prev前移
curr = next_temp # curr前移
return prev
这个代码看着简单,但很多初学者在这里容易出错。我建议你拿笔画画,每一步都要理解指针怎么移动。
栈和队列这两个结构看起来相似,但本质完全不同。栈是后进先出,队列是先进先出。它们在各种算法题里都有应用,比如括号匹配问题用栈,BFS广度优先搜索用队列。
树是最复杂也是面试出现频率最高的数据结构之一。二叉树、二叉搜索树、红黑树、B树……光是名字就够吓人的。但不用怕,面试里最常考的就是二叉树的遍历(前序、中序、后序、层序)和递归解题。
# 二叉树的三种递归遍历
def preorder(root):
if not root:
return
print(root.val) # 先访问根节点
preorder(root.left) # 再递归左子树
preorder(root.right) # 最后递归右子树
def inorder(root):
if not root:
return
inorder(root.left) # 先递归左子树
print(root.val) # 再访问根节点
inorder(root.right) # 最后递归右子树
def postorder(root):
if not root:
return
postorder(root.left) # 先递归左子树
postorder(root.right) # 再递归右子树
print(root.val) # 最后访问根节点
图的结构相对复杂,但面试里考得也没那么深。最常考的是DFS深度优先搜索和BFS广度优先搜索,以及图的遍历。
高频算法题分类:把题型摸透再刷题
光懂数据结构还不够,算法题是有规律可寻的。我把面试里出现频率最高的题型给你分成几大类,这样你刷题的时候就有方向了。
第一类:数组和字符串
这类题占面试算法题的40%左右,是最基础的题型。常见的有:
- 两数之和、三数之和
- 最长无重复字符子串
- 盛最多水的容器
- 字符串反转、回文判断
- 滑动窗口技巧题
滑动窗口是这类题里最重要的技巧之一,它能把很多O(n²)的解法优化到O(n):
# 滑动窗口经典题:最长无重复字符子串
def lengthOfLongestSubstring(s):
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
# 当窗口内有重复字符时,收缩左边界
while s[right] in char_set:
char_set.remove(s[left])
left += 1
# 更新最大长度
max_len = max(max_len, right - left + 1)
# 将当前字符加入窗口
char_set.add(s[right])
return max_len
第二类:链表
链表题的套路比较固定,主要有几种:
- 快慢指针找中点/判断环
- 反转链表
- 合并有序链表
- 删除链表倒数第N个节点
- 链表排序
快慢指针是链表题里最常用的技巧,很多题一眼就能看出来用这个:
# 快慢指针判断链表是否有环
def hasCycle(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next # 慢指针走一步
fast = fast.next.next # 快指针走两步
if slow == fast: # 相遇说明有环
return True
return False
第三类:二叉树
二叉树题的核心是递归和层序遍历:
- 最大深度、最小深度
- 是否平衡二叉树
- 路径总和
- 二叉树的序列化与反序列化
- 最近公共祖先
# 二叉树的最大深度
def maxDepth(root):
if not root:
return 0
return 1 + max(maxDepth(root.left), maxDepth(root.right))
看起来很简单对吧?但这道题是后续很多复杂题的基础,比如判断平衡二叉树、求最近公共祖先,都是在最大深度的思路上加一点变化。
第四类:动态规划
动态规划是面试里最难的部分,也是很多人心中的”噩梦”。但动态规划其实是有套路可循的:
- 先找到状态转移方程
- 确定base case
- 自顶向下或自底向上求解
最经典的例子是”爬楼梯”问题:
# 爬楼梯 - 最基础的动态规划
def climbStairs(n):
if n <= 2:
return n
# dp[i]表示爬到第i层的方法数
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # 状态转移方程
return dp[n]
# 空间优化版本
def climbStairs_optimized(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
动态规划的难点不在于代码本身,而在于怎么找到状态转移方程。我建议你从简单的题目开始,比如斐波那契数列、爬楼梯、背包问题,慢慢培养找规律的感觉。
第五类:回溯算法
回溯算法本质上是穷举搜索,通过剪枝来优化。常见的题目有:
- 全排列
- 子集
- N皇后
- 数独
- 组合总和
# 全排列回溯算法
def permute(nums):
result = []
def backtrack(path, used):
# 终止条件
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
# 做选择
path.append(nums[i])
used[i] = True
# 递归
backtrack(path, used)
# 撤销选择
used[i] = False
path.pop()
backtrack([], [False] * len(nums))
return result
回溯算法的题目看起来复杂,但套路是固定的:做选择→递归→撤销选择。你只要记住这个框架,大部分题都能套进去。
第六类:二分查找
二分查找的变形非常多,也是面试常考题。核心思想是每次把搜索范围缩小一半:
# 标准二分查找
def binarySearch(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止溢出
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
变形题包括:旋转数组找最小值、搜索插入位置、在排序数组中查找元素的第一个和最后一个位置等。这些题的本质都是一样的,只是边界条件略有不同。
刷题方法论:别光刷,要会刷
很多学员跟我抱怨”刷了上百道题还是不会”,问题不在题量,而在方法。我给你一套经过验证的刷题方法论:
第一阶段:按题型刷(2-3周)
不要漫无目的地刷,要按题型来。比如这周专门刷链表题,下周专门刷二叉树。这样做的目的是快速建立题感,让你看到题目就能判断出大概用什么方法。
建议先刷每个题型里最经典的10-15道题,把套路吃透。
第二阶段:按频率刷(2-3周)
刷完基础题之后,开始刷高频题。很多平台都有”面试高频题”榜单,直接跟着刷就行。这个阶段要注意:
- 每道题都要自己先想5-10分钟,实在想不出来再看答案
- 看完答案后自己要重新写一遍,不能只看不动手
- 把不会的题标记出来,周末集中复习
第三阶段:模拟面试(持续)
刷到一定数量后,开始做模拟面试。可以在网上找小伙伴互相出题,或者用一些模拟面试的平台。模拟面试的目的是:
- 训练在压力下解题的能力
- 练习口头表达解题思路
- 发现自己的知识盲区
复盘比刷题更重要
我见过太多人刷题不复盘,今天刷了20道题,第二天全忘了。复盘的方法很简单:
- 建立一个错题本,记录做错的题
- 每周抽出时间复习错题
- 对于反复出错的题型,要回到基础概念重新理解
面试实战:从做题到拿offer
算法题刷得好,不代表面试一定能过。我总结了几个面试实战的技巧:
表达清楚解题思路
面试官最想看到的不是你的代码,而是你的思考过程。拿到题目后,先不要急着写代码,先口头描述你的思路:
“这道题我想到可以用动态规划来解,因为问题有最优子结构……状态转移方程是……base case是……”
这样既能给面试官留下好印象,也能让自己理清思路。
先讲暴力解法,再优化
很多面试官喜欢追问”有没有更优的解法”。所以建议你从暴力解法开始,然后一步步优化:
“暴力解法是用双重循环,时间复杂度是O(n²)。但我们可以用哈希表把查找的时间降到O(1),这样总体复杂度就变成O(n)了……”
这种”递进式”的解题方式,能展现你的思维深度。
手写代码要注意规范
面试手写代码时,注意以下几点:
- 变量命名要有意义,不要用a、b、c
- 重要的逻辑加注释
- 边界条件要处理(空输入、单元素等)
- 写完后主动检查一下有没有bug
遇到不会的题别慌
面试中遇到不会的题很正常,千万不要直接说”我不会”就放弃了。你可以说:
“这道题我暂时没想到最优解法,但我可以试试用XX方法暴力解一下,然后再优化……”
这样即使最终没做出来,也能展现你的思考过程。
资源推荐:去哪里刷题最有效
市面上的刷题资源很多,我推荐几个比较靠谱的:
- LeetCode:最主流的平台,题量大,有中文社区
- 牛客网:国内开发者用得多,有真题库
- 力扣(中文版):和LeetCode差不多,但界面更友好
- Codeforces:适合进阶,题目难度较高
对于新手,我建议从LeetCode的”面试必刷100题”开始,这个列表是根据历年面试真题统计出来的,含金量很高。
最后说几句
刷题这条路没有捷径,但也不是靠蛮力就能成功的。关键是要有方法、有规划、有复盘。我见过太多人每天刷10道题,刷了三个月,最后面试还是挂了。问题不在努力程度,而在效率。
建议你制定一个计划,比如三个月内刷完200道高频题,每周复盘一次,模拟考试几次。坚持下来,offer自然会来。
祝你早日拿到心仪的offer!
