(b2)完全二叉堆:插入与上滤

官官方信息技术老师·13 页·深入(追求细节与边界)·1 次浏览·38 天前
二叉堆上滤优先队列

完全二叉堆:插入与上滤

看完你能逐步画出新元素上滤的每一步交换并说清何时停止

按 空格/→ 演示下一步

问一句
1 / 13 页

全部页面点击任意一页,跳回舞台从这页播放

二叉堆上滤优先队列

完全二叉堆:插入与上滤

看完你能逐步画出新元素上滤的每一步交换并说清何时停止

1第 1 页 · 完全二叉堆:插入与上滤

堆序性:堆的秩序

上一页认识了完全二叉堆的形状,但光有形状还不够——节点之间得讲「规矩」。想象一家公司:上级的能力必须不弱于直接下属,否则组织就乱套了。堆的这条规矩,就叫堆序性。

父亲≥孩子
大顶堆中每个节点都不小于其孩子,从根到叶层层递减
兄弟不管
兄弟之间、叔侄之间没有大小约束,只看父子这条线
局部规则
只需逐对检查父子关系,无需全局排序,维护成本低
公司职级对应 →大顶堆

上级不弱于直接下属对应父亲不小于孩子,平级同事互不管辖

2第 2 页 · 堆序性:堆的秩序

完全二叉树的向量存储

上层是树形结构,实线箭头 2i+1、2i+2 指向孩子;虚线箭头标出父亲下标的换算,物理上它们连续存进一个向量。

图解渲染中…
a0:方框内是向量下标与元素值,树根对应下标 0a1:实线=父子链在树里的形状,虚线=从孩子下标反推父亲a5:最后一个节点,向量末尾;插入新元素先进这里再上滤
3第 3 页 · 完全二叉树的向量存储

插入三步走

插入一个新元素,靠三步既保住完全二叉的形,又保住堆序。

1
接到末尾
新元素追加到向量末尾,正好保持完全二叉树形状
2
和父亲比
按 (i-1)/2 找到父亲,比较大小,判断是否违反堆序
3
交换上滤
若新元素更优就与父亲交换,重复比较直到父亲不再更小
4第 4 页 · 插入三步走

上滤(percolate up)

上一步我们把新元素直接追加到向量末尾——这是完全二叉树唯一能放的位置。但这个新来的可能比父节点还大,堆序被破坏了。怎么办?先看看它在树里和父亲的关系。

只和父节点比
i 的父亲下标是 ⌊(i−1)/2⌋,新元素只需与父亲比较
违反就交换
若新元素 > 父节点,两者交换,新元素升到父亲位置
重复直到停止
两种情况停:父亲更大(堆序满足),或已到根(无父亲可比)
最多上滤 O(log n)
树高 h≈log₂n,每层至多交换一次,比较次数上界即树高
水中的气泡对应 →上滤过程

气泡比水轻就上升一层,比周围重(父亲更大)就停住

parent(i)=⌊(i−1)/2⌋,上滤次数≤⌈log⁡2n⌉\text{parent}(i)=\lfloor (i-1)/2 \rfloor,\quad \text{上滤次数} \le \lceil \log_2 n \rceil
5第 5 页 · 上滤(percolate up)

与父亲交换 vs 整体上滤

两版上滤结果相同,但赋值开销差三倍——混淆点在于以为比较次数也变了。

朴素交换
  • 新元素一路与父亲swap换位
  • 每次交换需3次赋值
  • 比较与赋值都为O(logn)常数较大
整体上滤
  • 先备份新元素,父亲逐一整体下移
  • 每步只需1次赋值,最后归位1次
  • 比较次数不变,赋值约降为1/3
正确的理解是:两者比较次数相同,仅赋值不同——插入务必用整体上滤,交换版只是教学过渡。
6第 6 页 · 与父亲交换 vs 整体上滤

实例:完整走一遍插入

把 5 插入堆 [2,4,6,7],逐帧跟踪上滤的每一步。

1
末尾补位
5 先接到第 5 个位置,保持完全二叉树形状
2
比较父亲
5 的父亲是 6,孩子比父亲小,违反堆序
3
交换上滤
5 与 6 交换,5 升到上一层,继续检查
4
停止条件
5 的新父亲是 2,2≤5,堆序恢复,上滤结束
7第 7 页 · 实例:完整走一遍插入

实现:代码逐行读

cpp

insert 与 percolateUp 的真实实现,逐行对照前面的三步走与整体上滤。

代码高亮加载中…

L1/L2 对应三步走的前两步;L4 是向量存储下的父结点下标公式;L5 是循环不变式出口——父亲更大即堆序恢复;L7 完成「整体上移」。

8第 8 页 · 实现:代码逐行读

终止条件与边界

上一页我们逐行走完了上滤代码,循环里其实藏着一个问题:交换到什么时候才算完?理解了终止条件,边界情况——比如新元素一路顶到根——也就迎刃而解了。

终止条件一
不再大于父亲:堆序已恢复,立即停
终止条件二
到达顶端:已是根,没有父亲可比
哨兵 −1
把根的父亲下标记为 −1,越界比较自然失败,无需特判
两种停法等价
无论哪种停法,堆序性和结构完整都不曾被破坏
爬楼梯找办公室对应 →上滤终止

要么到了目标层(不大于父亲),要么到了顶层(是根)

i>0  ∧  heap[parent(i)]≥heap(i)i > 0 \;\land\; heap[parent(i)] \ge heap(i)
9第 9 页 · 终止条件与边界

效率:为什么是 O(logn)

前面我们走完了一遍插入:新元素从叶一路向上滤,直到找到位置。那这条路最长能有多长?答案藏在这棵树的高度里——而完全二叉树的高度,恰好是对数级的。

树高决定上界
上滤至多走一条从叶到根的路径,步数不超过树高
高度是对数的
n 个节点的完全二叉树,高度约 log₂n
规模翻倍只加一层
n 从千到百万,树高仅从 10 涨到 20
公司逐级上报对应 →上滤路径

员工逐层找上级,层级数对应树高,层内人数指数增长

h=⌊log⁡2n⌋⇒T(n)=O(log⁡n)h = \lfloor \log_2 n \rfloor \Rightarrow T(n) = O(\log n)
10第 10 页 · 效率:为什么是 O(logn)

自测:上滤考考你

点击作答

向含 15 个元素的完全二叉堆插入一个新元素,上滤最坏需要与父亲交换几次?

11第 11 页 · 自测:上滤考考你

要点收束

  • ✓堆序性只需父子成立,兄弟无序——这是插入能局部修复的前提
  • ✓向量按层序存完全二叉树,下标 i 的父亲是 ⌊(i−1)/2⌋,无需指针
  • ✓插入三步:接到末尾、与父亲比较、必要时上滤
  • ✓整体上滤优于逐次交换:比较次数不变,赋值从 3t 降到 t+1
  • ✓树高 O(logn) 封顶,单次插入最坏 O(logn)
延伸主题:删除与下滤(percolate down)Floyd 建堆:O(n) 的秘密堆排序完整流程
12第 12 页 · 要点收束

课后思考

先自己想一会儿,再对照参考答案——想不通的地方,值得回去翻对应那页。

1上滤为什么最多走 O(logn) 步?能不能更准地说出最好和最坏情况各是几次比较?

参考答案核心在完全二叉树的高度是 ⌊log₂n⌋。最好情况:新元素不比父亲小,一次比较就停,O(1);最坏情况:一路顶到根,恰好做 ⌊log₂n⌋ 次比较。

2优先队列的 decrease-key 场景里,如果元素优先级变小,该怎么复用上滤?方向反过来又该叫什么?

参考答案优先级变小意味着要往上冒,正是上滤——定位到该元素后从那个位置向上滤即可。反过来优先级变大则要下沉,那叫下滤(percolate down),是 delete-max 的核心。

3建堆时如果从空堆开始逐个插入,总代价为什么是 O(nlogn)?有没有办法只用 O(n) 就把一堆无序数据变成堆?

参考答案逐个插入时第 k 个元素最多上滤 log k 步,求和得 O(nlogn)。而 Floyd 自底向上建堆:从最后一个内部节点倒着逐个下滤,多数元素高度低,求和后恰好 O(n)——思路可迁移到本题。

13第 13 页 · 课后思考