Skip to content

入门:完全二叉树、堆序性与数组表示

基于通用数据结构概念 · 核于 2026-07

速查

  • 定义:堆是一棵完全二叉树,且满足堆序性——每个节点的值都 ≤(小根堆)或 ≥(大根堆)其子节点的值。
  • 完全二叉树:除最后一层外每层都填满,最后一层节点从左到右连续——这个「紧致」形状是堆能用数组无缝表示的前提。
  • 大根堆 / 小根堆:大根堆父 ≥ 子(堆顶是最大值);小根堆父 ≤ 子(堆顶是最小值)。注意堆序性是父子关系,不要求兄弟之间有序。
  • 数组表示:把完全二叉树按层序映射到数组(下标 0 是根)。父子下标:父 (i-1)/2(整除)、左子 2i+1、右子 2i+2——都是 O(1) 算术运算,无需存指针。
  • 为何 O(1) 父子访问:完全二叉树的「每层填满 + 末层左对齐」让节点编号与数组下标一一对应,父子关系是确定的算术公式,不依赖树的实际形状。
  • 核心复杂度:取堆顶(极值)O(1);插入(sift up)O(log n);删除堆顶(sift down)O(log n);建堆 O(n)(Floyd);查任意值 O(n)(中间无序)。
  • 堆 vs BST:堆只保证根是极值(部分序),BST 保证中序遍历有序(全序)。堆取最值 O(1)、查任意值 O(n);BST 取最值 O(log n)、查任意值 O(log n)——选型看「要极值还是要全序」。
  • 堆不关心全序:堆里同一层的节点、左右兄弟之间都没有大小约束,只约束父子——所以中序遍历堆得不到有序序列。
  • 高度:n 个节点的完全二叉树高度为 ⌈log₂(n+1)⌉ - 1 ≈ log₂n,所以 sift up/down 最多走树高步,即 O(log n)。
  • JS 无原生堆:JS 标准库没有 Heap 类,需手写(基于数组的 sift up/down)或用第三方库(如 heap-js);Java 有 PriorityQueue、C++ 有 std::priority_queue、Python 有 heapq
  • 进阶顺序堆的核心操作堆的工程应用参考

一、堆是什么:完全二叉树 + 堆序性

堆是两个性质的叠加:

  1. 完全二叉树(结构性质):除最后一层外,每层都被完全填满;最后一层的节点从左到右连续排列,中间不留空。这个「紧致」形状保证了树高最小(⌈log₂n⌉),也是它能用数组无缝表示的前提。
  2. 堆序性(值性质):每个节点的值都 ≤ 或 ≥ 其子节点。具体分两种:
    • 大根堆(max-heap):父节点 ≥ 子节点 → 堆顶是整堆的最大值
    • 小根堆(min-heap):父节点 ≤ 子节点 → 堆顶是整堆的最小值
        大根堆示例                  小根堆示例
            9                          1
          /   \                      /   \
         8     7                    3     2
        / \   /                    / \   /
       5   4 6                    7  6  4

关键:堆序性只约束「父子」,不约束「兄弟」。上面大根堆里 8 与 7(兄弟)谁大都行,5 与 4、6 之间也无序——只要每个父节点比自己的子节点大即可。这正是堆「只管根极值、不管全序」的根源。

二、数组表示:完全二叉树的天然映射

完全二叉树的「每层填满 + 末层左对齐」让它能按层序无缝映射到数组,且父子下标有确定的算术公式。约定根在下标 0:

下标 i 的节点:
  父节点下标    = (i - 1) / 2   (整除,向下取整)
  左子节点下标  = 2 * i + 1
  右子节点下标  = 2 * i + 2

示例(大根堆 [9, 8, 7, 5, 4, 6]):
  下标: 0  1  2  3  4  5
  值:   9  8  7  5  4  6

  根 9 (下标0)
    左子 8 (下标 2*0+1=1)
    右子 7 (下标 2*0+2=2)
  节点 8 (下标1) 的父 = (1-1)/2 = 0 ✓
  节点 7 (下标2) 的父 = (2-1)/2 = 0 ✓ (整除)

为什么这套映射是 O(1)

父子下标关系是纯算术运算(乘 2、加 1、除 2),不依赖树的实际形状、不需要遍历、不存指针——CPU 一个时钟周期就算完。这就是堆「数组实现、无指针开销」的原因:

  • 省内存:每个节点只存值,不存左右指针(BST 链式实现每个节点要 2 个指针)。
  • 缓存友好:数组连续存放,sift up/down 沿路径访问时命中缓存行。
  • 父子访问 O(1)parent = (i-1) >> 1left = (i<<1) + 1right = (i<<1) + 2,位运算更快。

前提是「完全二叉树」:如果树中间有空缺(普通二叉树),下标公式就失效了——因为「下标 2i+1/2i+2」隐含了「第 i 个节点的子节点一定紧挨在它后面」这一完全性假设。所以只有完全二叉树才能这样映射,这也是堆必须是完全二叉树的工程理由。

三、大根堆 vs 小根堆:选哪个

维度大根堆(max-heap)小根堆(min-heap)
堆序性父 ≥ 子父 ≤ 子
堆顶最大值最小值
典型场景取前 K 大的辅助、堆排序(升序)优先队列、Top-K 前 K 大、Dijkstra
Java PriorityQueue需传反转比较器默认

记忆:「要最小用小根堆,要最大用大根堆」——堆顶直接给你要的极值。一个反直觉但高频的技巧:求前 K 大元素用小根堆(维护大小为 K 的小根堆,新元素比堆顶大就替换堆顶,最后堆里就是前 K 大)——详见堆的工程应用

四、堆与二叉搜索树(BST)的区别

堆和 BST 都是二叉树,但约束完全不同:

维度二叉搜索树(BST)
结构约束完全二叉树(形状固定)任意形状(左 < 根 < 右)
值约束父子:父 ≥/≤ 子(部分序)全序:左子树 < 根 < 右子树
堆顶 / 根堆顶是极值(最大或最小)根是「中间值」,非极值
取最值O(1)(堆顶就是)O(log n)(走到最左/最右)
查任意值O(n)(中间无序,线性扫描)O(log n)(有序可二分)
插入/删除O(log n)(sift up/down)O(log n)(平均)/ O(n)(最坏退化成链)
中序遍历无序(不保证全序)有序(升序序列)

一句话:堆只管「根是极值」(取最值快,查任意值慢);BST 管「全序」(查任意值快,取极值要走到端点)。所以:

  • 要动态取极值、做优先队列 → 堆(取最值 O(1) 是核心优势)。
  • 要频繁按值查找、要有序遍历 → BST(平衡树 / 红黑树)。

堆「牺牲全序换 O(1) 极值」的设计,正是为优先队列这类「只关心谁该最先出队」的场景量身定做的。

五、堆的高度:为什么操作是 O(log n)

n 个节点的完全二叉树,每层节点数翻倍(第 0 层 1 个、第 1 层 2 个、第 2 层 4 个……),所以高度 h 满足:

2^0 + 2^1 + ... + 2^(h-1) + 最后不满一层 = n
即 2^h - 1 ≤ n ≤ 2^(h+1) - 1
解得 h ≈ ⌊log₂n⌋

sift up(从尾部往上走)和 sift down(从堆顶往下走)每步移动一层,最多走 h 步,所以都是 O(log n)。这也是「完全二叉树高度最小」的价值——同样的 n 个节点,链状树高度是 O(n),完全二叉树是 O(log n),操作快得多。

六、JS 无原生堆怎么办

主流语言的堆支持差异很大:

语言堆实现备注
Javajava.util.PriorityQueue默认小根堆,传 Comparator.reverseOrder() 变大根堆
C++std::priority_queue默认大根堆,配 greater<T> 变小根堆;还有 std::make_heap 系列操作原数组
Pythonheapq 模块默认小根堆,操作 list;大根堆用取负技巧
Gocontainer/heap接口式,需自己实现 Heap 接口
JavaScript无原生需手写(基于数组的 sift up/down,见堆的核心操作)或用第三方库(如 heap-js

JS 手写堆是面试常考题——核心就是下面两个操作(sift up / sift down),掌握了就能造出一个完整的小根堆。这也是为什么本叶在参考里给出完整的代码模板。

下一步

理解了堆的定义、数组表示与父子下标后,下一步是堆的两个核心操作——**sift up(插入上浮)**与 sift down(删除堆顶下沉),它们是所有堆操作的基石,见堆的核心操作