在算法面试与考题中,堆(Heap)、哈希表(Hash Table)和二叉树(Binary Tree) 堪称三大“万金油”数据结构。它们拥有独特的性质,能高效解决特定模式的问题。
堆的核心能力:高效维护动态数据集的最大值或最小值,时间复杂度为 O(log n) 的插入/删除,O(1) 的极值访问。
堆(Heap)结构示意图

import heapq
def top_k_largest(nums, k):
min_heap = []
for num in nums:
if len(min_heap) < k:
heapq.heappush(min_heap, num)
else:
if num > min_heap[0]: # 比最小堆的堆顶大
heapq.heappop(min_heap)
heapq.heappush(min_heap, num)
return min_heap # 此时堆中是最大的K个元素,堆顶是第K大堆在Top K问题中的应用

left_heap) 存放较小的一半数,一个最小堆 (right_heap) 存放较大的一半数。len(left_heap) == len(right_heap) 或 len(left_heap) == len(right_heap) + 1。( -left_heap[0] + right_heap[0]) / 2.0 (最大堆堆顶取负)-left_heap[0]num 插入 left_heap (需取负用 heapq.heappush 模拟最大堆)。left_heap 的堆顶(实际最大值)弹出并插入 right_heap。len(left_heap) < len(right_heap),将 right_heap 堆顶(最小值)弹出并插入 left_heap。
双堆求中位数原理

import heapq
class MedianFinder:
def __init__(self):
self.left = [] # 最大堆 (用负数模拟)
self.right = [] # 最小堆
def addNum(self, num: int) -> None:
heapq.heappush(self.left, -num) # 先放入左堆
max_left = -heapq.heappop(self.left) # 取出左堆最大值
heapq.heappush(self.right, max_left) # 放入右堆
if len(self.right) > len(self.left): # 保证左堆元素数 >= 右堆
min_right = heapq.heappop(self.right)
heapq.heappush(self.left, -min_right)
def findMedian(self) -> float:
if len(self.left) > len(self.right):
return -self.left[0]
return (-self.left[0] + self.right[0]) / 2.0(distance, node),每次从堆中取出当前距离最小的节点进行松弛操作。这是堆在图论中的核心应用。(f_score = g_score + h_score, node),按 f_score 出队。哈希表的核心能力:基于键(Key)的 O(1) 平均时间复杂度的插入、删除和查找。用于快速存取和记录状态。
dict (Python) / HashMap (Java) / unordered_map (C++)。freq_map[num] = freq_map.get(num, 0) + 1index_map[element] = index 或 set 存储元素。dict 记录 {target - num: index})set)prefix (prefix[i] = nums[0] + nums[1] + ... + nums[i-1])。prefix[j] - prefix[i] = K => prefix[j] = prefix[i] + K。遍历时,用哈希表记录 {当前前缀和值: 出现次数/最早出现索引}。对于当前 prefix[j],查找 prefix[j] - K 是否在哈希表中。set 或 dict 记录字符最新位置)dict 记录目标字符需求)dict 计数)visited)。set 或 dict 存储已访问的节点(状态)。状态的表示是关键(坐标、序列化字符串、位图等)。visited set 记录已访问单词)。visited set 记录已访问密码)。set 存储所有数字,快速查找 num+1)。defaultdict(list) 或 defaultdict(set) 是最常用的分组工具。nums1 中元素频率,遍历 nums2 匹配)。二叉树是递归定义的天然载体,其遍历(前序、中序、后序、层序)是解决相关问题的基础框架。
def dfs(root: TreeNode):
if not root: return # 递归终止条件
# 前序遍历位置 (操作 root.val)
dfs(root.left) # 递归左子树
# 中序遍历位置 (操作 root.val)
dfs(root.right) # 递归右子树
# 后序遍历位置 (操作 root.val)def preorderTraversal(root: TreeNode) -> List[int]:
if not root: return []
stack, res = [root], []
while stack:
node = stack.pop()
res.append(node.val) # 访问根
if node.right: stack.append(node.right) # 右先入后出
if node.left: stack.append(node.left) # 左后入先出
return resfrom collections import deque
def levelOrder(root: TreeNode) -> List[List[int]]:
if not root: return []
queue = deque([root])
result = []
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size): # 处理当前层所有节点
node = queue.popleft()
current_level.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
result.append(current_level) # 添加当前层结果
return result左子树 < 根 < 右子树。def searchBST(root: TreeNode, val: int) -> TreeNode:
if not root or root.val == val:
return root
if val < root.val:
return searchBST(root.left, val)
else:
return searchBST(root.right, val)二叉树遍历方式对比

dfs(node) 返回以 node 为根的子树相关的状态信息。dfs(node) 返回该节点能向上贡献的最大路径和,同时更新全局最大路径和)。dfs(node) 返回 [偷node的最大收益, 不偷node的最大收益] )。dfs(node) 返回以 node 为根的树的最大深度,同时更新经过 node 的最长路径即 L深度 + R深度 )。def dfs(node):
if not node: return base_case # 空节点基本情况
left_info = dfs(node.left) # 获取左子树信息
right_info = dfs(node.right) # 获取右子树信息
# 利用 left_info 和 right_info 计算以 node 为根的树的信息
current_info = ...
# 可能更新全局结果 (如最大路径和、最长直径)
return current_infop 和 q 的最近公共祖先。root 为空,或 root 等于 p 或 q,直接返回 root。left = LCA(root.left, p, q)right = LCA(root.right, p, q)left 和 right 都非空:说明 p 和 q 分别在 root 的两侧,root 就是 LCA。left 非空,right 为空:说明 LCA 在左子树中,返回 left。right 非空,left 为空:说明 LCA 在右子树中,返回 right。None。def lowestCommonAncestor(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left and right: # p 和 q 分列 root 两侧
return root
return left if left else right # 返回非空的那个,或者 Nonecur = root。cur 不为空: cur 没有左孩子: cur。cur = cur.right。cur 有左孩子: cur 左子树上的最右节点 pre(即 cur 的中序遍历前驱节点)。 pre.right 为空: pre.right 指向 cur (建立线索)。cur = cur.left。pre.right 指向 cur (说明已建立线索且左子树已访问完): pre.right = None。cur。cur = cur.right。许多复杂问题需要结合多种数据结构协同解决。
key_to_val_freq: {key: (value, freq)},O(1) 获取值及其频率。freq_to_keys: {freq: OrderedDict/LinkedHashSet},记录每个频率对应的键集合,该集合能快速删除最久未使用的键(链表特性)。通常用 defaultdict(OrderedDict) 或自定义双向链表实现 LRU。min_freq: 快速定位当前最低频率。get, put) 思路: get(key): key_to_val_freq 获取 value 和旧频率 freq。不存在返回 -1。freq_to_keys[freq] 中删除 key。如果删除后该频率链表为空且 freq == min_freq,则 min_freq++。key 的频率为 freq + 1。key 添加到 freq_to_keys[freq + 1] 链表的最新位置(尾部)。key_to_val_freq[key] = (value, freq+1)。value。put(key, value): key 已存在,更新 value,并执行一次 get(key) 更新频率和位置。key 不存在: freq_to_keys[min_freq] 中删除最久未使用的键(链表头部)。key_to_val_freq 中删除该键。key: min_freq = 1 (新键频率为 1)。key 添加到 freq_to_keys[1] 的尾部。key_to_val_freq[key] = (value, 1)。timestamp: 为每条推文分配唯一递增 ID(或直接存储时间戳)。user_map: {userId: User}。User 类包含: user_idfollowed (set): 关注的人 ID 集合。tweets (LinkedList / list): 该用户发布的推文链表(节点存储 tweet_id, timestamp)。(timestamp, tweet_id, user_id, tweet_index)。tweet_index 指向该用户下一条要合并的推文索引。postTweet(userId, tweetId):创建推文对象(tweetId, 当前timestamp++),添加到相应用户的 tweets 链表头部。getNewsFeed(userId): tweets 链表头节点(最新推文)。timestamp 排序)。tweet_index + 1),将其加入堆。follow(followerId, followeeId):followerId 的 followed 集合加入 followeeId。unfollow(followerId, followeeId):followerId 的 followed 集合移除 followeeId (如果存在)。visited)。