二叉堆是一种支持插入、删除、查询最值的数据结构。它其实是一颗满足"堆性质"的完全二叉树 (完全二叉树:叶子节点都在最后两层,且在最后一层集中于左侧的二叉树),树上的每个节点带有一个权值。若树中的任意一个节点的权值都小于等于其父节点的权值,则称该二叉树满足"大根堆"性质。若树中任意一个节点的权值都大于等于其父节点的权值,则称该二叉树满足"小根堆性质"。满足"大根堆性质"的完全二叉树就是"大根堆",而满足"小根堆性质"的完全二叉树就是"小根堆",二者都是二叉堆的形态之一。
根据完全二叉树的性质,我们可以采用层次序列存储方式,直接用一个数组保存二叉堆。层次序列存储方式,就是逐层从左到右为树中的节点依次编号,把此编号作为节点在数组中存储的位置(下标)。在这种存储方式中,父节点编号等于子节点编号除以2,左子节点编号等于父节点编号乘2,右子节点编号等于父节点编号乘以2加1。
我们以大根堆为例探讨堆支持的几种常见操作的实现。
$$ Insert $$
Insert(val) 操作向二叉堆中插入一个带有权值 val 的新节点。我们把这个新节点直接放在存储二叉堆的数组末尾,然后通过交换的方式向上调整,直至满足堆性质。
$$ 时间复杂度为堆的深度,即 O(logN). $$
int heap[N], n;
void up(int p) {
//向上调整
while (p > 1) {
if (heap[p] > heap[p >> 1]) {
// 子节点 > 父节点, 不满足大根堆性质
std::swap(heap[p], heap[p >> 1]);
p >>= 1;
} else {
break;
}
}
}
void Insert(int val) {
heap[++n] = val;
up(n);
}
$$ GetTop $$
GetTop 操作返回二叉堆的堆顶权值,即最大值 heap[1]。
$$ 时间复杂度为 O(1) $$
int GetTop() {
return heap[1];
}
$$ Extract $$
Extract 操作把堆顶从二叉堆中移除。我们把堆顶 heap[1] 与存储在数组末尾的节点 heap[n] 交换,然后移除数组末尾节点(n--),最后把堆顶通过交换的方式向下调整,直至满足堆性质。
$$ 时间复杂度为 O(logN). $$
void down(int s) {
int p = s << 1;
while (s <= n) {
if (s < n && heap[s] < heap[s + 1]) s++;
// 左右子节点中取较大者
if (heap[s] > heap[p]) {
// 子节点大于父节点, 不满足大根堆性质
std::swap(heap[s], heap[p]);
p = s;
s = p << 1;
} else {
break;
}
}
}
void Extract() {
heap[1] = heap[n--];
// heap[1] = heap[n];
// n--;
down(1);
}
$$ Remove $$
Remove(p) 操作把存储在数组下标 p 位置的节点从二叉堆中删除。与 Extract 相类似,我们先把 heap[p] 与 heap[n] 交换,然后令 n 减 1。注意此时 heap[p] 既有可能需要向下调整,也有可能需要向上调整,需要分别进行检查和处理。
$$ 时间复杂度为 O(logN) $$
void Remove(int k) {
heap[k] = heap[n--];
up(k), down(k);
}
C++ STL中的 priority_queue (优先队列) 为实现一个大根堆,支持push(Insert),top(Gettop) 和 pop(Extract) 操作,不支持 Remove 操作,详细用法参见 0x71 节。