数据结构|堆
完成阅读您将会了解堆的:
- 概念
- 构建方法
- 基本操作
- C++实现
- Rust实现
1. 概念
堆(Heap)的概念最早可见于 J. W. J. Williams 在1964年发表的堆排序[1]一文当中。本文介绍的堆指的是一种数据结构,注意与内存模型中垃圾回收机制(Garbage Collection Mechanism)区分。
不加限定时,堆作数据结构时通常指二叉堆(Binary Heap)。二叉堆本质是一种特殊的完全二叉树(Binary Tree),它满足以下基本性质: 若子节点恒小于等于母节点,则称其为最大堆(Max Heap),如图1[2];若子节点恒大于等于母节点,则称其为最小堆(Min Heap),如图2[2:1]。本文所介绍的堆即为二叉堆,具体编程示例为最大堆。


2. 构建方法
本文采用数组(Array)作为构建堆的基础容器,为了更方便地阐述,我们在此约定堆的下标依旧从0开始。若给定下标\([i]\)节点存在子节点,那么其左右子节点的下标分别为\([2i+1]\)和\([2i+2]\),除根节点外,其母节点下标为\([\lfloor\frac{i-1}{2}\rfloor]\)。
构建堆常用的方法是逐个插入元素,并在每一次插入时维护堆的性质不变,直至完成所有元素插入,如图3[2:2]。在这个过程中,维护堆性质的方法叫做堆化(Heapify),时间复杂度为\(O(\lg n)\),其具体过程见伪代码1。

伪代码1:堆化
变量说明:H\(\rightarrow\)堆数组;i\(\rightarrow\)堆化始发下标;left\(\rightarrow\)左子节点下标;right\(\rightarrow\) 右子节点下标
注意堆化是堆的基础上进行的,且堆化是向下进行的过程,只会影响堆化始发下标之后的元素。那么也就是说在每一次更新下标为\([i]\)的元素前,若\(A[2i+1]\)与\(A[2i+2]\)存在,它们首先应该是合法堆的堆顶。当然空集合与单元素集合都被视作合法的堆。如果我们从最末母节点\([\lfloor \frac{A.size-1}{2}\rfloor]\)开始自底向上(bottom-up)堆化,合法堆\(H\)规模将从最末母节点所领堆扩充到整个数组\(A\),从而实现原地的元素插入(位置更新),其具体过程见伪代码2。
显然,由于进行了\(O(n)\)次堆化调用,那么堆构建的时间复杂度为\(O(n\lg n)\)。这么判断堆构建的时间正确但不够紧确。实际上,我们可以通过更仔细的运算证明堆构建的时间复杂度为\(O(n)\)。
伪代码2:堆构建
变量说明:A \(\rightarrow\) 无序数组
3. 基本操作
| 操作 | 描述 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| TOP() | 获取堆顶元素 | \(\Theta(1)\) | \(\Theta(1)\) |
| PUSH(value) | 添加新元素 | \(O(\lg n)\) | \(\Theta(1)\) |
| POP() | 弹出堆顶元素 | \(O(\lg n)\) | \(\Theta(1)\) |
| SIZE() | 询问当前堆大小 | \(\Theta(1)\) | \(\Theta(1)\) |
其中POP操作过程如图4[2:3]与伪代码3,PUSH(value)操作过程如伪代码4。
伪代码3:堆顶弹出
变量说明:H \(\rightarrow\) 堆数组;H.size\(\rightarrow\) 堆大小

伪代码4:堆压入
变量说明:H \(\rightarrow\) 堆数组;H.size\(\rightarrow\) 堆大小;H.capacity \(\rightarrow\) 堆容量;value \(\rightarrow\) 添加值
4. C++实现
template <class T, size_t N> class Heap {
T H[N]; // N means capacity
size_t len = N;
size_t left, right;
void _heapfy(auto i) {
while (right = i + 1 << 1, left = right - 1, left < len) {
auto largest = H[i] < H[left] ? left : i;
largest = right < len && H[largest] < H[right] ? right : largest;
if (largest == i)
break;
std::swap(H[i], H[largest]);
i = largest;
}
}
public:
Heap(T *A) {
std::memcpy(H, A, N * sizeof(T));
auto i = N >> 1;
do {
_heapfy(--i);
} while (i > 0);
}
constexpr auto top() const noexcept { return *H; }
inline void pop() noexcept {
if (size()) {
std::swap(*H, H[--len]);
_heapfy(0);
}
}
inline void push(auto val) noexcept {
if (len < N) {
H[len] = val;
auto i = len;
++len;
while (i) {
auto parent = i - 1 >> 1;
if (H[parent] >= H[i])
break;
std::swap(H[parent], H[i]);
i = parent;
}
}
}
constexpr auto size() const noexcept { return len; }
};
此外,C++标准库提供了priority_queue直接使用。
5. Rust实现
impl<T: Clone + std::cmp::PartialOrd> Heap<T> {
fn _heapfy(&mut self, mut i: usize) {
let mut right = i + 1 << 1;
let mut left = right - 1;
while left < self.size {
let mut largest = if self.H[i] < self.H[left] { left } else { i };
largest = if right < self.size && self.H[largest] < self.H[right] { right } else { largest };
if largest == i { break; }
self.H.swap(i, largest);
i = largest;
right = i + 1 << 1;
left = right - 1;
}
}
pub fn new() -> Self { Self { size: 0, capacity: 0, H: vec![] } }
pub fn from(mut self, A: Vec<T>) -> Self {
self.size = A.len();
self.capacity = self.size;
self.H = A;
let mut i = self.size >> 1;
while i > 0 {
i -= 1;
self._heapfy(i);
}
self
}
pub fn top(&self) -> Option<T> { if self.size > 0 { Some(self.H[0].clone()) } else { None } }
pub fn pop(&mut self) {
if self.size > 0 {
self.size -= 1;
self.H.swap(0, self.size);
self._heapfy(0);
}
}
pub fn push(&mut self, val: T) {
if self.size < self.capacity {
self.H[self.size] = val;
let mut i = self.size;
self.size += 1;
while i > 0 {
let parent = i - 1 >> 1;
if self.H[parent] >= self.H[i] { break; }
self.H.swap(i, parent);
i = parent;
}
}
}
pub fn len(&self) -> usize { self.size }
}
pub struct Heap<T> {
H: Vec<T>,
size: usize,
capacity: usize,
}
此外,Rust标准库提供了BinaryHeap直接使用。
6. 自我测试
伪代码实践:
- 寻找数据流S的中位数C。
LeetCode选荐:
- Kth Largest Element in a Stream
- Network Delay Time
- Minimize Deviation in Array
- Furthest Building You Can Reach
- Minimum Number of Refueling Stops
让每一天足够精致,期待与您的再次相遇! ^_^

浙公网安备 33010602011771号