Menu
CoddyTech

堆(二叉堆)

最后更新

二叉堆是一棵完全二叉树,它把最小值(最小堆)或最大值(最大堆)放在根节点。这里演示的是最小堆:每个父节点都小于或等于它的子节点。添加一个值时,先把它放到下一个空位,再让它“上浮”(sift up):只要它比父节点小,就和父节点交换,直到重新满足堆的性质。点击上方的“播放”,观看每个新值如何一步步上浮到自己的位置。

因为堆是完全二叉树,所以可以紧凑地存放在数组里:节点 i 的子节点位于 2i+1 和 2i+2。插入和删除最小值都是 O(log n)(只走一条从根到叶的路径),查看最小值是 O(1),这正是优先队列需要的。

时间复杂度与空间复杂度

操作复杂度说明
查看最小值/最大值O(1)永远在根节点
插入(push)O(log n)沿一条路径上浮
删除最小值/最大值O(log n)沿一条路径下沉
建堆O(n)一次性堆化
空间O(n)基于数组,不需要指针

分步说明(push)

步骤发生了什么
1把新值追加到末尾(下一个空的叶子位置)。
2把它和父节点比较。
3如果它更小(最小堆),就和父节点交换,向上移动。
4重复以上步骤,直到它不再小于父节点,或者到达根节点。

实例演示

依次插入 [5, 3, 8, 1, 4],建立一个最小堆:

插入上浮后的数组操作
5[5]第一个值成为根节点。
3[3, 5]3 小于父节点 5,交换后升到根节点。
8[3, 5, 8]8 大于父节点 5,留在叶子位置。
1[1, 3, 8, 5]1 小于父节点 5,交换;接着 1 小于父节点 3,再交换到根节点。
4[1, 3, 8, 5, 4]4 大于父节点 3,原地不动;最小值 1 仍在根节点。

什么时候用堆

适合使用不适合使用
你需要反复从一个不断变化的集合中取出最小或最大的元素。你需要查找任意值,而不仅仅是最值:这时用二叉搜索树或哈希集合。
你要为 Dijkstra、A* 或任务调度器实现优先队列。你需要数据始终保持完全有序。
你想要 O(log n) 的插入和删除最小值,同时使用紧凑的数组布局。你需要快速查找或删除某个特定的(非根)元素。
你需要合并一串数据流,并按优先级逐个取出。数据量很小,线性扫描更简单,也足够快。

Heap (Priority Queue) 代码

简洁、可运行的 Heap (Priority Queue) 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Heap (Priority Queue) 的 Python 代码

Python
1class MinHeap:2    def __init__(self):3        self.data = []4
5    def push(self, value):6        # Append at the end, then bubble up to restore order7        self.data.append(value)8        i = len(self.data) - 19        while i > 0:10            parent = (i - 1) // 211            if self.data[parent] <= self.data[i]:12                break13            self.data[i], self.data[parent] = self.data[parent], self.data[i]14            i = parent15
16    def pop(self):17        # Move the last leaf to the root, then sift it down18        top = self.data[0]19        last = self.data.pop()20        if self.data:21            self.data[0] = last22            self._sift_down(0)23        return top24
25    def _sift_down(self, i):26        n = len(self.data)27        while True:28            smallest = i29            left, right = 2 * i + 1, 2 * i + 230            if left < n and self.data[left] < self.data[smallest]:31                smallest = left32            if right < n and self.data[right] < self.data[smallest]:33                smallest = right34            if smallest == i:35                return36            self.data[i], self.data[smallest] = self.data[smallest], self.data[i]37            i = smallest38
39
40heap = MinHeap()41for value in [5, 3, 8, 1, 9, 2]:42    heap.push(value)43
44print("Heap array:     ", heap.data)45print("Popped in order:", [heap.pop() for _ in range(6)])
在 Python 在线编辑器中运行这段代码

堆常见问题

堆有什么用?

堆用来实现优先队列,而优先队列支撑着 Dijkstra 最短路径算法、任务调度器和事件模拟。堆也是堆排序的核心。只要你需要反复从变化的集合中取出最小或最大的元素,堆就是合适的工具。

堆和二叉搜索树有什么区别?

两者都是二叉树,但二叉搜索树维持从左到右的完整顺序(因此能有序查找),而堆只保证父子之间的大小关系(最小值或最大值在根节点)。堆能以 O(1) 取得最值;二叉搜索树能以 O(log n) 查找任意值。

为什么堆要存放在数组里?
因为堆始终是一棵完全二叉树,它的节点可以完美对应到数组下标:下标 i 的子节点在 2i+1 和 2i+2,父节点在 (i-1)/2。这样不必存储子节点指针,缓存性能也非常好。
堆和有序数组是一回事吗?
不是。堆只保证每个父节点比子节点小(最小堆)或大(最大堆),兄弟节点和堂兄弟节点之间没有特定顺序。有序数组完全有序,但插入要花 O(n);堆插入只需 O(log n),同时仍能立即取得最值。
什么时候该用堆,而不是直接对数组排序?
当数据不断变化,而你只关心当前的最小值或最大值时,就用堆:每次插入和弹出都只需 O(log n),不必重新排序整个数组。如果数据是静态的,而你需要所有元素都有序,一次 O(n log n) 的排序更简单,往往也更快。
用 n 个元素建堆需要 O(n log n) 吗?
一次性建堆就不需要。逐个插入 n 个元素是 O(n log n),但自底向上的 heapify 从最后一个父节点开始一路下沉到根节点,总共只需 O(n),因为大多数节点都在底部,只需下沉很短的距离。
Coddy 编程语言插图

在 Coddy 掌握算法

开始