跳到主要内容
预计阅读 23 分钟

从刷题到通关 —— 算法面试实战指南

学了那么多数据结构和算法,面试的时候到底怎么用?刷题到底该怎么刷?这一章不讲新知识,只讲一件事:如何把前面学的所有东西变成你的实战能力。

📋 开篇自测:你准备好了吗?

  1. 面试官给你一道从未见过的算法题,你的前3分钟会做什么?
  2. 你能在2分钟内说出数组、链表、栈、队列、哈希表、树、堆、图各自最适合解决什么类型的问题吗?
  3. 当你写完代码后,面试官问”还能优化吗?“——你知道从哪些角度去思考吗?

一、算法面试的本质——考的不是背题

很多人以为算法面试就是”背题”——把LeetCode的题刷几百道,面试时遇到原题就稳了。这个思路有一个致命问题:题目的变体是无穷的,你不可能背完。

算法面试真正考察的是三个能力:

  1. 问题分析能力: 把模糊的问题转化为精确的数据结构和算法模型
  2. 编码实现能力: 把思路正确地翻译成可运行的代码
  3. 沟通优化能力: 清晰地表达你的思路,并在面试官的引导下改进方案

掌握这三个能力,即使遇到没见过的题,你也能从容应对。


二、数据结构选择速查表

面试中最关键的第一步是:选对数据结构。 数据结构选对了,算法往往就自然浮现了。

问题特征优先考虑的数据结构典型例题
需要O(1)查找/判重哈希表(dict/set)两数之和、判断重复元素
有序数据中查找二分查找搜索插入位置、旋转数组查找
需要维护顺序+快速增删链表合并有序链表、反转链表
”最近”相关的操作括号匹配、每日温度、最小栈
”先来先到”的处理顺序队列BFS、任务调度
需要频繁取最大/最小值堆(优先队列)前K大元素、合并K个有序链表
层级关系、递归结构路径总和、最近公共祖先
多对多关系、连通性课程表(拓扑排序)、岛屿数量
求最优解 + 有重叠子问题动态规划背包问题、最长子序列
求所有方案/排列组合回溯全排列、N皇后、子集

三、面试解题四步法

拿到一道算法题,不要急着写代码。按这四步走:

第一步:理解问题(1-2分钟)

  • 用自己的话复述一遍题意,确保理解正确
  • 确认边界条件:空输入怎么办?只有一个元素呢?有负数吗?
  • 和面试官确认输入输出格式
# 例题:给定整数数组nums和目标值target,找出和为target的两个数的下标。
# 先问清楚:
# - 同一个元素能用两次吗?(不能)
# - 一定有解吗?(假设一定有)
# - 有多组解时返回哪一组?(任意一组)

第二步:想思路(3-5分钟)

  • 先想暴力解法——它永远是你的保底方案
  • 分析暴力解法的瓶颈在哪里,能不能用更好的数据结构消除
  • 说出你的时间复杂度和空间复杂度
# 两数之和——思路演进

# 暴力法:两层循环,O(n^2)
def two_sum_brute(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]

# 思考:内层循环在做什么?在找 target - nums[i] 是否存在。
# 这不就是一个"查找"操作吗?用哈希表可以把查找从O(n)降到O(1)!

# 哈希表法:一次遍历,O(n)
def two_sum_hash(nums, target):
    seen = {}  # 值 -> 下标
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i

print(two_sum_hash([2, 7, 11, 15], 9))  # [0, 1]

第三步:写代码(10-15分钟)

  • 先写出代码骨架(函数签名、主要逻辑框架)
  • 再填充细节(边界判断、索引处理)
  • 变量命名要有意义(别用a, b, c,用left, right, count

第四步:验证和优化(3-5分钟)

  • 用一两个简单例子手动走一遍代码逻辑
  • 检查边界情况(空数组、单元素、全相同元素等)
  • 如果面试官问”还能优化吗?“,从时间和空间两个角度思考

四、高频题型与解题模板

题型1:滑动窗口

适用场景: 连续子数组/子串问题,要求满足某种条件的最长或最短窗口。

def sliding_window_template(s):
    """滑动窗口模板(伪代码,需根据具体问题替换收缩条件)"""
    left = 0
    window = {}  # 窗口内的状态
    result = 0

    for right in range(len(s)):
        # 1. 扩大窗口:加入右边元素
        char = s[right]
        window[char] = window.get(char, 0) + 1

        # 2. 收缩窗口:当窗口不满足条件时
        # 将下面的条件替换为具体问题的收缩判断,例如:
        #   while len(window) > k:    (最多k个不同字符)
        #   while window_sum > target: (子数组和超过目标)
        while should_shrink(window):  # 替换为具体条件
            left_char = s[left]
            window[left_char] -= 1
            left += 1

        # 3. 更新结果
        result = max(result, right - left + 1)

    return result

# 实战:无重复字符的最长子串
def length_of_longest_substring(s):
    """找出不含重复字符的最长子串的长度"""
    left = 0
    char_index = {}  # 字符 -> 最近出现的位置
    max_length = 0

    for right in range(len(s)):
        char = s[right]
        if char in char_index and char_index[char] >= left:
            left = char_index[char] + 1  # 跳过重复字符
        char_index[char] = right
        max_length = max(max_length, right - left + 1)

    return max_length

print(length_of_longest_substring("abcabcbb"))  # 3 ("abc")
print(length_of_longest_substring("bbbbb"))      # 1 ("b")
print(length_of_longest_substring("pwwkew"))     # 3 ("wke")

题型2:双指针

适用场景: 有序数组/链表上的搜索和匹配问题。

# 实战:盛最多水的容器
def max_area(height):
    """双指针从两端向中间逼近"""
    left, right = 0, len(height) - 1
    max_water = 0

    while left < right:
        # 计算当前面积
        width = right - left
        h = min(height[left], height[right])
        max_water = max(max_water, width * h)

        # 移动较矮的那一端(因为移动较高的一端不可能让面积更大)
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return max_water

print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))  # 49

题型3:BFS/DFS

适用场景: 树和图的遍历、连通性问题、层级相关问题。

from collections import deque

# 实战:岛屿数量(网格中的连通分量)
def num_islands(grid):
    """计算网格中'1'组成的连通岛屿数量"""
    if not grid:
        return 0

    rows, cols = len(grid), len(grid[0])
    count = 0

    def bfs(r, c):
        queue = deque([(r, c)])
        grid[r][c] = '0'  # 标记为已访问
        while queue:
            row, col = queue.popleft()
            for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                nr, nc = row + dr, col + dc
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                    grid[nr][nc] = '0'
                    queue.append((nr, nc))

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                bfs(r, c)
                count += 1

    return count

grid = [
    ['1', '1', '0', '0', '0'],
    ['1', '1', '0', '0', '0'],
    ['0', '0', '1', '0', '0'],
    ['0', '0', '0', '1', '1']
]
print(num_islands(grid))  # 3

题型4:二分查找的变体

适用场景: 在有序或半有序数据中查找目标值,或查找满足条件的边界。

# 实战:在旋转排序数组中搜索
def search_rotated(nums, target):
    """旋转数组中的二分查找"""
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid

        # 判断哪一半是有序的
        if nums[left] <= nums[mid]:
            # 左半边有序
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
        else:
            # 右半边有序
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1

    return -1

print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0))  # 4
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 3))  # -1

五、复杂度速查表

面试中经常需要快速说出各种操作的复杂度。这张表值得记住:

数据结构操作复杂度

数据结构查找插入删除空间
数组O(n) / O(1)按索引O(n)O(n)O(n)
链表O(n)O(1)已知位置O(1)已知位置O(n)
哈希表O(1)平均O(1)平均O(1)平均O(n)
二叉搜索树O(log n)平均 / O(n)最坏O(log n)平均 / O(n)最坏O(log n)平均 / O(n)最坏O(n)
AVL树/红黑树O(log n)O(log n)O(log n)O(n)
O(n) / O(1)取极值O(log n)O(log n)O(n)
栈/队列O(n)O(1)O(1)O(n)

排序算法复杂度

算法最优平均最差空间稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定
快速排序O(n log n)O(n log n)O(n^2)O(log n)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定

图与字符串算法复杂度

算法时间复杂度空间复杂度说明
BFS/DFSO(V + E)O(V)V为顶点数,E为边数
Dijkstra(堆优化)O((V + E) log V)O(V)不支持负权边
KruskalO(E log E)O(V)需要并查集
KMPO(n + m)O(m)n为文本长度,m为模式长度
BM(完整版)O(n/m) 平均,O(n*m) 最坏O(m)实际中通常最快;加入Galil规则后最坏可优化为O(n+m)

六、刷题策略——质量远比数量重要

按专题刷,不要随机刷

随机刷题就像把10种药混在一起吃——效果不好还容易晕。正确的做法是按专题集中突破:

  1. 第一周: 数组 + 哈希表(两数之和、三数之和、移动零、有效的字母异位词)
  2. 第二周: 链表 + 栈 + 队列(反转链表、合并链表、有效括号、每日温度)
  3. 第三周: 二叉树 + BFS/DFS(最大深度、翻转二叉树、层序遍历、验证BST)
  4. 第四周: 二分查找 + 排序(搜索插入位置、旋转数组、前K大元素)
  5. 第五周: 动态规划(爬楼梯、最长子序列、背包问题、编辑距离)
  6. 第六周: 回溯 + 贪心(全排列、子集、组合总和、活动选择)

每道题至少做三遍

  • 第一遍: 花30分钟尝试,实在想不出就看题解。重点是理解思路
  • 第二遍: 隔一天,不看任何提示,独立写出完整代码。重点是记住模式
  • 第三遍: 隔一周,再写一次。重点是检验内化程度

建立自己的题型笔记

每做完一类题,用自己的话总结:

题型:滑动窗口
核心思路:维护一个窗口[left, right],right不断右移扩大窗口,
         当窗口不满足条件时left右移收缩窗口。
关键细节:窗口内用哈希表维护状态,注意更新结果的时机。
典型题目:无重复最长子串、最小覆盖子串、字母异位词。
我曾经犯的错:忘记在收缩窗口时更新哈希表的计数。

七、面试中的沟通技巧

算法面试不是闭卷考试。你要像和同事讨论技术方案一样,和面试官交流。

技巧1:先说暴力解法

即使你一眼看出了最优解,也建议先说暴力解法。这展示了你的分析过程,也给自己留了退路。

“最直接的方法是两层循环暴力搜索,时间复杂度O(n^2)。但如果我们用哈希表来存已经遍历过的元素,就可以把内层循环的查找降到O(1),整体变成O(n)。”

技巧2:写代码前先说清楚思路

面试官更看重思路而非代码。在写代码之前,用1-2句话概括你的方案。

技巧3:卡住时主动沟通

卡住了不丢人,闷头想5分钟才丢人。你可以说:

“我现在的思路是…,但在处理…这个部分遇到了困难。我在想是不是可以用…来解决?”

面试官通常会给出有用的提示。

技巧4:分析完复杂度后主动提优化

写完代码后,主动说出时间和空间复杂度,再想想有没有优化空间。

“当前方案时间O(n log n),空间O(n)。如果要优化空间到O(1)的话,可以考虑原地操作…”


📝 掌握度自测

  1. 分析题: 给你一个无序数组,要求找出第K大的元素。你能想出几种方法?每种方法的时间复杂度是多少?哪种最优?
  2. 编码题: 实现一个LRU缓存——支持O(1)的get和put操作,容量满时淘汰最久没使用的元素。(提示:哈希表 + 双向链表)
  3. 设计题: 如果面试官让你设计一个自动补全系统(像搜索引擎的搜索框),你会用什么数据结构?如何处理按热度排序?
  4. 综合题: 拿到这道题:“给定一个二维网格和一个单词,判断单词是否存在于网格中(相邻字符可以上下左右连接,同一格不能重复使用)“。按四步法,写出你的完整解题过程。
  5. 反思题: 回顾本课程前10章,选出你最薄弱的3个知识点,为每个知识点制定一个具体的强化计划。

💡 自我评估

  • 全部答对:你已经具备了扎实的面试实战能力,继续保持这个手感。
  • 答对3-4题:基础扎实,建议重点练习编码速度和沟通表达。
  • 答对1-2题:回到前面的章节,把每个数据结构的增删查改操作再实现一遍,然后回来做这些题。
  • 全部答错:不要气馁。算法能力需要时间积累。建议从”刷题策略”中的第一周开始,每天只做1-2题,坚持6周。

面试技巧讲完了,但我们的数据结构之旅还没有结束。前面的章节中,有两个重要的数据结构被刻意留到了后面——因为它们的理解需要以前面的知识为基础。下一章,我们来认识”堆与优先队列”——一种能在 O(log n) 时间内找到最大值或最小值的数据结构,它在 Top-K 问题、任务调度、合并有序序列等场景中大显身手。

购买课程解锁全部内容

刷题到通关:数据结构与算法面试实战

¥29.90