堆(Heap)优先队列的核心实现,手动实现大顶堆和小顶堆详解,面试要点
堆(Heap)是优先队列的高效实现,分为大顶堆(父节点值≥子节点)和小顶堆(父节点值≤子节点)。核心操作包括插入元素时的上浮调整和删除堆顶时的下沉调整,时间复杂度均为O(log n)。手动实现要点:利用数组存储完全二叉树,通过索引计算父子节点位置(父节点(i-1)//2,左子节点2i+1,右子节点2i+2)。大顶堆与小顶堆仅比较逻辑相反。面试常考堆排序(先建堆再反复交换堆顶)和Top K问题(维护
·
堆(Heap)作为优先队列的核心实现,是算法面试的高频考点。以下是手动实现大顶堆和小顶堆的详解,结合代码实例与面试技巧:
⚙️ 一、堆的核心特性
- 完全二叉树结构
堆本质是完全二叉树,可用紧凑数组存储(索引从0开始):- 父节点索引:
parent(i) = (i-1)//2
- 左子节点索引:
left(i) = 2*i + 1
- 右子节点索引:
right(i) = 2*i + 2
- 父节点索引:
- 堆序性质
- 大顶堆:父节点值 ≥ 子节点值 → 根节点为最大值
- 小顶堆:父节点值 ≤ 子节点值 → 根节点为最小值
🧱 二、手动实现堆的关键操作
1. 大顶堆完整实现(Python)
class MaxHeap:
def __init__(self):
self.heap = []
def insert(self, val):
self.heap.append(val) # 插入末尾
self._sift_up(len(self.heap) - 1) # 上浮调整
def extract_max(self):
if not self.heap: return None
max_val = self.heap[0]
self.heap[0] = self.heap[-1] # 末尾元素移至堆顶
self.heap.pop()
self._sift_down(0) # 下沉调整
return max_val
def _sift_up(self, idx):
while idx > 0:
parent_idx = (idx - 1) // 2
if self.heap[idx] <= self.heap[parent_idx]:
break
# 交换父节点与当前节点
self.heap[idx], self.heap[parent_idx] = self.heap[parent_idx], self.heap[idx]
idx = parent_idx
def _sift_down(self, idx):
size = len(self.heap)
while (left_idx := 2*idx + 1) < size: # 存在左子节点
right_idx = 2*idx + 2
largest = idx
# 比较左子节点
if self.heap[left_idx] > self.heap[largest]:
largest = left_idx
# 比较右子节点(若存在)
if right_idx < size and self.heap[right_idx] > self.heap[largest]:
largest = right_idx
if largest == idx: break # 堆序已满足
# 交换并继续下沉
self.heap[idx], self.heap[largest] = self.heap[largest], self.heap[idx]
idx = largest
2. 小顶堆的改动点
仅需修改比较方向:
class MinHeap(MaxHeap):
def _sift_up(self, idx):
while idx > 0:
parent_idx = (idx - 1) // 2
if self.heap[idx] >= self.heap[parent_idx]: # 改为≥
break
self.heap[idx], self.heap[parent_idx] = self.heap[parent_idx], self.heap[idx]
idx = parent_idx
def _sift_down(self, idx):
size = len(self.heap)
while (left_idx := 2*idx + 1) < size:
right_idx = 2*idx + 2
smallest = idx
if self.heap[left_idx] < self.heap[smallest]: # 改为<
smallest = left_idx
if right_idx < size and self.heap[right_idx] < self.heap[smallest]: # 改为<
smallest = right_idx
if smallest == idx: break
self.heap[idx], self.heap[smallest] = self.heap[smallest], self.heap[idx]
idx = smallest
⚡️ 三、核心操作原理解析
1. 插入操作(上浮调整)
- 步骤:
- 新元素插入数组末尾
- 与其父节点比较
- 若破坏堆序则交换,递归上浮
- 时间复杂度:
O(log n)
示例(大顶堆插入92
):
mermaid graph TD A[原始堆:100,90,80,30,60] --> B[插入92到末尾:100,90,80,30,60,92] B --> C[92>60 → 交换:100,90,80,92,60,30] C --> D[92>90 → 交换:100,92,80,90,60,30] D --> E[92<100 → 停止]
2. 删除堆顶(下沉调整)
- 步骤:
- 堆顶与末尾元素交换
- 移除原堆顶(最大值)
- 新堆顶与其较大子节点比较,若需交换则下沉
- 时间复杂度:
O(log n)
示例(删除大顶堆的100
):
mermaid graph TD A[原始堆:100,92,80,90,60,30] --> B[交换堆顶与末尾:30,92,80,90,60,100] B --> C[移除100 → 新堆:30,92,80,90,60] C --> D[30<92 → 与92交换:92,30,80,90,60] D --> E[30<90 → 与90交换:92,90,80,30,60] E --> F[30无子节点 → 停止]
💡 四、面试高频考点与技巧
- 手写堆排序
- 步骤:
- 将无序数组构建成大顶堆(从最后一个非叶子节点开始调整)
- 重复交换堆顶与末尾元素 → 移除堆顶 → 调整剩余堆
- 代码关键:
- 步骤:
def heap_sort(arr):
n = len(arr)
# 构建大顶堆(从最后一个非叶子节点开始)
for i in range(n//2 - 1, -1, -1):
heapify(arr, n, i)
# 逐个提取最大值
for i in range(n-1, 0, -1):
arr[0], arr[i] = arr[i], arr[0] # 交换堆顶与末尾
heapify(arr, i, 0) # 调整剩余堆
- Top K问题优化
- 场景:海量数据中找前K个最大值
- 解法:维护小顶堆(堆顶为最小屏障)
def top_k_max(nums, k):
min_heap = []
for num in nums:
if len(min_heap) < k:
heapq.heappush(min_heap, num)
elif num > min_heap[0]: # 当前值 > 堆顶
heapq.heapreplace(min_heap, num) # 替换堆顶
return min_heap
- 双向比较陷阱
- 错误示例:下沉时未同时比较左右子节点
- 正确做法:先比较左右子节点大小,再与父节点比较
# 在大顶堆下沉函数中:
left_val = heap[left_idx]
right_val = heap[right_idx] if right_idx < size else -float('inf')
larger_child = max(left_val, right_val) # 先找到较大子节点
if larger_child > parent_val: # 再与父节点比较
swap(parent, larger_child)
🚀 五、性能优化技巧
- 减少交换次数
上浮/下沉时暂存目标值,最后一次性写入(避免多次交换):
def _sift_up_fast(self, idx):
tmp = self.heap[idx]
while idx > 0:
parent_idx = (idx - 1) // 2
if tmp <= self.heap[parent_idx]: break
self.heap[idx] = self.heap[parent_idx] # 父节点下移
idx = parent_idx
self.heap[idx] = tmp # 最终位置写入
- 批量建堆优化
直接对无序数组建堆(时间复杂度O(n)
,优于逐个插入的O(n log n)
):
def build_heap(arr, is_max_heap=True):
n = len(arr)
for i in range(n//2 - 1, -1, -1): # 从最后一个非叶子节点开始
heapify(arr, n, i, is_max_heap)
💎 总结
操作 | 大顶堆实现要点 | 小顶堆改动点 |
---|---|---|
插入(上浮) | 与父节点比较,若更大则交换 | 与父节点比较,若更小则交换 |
删除(下沉) | 与较大子节点比较并交换 | 与较小子节点比较并交换 |
应用场景 | 堆排序、Top K最小值 | Top K最大值、Dijkstra算法 |
面试技巧:
- 强调堆的完全二叉树性质与数组表示法
- 写代码时先注释
_sift_up
和_sift_down
的步骤- 主动分析时间复杂度(插入/删除
O(log n)
,建堆O(n)
)
练习题目:https://leetcode.com/problems/kth-largest-element-in-an-array/(小顶堆)、https://leetcode.com/problems/top-k-frequent-elements/(大顶堆+哈希)
更多推荐
所有评论(0)