← 返回题目列表

堆是如何删除堆顶元素的?什么是「下沉」?

中等 第 20 / 28 题 更新于 2026/07/28
删除下沉siftDown

简化版

删除堆顶(取最值)分三步:先取出堆顶(数组第 0 个)作为返回值;把最后一个元素移到堆顶(填补空缺、保持完全二叉树);然后让它下沉(sift down)——不断和较大的孩子(大顶堆)或较小的孩子(小顶堆)比较,若违反堆序就交换下沉,直到就位或成为叶子。O(log n)。

详细版

大顶堆为例:

int poll(int[] heap) {          // 取出并删除最大值
    int top = heap[0];          // 1. 堆顶就是最大值
    heap[0] = heap[--size];     // 2. 最后一个元素移到堆顶
    siftDown(heap, 0);          // 3. 下沉
    return top;
}
void siftDown(int[] heap, int i) {
    while (true) {
        int l = 2*i+1, r = 2*i+2, largest = i;
        if (l < size && heap[l] > heap[largest]) largest = l;
        if (r < size && heap[r] > heap[largest]) largest = r;  // 找最大的孩子
        if (largest == i) break;              // 已满足堆序
        swap(heap, i, largest);               // 和较大孩子交换,下沉
        i = largest;
    }
}
  • 移最后一个到堆顶:末尾元素删掉不影响结构,把它顶到根来填补。
  • 下沉:这个元素通常较小,要和「较大的那个孩子」交换往下走(小顶堆则和较小孩子)。

完整版教学

一、为什么用「最后一个元素填堆顶」

删掉堆顶后,根的位置空了。如果直接把某个孩子提上来,会连锁地在下面留下更多空洞,破坏完全二叉树结构。最巧妙的做法是:把数组最后一个元素移到堆顶。因为删掉「最后一个」不会破坏完全二叉树(末尾正是可以安全移除的位置),移到堆顶后结构依然完整。代价是这个元素通常很小(它本来在底层),放到堆顶违反了堆序,所以要下沉修正。

二、下沉:把「太小的」往下送,且要和较大孩子换

下沉的关键细节是:必须和「较大的那个孩子」交换(大顶堆)。为什么?因为交换上来的孩子要当新的父节点,它必须 ≥ 另一个孩子才满足堆序。如果和较小的孩子换,换上来的还是比另一个孩子小,堆序仍被破坏。所以每一步:

  1. 找出左右孩子中较大的那个。
  2. 如果当前节点比这个较大孩子还小(违反父 ≥ 子),就和它交换、下沉一层。
  3. 重复,直到比两个孩子都大(就位)或没有孩子(成为叶子)。

小顶堆对称:和较小的孩子比较、交换。

三、走一个例子

大顶堆 [10,7,9,3,5,8] 删除堆顶 10:
1) 取出 10
2) 末尾 8 移到堆顶: [8,7,9,3,5](size 从 6 变 5)
3) 下沉 8:
   孩子 l=1(7), r=2(9),较大是 9 > 8 → 交换 → [9,7,8,3,5],i=2
   i=2 的孩子 l=5 越界(size=5),无孩子 → 停止
结果堆顶变成次大值 9 ✓

四、复杂度

  • 时间 O(log n):下沉最多走一条到叶子的路径,长度 = 树高 = log n。
  • 空间 O(1):原地交换。

peek(只看堆顶不删)是 O(1)——直接返回 heap[0]。

五、易错点

  • 必须和较大/较小的孩子换:大顶堆和较大孩子换、小顶堆和较小孩子换,换错会破坏堆序。
  • 孩子越界检查l < sizer < size 才是有效孩子(可能只有左孩子或没有孩子)。
  • 先取堆顶再覆盖:先保存 heap[0] 作返回值,再用末尾元素覆盖它。
  • size 要先减:末尾元素被移走后,堆的有效范围缩小 1。

六、常见误区与追问

考点正确口径
删除对象只能直接删除堆顶最优元素
补位用最后一个元素放到堆顶保持形状
下沉和更优孩子交换,直到堆序恢复
ans = heap[0]
heap[0] = heap[last]
remove last
siftDown(0)
return ans

删除堆顶也是先保形状,再修堆序;下沉时必须选择两个孩子中更优的那个。

  • 误区:删除堆顶后可以直接把左孩子提上来。 这样可能破坏完全二叉树形状,标准做法是用最后一个元素补根。
  • 误区:下沉时随便和一个孩子交换。 大顶堆必须和较大孩子交换,小顶堆必须和较小孩子交换,否则另一侧可能仍破坏堆序。
  • 误区:poll 后数组末尾元素可以不删除。 不删除会留下重复元素,也会让堆大小错误。
  • 追问:为什么下沉只走一条路径? 每次把当前元素放到更合适的一侧,未交换的子树仍然保持堆序。
  • 追问:复杂度是多少? 最多从根下沉到叶子,时间 O(log n),额外空间 O(1)。
  • 追问:堆能高效删除任意元素吗? 普通堆不能直接定位任意元素;需要索引表辅助,或接受 O(n) 查找。

七、加强记忆

删堆顶三步:取出 heap[0](返回最值)→ 末尾元素移到堆顶(保持完全二叉树)→ 下沉(siftDown):和较大孩子(大顶堆)/较小孩子(小顶堆)比较,违反堆序就交换往下,直到就位或成叶子。O(log n),peek 是 O(1)。关键:下沉必须和「较大/较小的那个孩子」换,且要判孩子越界。