1. 红黑树 (Red-Black Tree)
面试核心: 为什么 Linux 内核、Python 字典(某些版本)和 Java HashMap 选择红黑树?
- 红黑树 vs AVL 树:
- AVL 树:严格平衡(高度差 $\le 1$)。查询极快,但插入和删除时需要频繁旋转,开销大。
- 红黑树:非严格平衡。它通过牺牲一定的查询性能,换取了更低的增删成本(旋转次数少)。
- 结论: * 如果你的应用中 搜索次数远多于增删(如只读数据库),选 AVL。
- 如果 搜索、增删频率相当(如通用容器),红黑树 是最佳折中方案。
2. 经典递归与动态规划
2.1 台阶问题 (Fibonacci 变种)
一只青蛙一次可以跳 1 级或 2 级台阶,求跳上 $n$ 级台阶的方法数。
def fib(n):
# 迭代法:时间 O(n),空间 O(1)
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return b
2.2 变态台阶问题
一次可以跳 1, 2, ..., $n$ 级。
f = lambda n: 1 << (n - 1) # 位运算最快
3. 杨氏矩阵查找
面试核心: 如何在行列皆有序的矩阵中寻找目标值?
- 思路: 从右上角(或左下角)开始寻找。
- 若当前值 > 目标:左移。
- 若当前值 < 目标:下移。
- 复杂度: $O(m+n)$。
def find_in_matrix(matrix, target):
if not matrix: return False
row, col = 0, len(matrix[0]) - 1
while row < len(matrix) and col >= 0:
val = matrix[row][col]
if val == target: return True
elif val > target: col -= 1
else: row += 1
return False
4. 链表技巧
4.1 成对调换链表节点
1->2->3->4 $\Rightarrow$ 2->1->4->3
def swap_pairs(head):
if head and head.next:
next_node = head.next
head.next = swap_pairs(next_node.next)
next_node.next = head
return next_node
return head
4.2 交叉链表求交点
核心思路: 浪漫相遇法。
两个指针 $A$ 和 $B$ 同时出发,走完自己的路就去走对方的路,由于 $L_a + L_b = L_b + L_a$,它们一定会在交点相遇。
def get_intersection_node(headA, headB):
pA, pB = headA, headB
while pA != pB:
pA = pA.next if pA else headB
pB = pB.next if pB else headA
return pA
5. 列表去重(保持顺序)
在 Python 中,如何优雅地去重并保留原序?
# Python 3.7+ 字典是有序的,利用这一特性最简洁:
items = ['b','c','d','b','c','a','a']
unique_items = list(dict.fromkeys(items))
6. 链表成对调换 (Swap Nodes in Pairs)
面试核心: 考察递归思维与链表指针的操作细节。
任务:将 1->2->3->4 转换为 2->1->4->3。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
# 递归终止条件:节点为空或只有一个节点
if head is None or head.next is None:
return head
# 记录第二个节点
next_node = head.next
# 递归处理后续节点,并将其挂在当前 head 的后面
head.next = self.swapPairs(next_node.next)
# 将 next_node 指向当前的 head,完成反转
next_node.next = head
return next_node
7. Python 创建字典的多种姿势
面试核心: 了解 Python 语法的灵活性及底层工厂方法。
- 直接创建 (Literal):最常用且性能最好。
d = {'name': 'earth', 'port': '80'}
- 工厂方法 (dict):支持元组列表或关键字参数。
d = dict([('name', 'earth'), ('port', '80')])
d = dict(name='earth', port='80')
- fromkeys 方法:用于初始化具有统一默认值的字典。
d = {}.fromkeys(('x', 'y'), -1) # {'x': -1, 'y': -1}
8. 合并两个有序列表
面试核心: 归并排序的核心步骤,考查边界处理能力。
循环解法 (推荐:空间复杂度更优)
def merge_sorted_list(a, b):
result = []
while a and b:
# 每次弹出更小的那个元素
if a[0] <= b[0]:
result.append(a.pop(0))
else:
result.append(b.pop(0))
# 将剩余部分直接合并
result.extend(a or b)
return result
9. 交叉链表求交点
面试核心: 空间复杂度 $O(1)$ 的解法。
思路:如果两个链表相交,那么从交点到尾部的一段是公共的。
def get_intersection_node(l1, l2):
# 计算两个链表的长度差,让长的先走
p1, p2 = l1, l2
len1 = len2 = 0
while p1: len1 += 1; p1 = p1.next
while p2: len2 += 1; p2 = p2.next
# 指针复位
p1, p2 = l1, l2
# 消除长度差
if len1 > len2:
for _ in range(len1 - len2): p1 = p1.next
else:
for _ in range(len2 - len1): p2 = p2.next
# 同步走,相遇点即交点
while p1 != p2:
p1 = p1.next
p2 = p2.next
return p1
10. 二分查找 (Binary Search)
面试核心: 注意 mid 计算防止溢出的细节,以及边界条件的控制。
def binary_search(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
# 避免 (high + low) // 2 在大数时溢出
mid = low + (high - low) // 2
guess = arr[mid]
if guess == target:
return mid
if guess > target:
high = mid - 1
else:
low = mid + 1
return None
# 测试
my_list = [1, 3, 5, 7, 9]
print(f"Index of 3: {binary_search(my_list, 3)}") # Output: 1
11. 快速排序 (Quick Sort)
面试核心: 分治思想(Divide and Conquer)。平均时间复杂度 $O(n \log n)$,空间复杂度取决于实现方式。
def quicksort(arr):
"""
Python 风格的快排实现(利用列表推导式)
"""
if len(arr) < 2:
return arr
# 选取基准值(Pivot)
pivot = arr[0]
# 分区(Partition)
less = [i for i in arr[1:] if i <= pivot]
greater = [i for i in arr[1:] if i > pivot]
# 递归调用
return quicksort(less) + [pivot] + quicksort(greater)
# 测试
print(quicksort([2, 4, 6, 7, 1, 2, 5]))
12. 找零问题 (Coin Change)
面试核心: 典型的动态规划(DP)问题。通过构建最优子结构,避免重复计算。
def coin_change(values, money):
"""
values: 硬币面额列表,如 [25, 21, 10, 5, 1]
money: 需要找零的总金额
返回: 达到该金额所需的最少硬币数
"""
# 初始化 DP 数组,长度为 money + 1,初始值为无穷大
# dp[i] 表示凑齐金额 i 所需的最少硬币数
dp = [float('inf')] * (money + 1)
dp[0] = 0
for cents in range(1, money + 1):
for coin in values:
if coin <= cents:
# 状态转移方程:取当前金额减去该硬币面值后的最少硬币数 + 1
dp[cents] = min(dp[cents], dp[cents - coin] + 1)
return dp[money] if dp[money] != float('inf') else -1
# 示例:找零 63 元
# print(coin_change([25, 21, 10, 5, 1], 63))
13. 二叉树的构建与遍历
面试核心: 考察对递归结构(Node)和队列(Queue)在广度优先搜索(BFS)中应用的掌握。
14. 二叉树节点定义
class Node:
def __init__(self, data, left=None, right=None):
self.data = data
self.left = left
self.right = right
# 构建示例树
# 1
# / \
# 3 2
# / \ / \
# 7 6 5 4
# /
# 0
tree = Node(1,
Node(3, Node(7, Node(0)), Node(6)),
Node(2, Node(5), Node(4)))
15. 层次遍历 (BFS / Level Order Traversal)
面试核心: 使用列表推导式或双端队列 collections.deque 实现高效遍历。
def level_order_lookup(root):
if not root:
return
# 使用当前层节点列表
current_row = [root]
while current_row:
# 打印当前层的所有数据
print([node.data for node in current_row])
# 获取下一层的所有非空子节点
next_row = []
for node in current_row:
if node.left: next_row.append(node.left)
if node.right: next_row.append(node.right)
current_row = next_row
# 调用
# level_order_lookup(tree)
进阶技巧: 面试中常要求按层返回嵌套列表(如 [[1], [3, 2], [7, 6, 5, 4], [0]]),上述逻辑只需稍作修改即可实现。
16. 深度优先遍历 (DFS) 与 17. 三种遍历顺序
面试核心: 深度优先遍历(DFS)本质上是利用了栈(递归即系统栈)的特性。改变访问根节点的时机,就得到了前、中、后序遍历。
核心代码实现
class Node:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# 1. 前序遍历 (根 -> 左 -> 右)
def pre_order(root):
if not root: return
print(root.value, end=' ')
pre_order(root.left)
pre_order(root.right)
# 2. 中序遍历 (左 -> 根 -> 右)
def mid_order(root):
if not root: return
mid_order(root.left)
print(root.value, end=' ')
mid_order(root.right)
# 3. 后序遍历 (左 -> 右 -> 根)
def post_order(root):
if not root: return
post_order(root.left)
post_order(root.right)
print(root.value, end=' ')
18. 求最大树深 (Max Depth)
面试核心: 分治法。整棵树的高度 = max(左子树高度, 右子树高度) + 1。
def get_max_depth(root):
if not root:
return 0
return max(get_max_depth(root.left), get_max_depth(root.right)) + 1
19. 判断两棵树是否相同 (Same Tree)
面试核心: 递归比较。两棵树相同的条件是:当前节点值相等,且左右子树也分别相同。
def is_same_tree(p, q):
# 均为 None 则相同
if not p and not q:
return True
# 一个为空或值不等则不同
if not p or not q or p.value != q.value:
return False
# 递归比较子树
return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right)
20. 重建二叉树:根据前序与中序求后序
面试核心: 1. 前序遍历的第一个元素永远是当前的根节点。
2. 在中序遍历中找到该根节点,其左侧即为左子树,右侧为右子树。
def rebuild_tree(pre_order_list, in_order_list):
if not pre_order_list or not in_order_list:
return None
# 1. 确定根节点
root_val = pre_order_list[0]
root = Node(root_val)
# 2. 在中序序列中定位根节点,以此切分左右子树
pivot_index = in_order_list.index(root_val)
# 3. 递归构建
# 左子树的前序:pre_order_list[1 : pivot_index + 1]
# 左子树的中序:in_order_list[: pivot_index]
root.left = rebuild_tree(pre_order_list[1:pivot_index+1], in_order_list[:pivot_index])
# 右子树的前序:pre_order_list[pivot_index + 1 :]
# 右子树的中序:in_order_list[pivot_index + 1 :]
root.right = rebuild_tree(pre_order_list[pivot_index+1:], in_order_list[pivot_index+1:])
return root
# 重建后通过后序遍历验证
def verify_post_order(root):
if not root: return
verify_post_order(root.left)
verify_post_order(root.right)
print(root.value, end=' ')
21. 单链表逆置 (Reverse Linked List)
面试核心: 考察对指针(引用)变化的精确控制。
任务:将 1->2->3->...->9 转换为 9->8->...->1。
class Node:
def __init__(self, data=None, next=None):
self.data = data
self.next = next
def reverse_linked_list(head):
"""
通过三个指针迭代实现逆置:pre, cur, tmp
"""
if not head or not head.next:
return head
pre = None
cur = head
while cur:
tmp = cur.next # 1. 暂存下一个节点
cur.next = pre # 2. 核心:将当前指针反转指向前一个
pre = cur # 3. pre 前移
cur = tmp # 4. cur 前移
return pre
# 测试代码
link = Node(1, Node(2, Node(3, Node(4, Node(5)))))
root = reverse_linked_list(link)
while root:
print(root.data, end=" -> ")
root = root.next
22. 变位词判断 (Anagram Detection)
面试核心: 同一个算法的不同复杂度实现,体现了从“暴力破解”到“空间换时间”的进阶思路。
方法 1:逐位检查 (暴力法)
- 原理:遍历第一个字符串,在第二个字符串中寻找并标记。
- 复杂度:$O(n^2)$。
方法 2:排序对比
- 原理:将两个字符串转化为列表并排序,若排序后完全一致则是变位词。
- 复杂度:$O(n \log n)$。
方法 3:计数统计 (最优解)
- 原理:利用哈希思想(或长度为 26 的数组)记录字符出现频率。
- 复杂度:$O(n)$。
class Anagram:
@staticmethod
def is_anagram_v2(s1, s2):
"""排序法:逻辑最简洁"""
return sorted(s1) == sorted(s2)
@staticmethod
def is_anagram_v3(s1, s2):
"""计数法:性能最高"""
if len(s1) != len(s2): return False
# 记录字符频次
counts = [0] * 26
for char in s1:
counts[ord(char) - ord('a')] += 1
for char in s2:
counts[ord(char) - ord('a')] -= 1
# 检查是否全部归零
return all(c == 0 for c in counts)
# 测试
print(Anagram.is_anagram_v3('apple', 'pleap')) # True
23. 动态规划 (Dynamic Programming)
面试核心: 动态规划不仅仅是找零或斐波那契,它的核心在于:
- 寻找最优子结构。
- 定义状态转移方程。
- 存储中间结果(备忘录/DP表)。
为了提升“道满Python AI”面试教程的深度,引入 最长公共子序列 (LCS) 和 0-1 背包问题 是极佳的选择。这两个问题是动态规划(DP)的“教科书级”案例,能帮助学员深刻理解二维状态转移方程。
🏆 动态规划进阶:经典模型实战
24. 最长公共子序列 (Longest Common Subsequence, LCS)
面试核心: 子序列(Subsequence)不要求连续,只需保持相对顺序。常用于版本控制(如 git diff)和基因序列比对。
1. 状态转移方程
设 $dp[i][j]$ 为字符串 $s1$ 前 $i$ 个字符与 $s2$ 前 $j$ 个字符的最长公共子序列长度:
- 若 $s1[i-1] == s2[j-1]$:则 $dp[i][j] = dp[i-1][j-1] + 1$
- 若不相等:则 $dp[i][j] = \max(dp[i-1][j], dp[i][j-1])$
2. Python 代码实现
def lcs(s1, s2):
m, n = len(s1), len(s2)
# 构建 (m+1) x (n+1) 的二维 DP 表
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
# 测试: "abcde" 与 "ace" -> "ace", 长度为 3
print(f"LCS Length: {lcs('abcde', 'ace')}")
25. 0-1 背包问题 (0-1 Knapsack Problem)
面试核心: 核心在于“选”或“不选”。每个物品只有一件,不可拆分。
1. 状态转移方程
设 $dp[i][j]$ 为前 $i$ 件物品放入容量为 $j$ 的背包能获得的最大价值:
- 不选第 $i$ 件:$dp[i][j] = dp[i-1][j]$
- 选第 $i$ 件(前提是当前容量 $j \ge weight[i]$):$dp[i][j] = \max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])$
2. Python 优化版实现(一维滚动数组)
在面试中,能写出一维数组优化的版本会极大地加分,因为它将空间复杂度从 $O(NW)$ 降到了 $O(W)$。
def knapsack_01(weights, values, capacity):
"""
weights: 物品重量列表
values: 物品价值列表
capacity: 背包最大容量
"""
n = len(weights)
# 使用一维数组优化空间
dp = [0] * (capacity + 1)
for i in range(n):
# 必须逆序遍历,防止重复计算同一件物品(这是 0-1 背包的关键)
for j in range(capacity, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 测试: 物品重量 [1, 3, 4], 价值 [15, 20, 30], 背包容量 4
# 选第1件和第2件(1+3=4),总价值 15+20=35;选第3件价值30。最大价值为 35。
print(f"Max Value: {knapsack_01([1, 3, 4], [15, 20, 30], 4)}")