1. 二叉树算法精要解析
作为一名长期奋战在算法竞赛一线的开发者,我深知二叉树相关题目在技术面试中的高频出现率。今天我将系统梳理LeetCode中经典的二叉树问题,分享从思考到AC的全过程经验。这些题目覆盖了前序/中序/后序遍历、路径计算、树形转换等核心考点,是每位开发者必须掌握的硬核技能。
1.1 中序遍历的递归与迭代实现
中序遍历(左-根-右)是二叉树最基础的操作之一。递归解法直观易懂:
python复制def inorderTraversal(root):
res = []
def helper(node):
if not node:
return
helper(node.left) # 先访问左子树
res.append(node.val) # 再访问根节点
helper(node.right) # 最后访问右子树
helper(root)
return res
但在实际工程中,递归可能存在栈溢出风险。更稳健的迭代解法使用显式栈:
python复制def inorderTraversal(root):
res = []
stack = []
curr = root
while curr or stack:
while curr: # 将左子树全部入栈
stack.append(curr)
curr = curr.left
curr = stack.pop() # 弹出最深层左节点
res.append(curr.val)
curr = curr.right # 转向右子树
return res
关键技巧:迭代法中内层while循环会持续向左深入,直到叶子节点。每次弹出栈顶时,意味着该节点的左子树已处理完毕。
1.2 翻转二叉树的三种视角
翻转二叉树(镜像操作)看似简单,却蕴含着对递归的深刻理解。以下是三种等效实现:
- 后序遍历风格:
python复制def invertTree(root):
if not root:
return None
left = invertTree(root.left) # 先翻转左子树
right = invertTree(root.right) # 再翻转右子树
root.left, root.right = right, left # 最后交换指针
return root
- 前序遍历风格:
python复制def invertTree(root):
if not root:
return None
root.left, root.right = root.right, root.left # 先交换指针
invertTree(root.left) # 再处理左子树
invertTree(root.right) # 最后处理右子树
return root
- 层序遍历风格(BFS):
python复制from collections import deque
def invertTree(root):
if not root:
return None
queue = deque([root])
while queue:
node = queue.popleft()
node.left, node.right = node.right, node.left
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
经验之谈:面试时建议展示多种解法,并说明各自适用场景。递归简洁但可能栈溢出,BFS适合大规模树结构。
2. 二叉树路径问题实战
2.1 二叉树直径的计算奥秘
直径定义为任意两节点间最长路径长度。关键突破点在于:
- 直径必然经过某个节点的左右子树
- 对每个节点,计算左右子树深度之和
python复制def diameterOfBinaryTree(root):
self.max_diameter = 0
def depth(node):
if not node:
return 0
left = depth(node.left)
right = depth(node.right)
self.max_diameter = max(self.max_diameter, left + right)
return max(left, right) + 1
depth(root)
return self.max_diameter
易错点:直径是边数而非节点数。当树为空时,直径应为0而非-1。
2.2 路径总和Ⅲ的两种高效解法
题目要求找出路径和等于目标值的路径数量(路径不必从根开始)。推荐两种解法:
- 前缀和+回溯(O(n)时间复杂度):
python复制from collections import defaultdict
def pathSum(root, target):
prefix = defaultdict(int)
prefix[0] = 1 # 空路径和为0
self.count = 0
def dfs(node, curr_sum):
if not node:
return
curr_sum += node.val
self.count += prefix[curr_sum - target]
prefix[curr_sum] += 1
dfs(node.left, curr_sum)
dfs(node.right, curr_sum)
prefix[curr_sum] -= 1 # 回溯
dfs(root, 0)
return self.count
- 双重递归(直观但O(n^2)):
python复制def pathSum(root, target):
if not root:
return 0
def dfs(node, remaining):
if not node:
return 0
return (1 if remaining == node.val else 0) + \
dfs(node.left, remaining - node.val) + \
dfs(node.right, remaining - node.val)
return dfs(root, target) + \
pathSum(root.left, target) + \
pathSum(root.right, target)
性能对比:当树平衡时,解法1明显优于解法2。但在最坏情况下(单链树),两者时间复杂度相同。
3. 二叉搜索树专项突破
3.1 有序数组构建高度平衡BST
将升序数组转换为高度平衡的BST,核心在于二分递归:
python复制def sortedArrayToBST(nums):
def helper(left, right):
if left > right:
return None
mid = (left + right) // 2
root = TreeNode(nums[mid])
root.left = helper(left, mid - 1)
root.right = helper(mid + 1, right)
return root
return helper(0, len(nums) - 1)
优化点:对于极大数组,可改用迭代法避免递归深度问题。使用栈存储待处理的(left, right, parent, is_left)元组。
3.2 二叉搜索树中第K小元素
利用BST的中序遍历有序特性:
python复制def kthSmallest(root, k):
stack = []
while True:
while root: # 深入左子树
stack.append(root)
root = root.left
root = stack.pop() # 访问当前最小
k -= 1
if k == 0:
return root.val
root = root.right # 转向右子树
进阶问题:如果频繁插入/删除节点,如何优化查询?
- 方案:为每个节点维护子树节点数,实现O(h)时间复杂度
- 实现:修改插入/删除逻辑,动态更新size计数器
4. 树形结构转换技巧
4.1 二叉树展开为链表
题目要求原地展开为右指针单向链表。关键步骤:
- 将左子树插入到右子树位置
- 将原右子树接到新右子树末端
python复制def flatten(root):
while root:
if root.left:
# 找到左子树的最右节点
pre = root.left
while pre.right:
pre = pre.right
# 重组指针关系
pre.right = root.right
root.right = root.left
root.left = None
root = root.right
可视化技巧:展开过程类似于拉直衣架。每次操作都将当前节点的左子树"塞入"右子树之前。
4.2 前序中序构造二叉树
经典的分治算法应用:
python复制def buildTree(preorder, inorder):
if not preorder:
return None
root_val = preorder[0]
root = TreeNode(root_val)
idx = inorder.index(root_val)
root.left = buildTree(preorder[1:idx+1], inorder[:idx])
root.right = buildTree(preorder[idx+1:], inorder[idx+1:])
return root
优化版本(使用哈希表加速查找):
python复制def buildTree(preorder, inorder):
inorder_map = {val: idx for idx, val in enumerate(inorder)}
def helper(pre_start, pre_end, in_start, in_end):
if pre_start > pre_end:
return None
root_val = preorder[pre_start]
root = TreeNode(root_val)
idx = inorder_map[root_val]
left_size = idx - in_start
root.left = helper(pre_start+1, pre_start+left_size, in_start, idx-1)
root.right = helper(pre_start+left_size+1, pre_end, idx+1, in_end)
return root
return helper(0, len(preorder)-1, 0, len(inorder)-1)
5. 树形DP典型问题
5.1 二叉树最大路径和
路径定义为任意节点序列,要求路径上节点值之和最大。解题框架:
python复制def maxPathSum(root):
self.max_sum = float('-inf')
def max_gain(node):
if not node:
return 0
left_gain = max(max_gain(node.left), 0) # 舍弃负增益
right_gain = max(max_gain(node.right), 0)
self.max_sum = max(self.max_sum, node.val + left_gain + right_gain)
return node.val + max(left_gain, right_gain)
max_gain(root)
return self.max_sum
易错警示:初始时max_sum应设为负无穷而非0,因为路径和可能为负值。
5.2 岛屿数量问题
虽然是网格问题,但本质是树的DFS/BFS遍历。典型解法:
python复制def numIslands(grid):
if not grid:
return 0
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
count += 1
self.dfs(grid, i, j)
return count
def dfs(grid, i, j):
if i<0 or j<0 or i>=len(grid) or j>=len(grid[0]) or grid[i][j]!='1':
return
grid[i][j] = '0' # 标记为已访问
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
性能优化:对于大规模网格,可改用迭代DFS或BFS,避免递归栈溢出。
6. 回溯算法在树问题中的应用
6.1 组合总和问题系列
组合总和I(可重复使用元素):
python复制def combinationSum(candidates, target):
res = []
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
continue
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i]) # 注意start保持i
path.pop()
backtrack(0, [], target)
return res
组合总和II(元素不可重复):
python复制def combinationSum2(candidates, target):
candidates.sort()
res = []
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
if i > start and candidates[i] == candidates[i-1]: # 去重关键
continue
if candidates[i] > remaining:
break
path.append(candidates[i])
backtrack(i+1, path, remaining - candidates[i]) # i+1确保不重复
path.pop()
backtrack(0, [], target)
return res
6.2 N皇后问题的位运算优化
传统解法使用二维数组记录攻击范围,这里展示更高效的位运算版:
python复制def solveNQueens(n):
def backtrack(row, cols, diag1, diag2, path):
if row == n:
res.append(['.'*i + 'Q' + '.'*(n-i-1) for i in path])
return
available = ((1 << n) - 1) & ~(cols | diag1 | diag2)
while available:
pos = available & -available # 获取最低位的1
col = bin(pos-1).count('1')
backtrack(row+1, cols | pos, (diag1 | pos) << 1, (diag2 | pos) >> 1, path + [col])
available &= available - 1 # 移除最低位的1
res = []
backtrack(0, 0, 0, 0, [])
return res
位运算技巧:cols/diag1/diag2的每一位表示对应列/对角线是否被占用。pos & -pos可以快速获取最低设置位。
7. 特殊数据结构实现
7.1 LRU缓存的双向链表实现
LRU缓存需要O(1)时间完成get和put操作。经典实现:
python复制class DLinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.size = 0
self.cache = {}
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key):
if key not in self.cache:
return -1
node = self.cache[key]
self._move_to_head(node)
return node.value
def put(self, key, value):
if key in self.cache:
node = self.cache[key]
node.value = value
self._move_to_head(node)
else:
node = DLinkedNode(key, value)
self.cache[key] = node
self._add_to_head(node)
self.size += 1
if self.size > self.capacity:
removed = self._remove_tail()
del self.cache[removed.key]
self.size -= 1
def _add_to_head(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _move_to_head(self, node):
self._remove_node(node)
self._add_to_head(node)
def _remove_tail(self):
node = self.tail.prev
self._remove_node(node)
return node
7.2 最小栈的同步辅助栈设计
要求所有操作O(1)时间复杂度:
python复制class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, x):
self.stack.append(x)
if not self.min_stack or x <= self.min_stack[-1]:
self.min_stack.append(x)
def pop(self):
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
return self.min_stack[-1]
设计要点:min_stack栈顶始终保存当前最小值。当新元素≤当前min时入栈,出栈时检查是否等于min_stack栈顶。
8. 字符串处理进阶技巧
8.1 字符串解码的嵌套处理
处理形如"3[a2[c]]"的编码字符串,需要处理嵌套结构:
python复制def decodeString(s):
stack = []
curr_str = ''
curr_num = 0
for char in s:
if char.isdigit():
curr_num = curr_num * 10 + int(char)
elif char == '[':
stack.append((curr_str, curr_num))
curr_str = ''
curr_num = 0
elif char == ']':
prev_str, num = stack.pop()
curr_str = prev_str + num * curr_str
else:
curr_str += char
return curr_str
8.2 单词搜索的DFS+回溯
在二维网格中搜索单词存在性:
python复制def exist(board, word):
def dfs(i, j, k):
if not (0 <= i < len(board)) or not (0 <= j < len(board[0])) or board[i][j] != word[k]:
return False
if k == len(word) - 1:
return True
tmp, board[i][j] = board[i][j], '/'
res = dfs(i+1, j, k+1) or dfs(i-1, j, k+1) or dfs(i, j+1, k+1) or dfs(i, j-1, k+1)
board[i][j] = tmp
return res
for i in range(len(board)):
for j in range(len(board[0])):
if dfs(i, j, 0):
return True
return False
剪枝优化:可以先统计board和word的字符频率,快速排除不可能情况。