发布于 

如何实现一个最小堆

本文介绍如何使用 C++ 实现一个通用的、线程安全的最小堆

顺便一提,写此文的时候突然想到了多年以前(16、17年)盛行的 Mooc,当时国内有很多类似的慕课网站,也有很多优秀的课程,我的基础数据结构和算法就是一位 ACM 铜牌的老师教的,至今受用,但是现在好像很少看到此类网站了,还是比较可惜的。

进入正题,最小堆本身是一个完全二叉树,保证每一个节点都不会大于其左右子节点,要实现一个最小堆的两个核心函数是 sift_upsift_down,用于调整最小堆结构以符合定义。

首先给出 MinHeap 的基本定义和成员变量:

1
2
3
4
5
6
template <std::three_way_comparable T>
class MinHeap
{
std::shared_mutex mtx;
std::vector<T> vec;
};

这里 vec 用于保存最小堆的各个节点(完全二叉树很适合用数组保存),而 std::three_way_comparable 则是对容器元素类型的 concept 约束,要求元素类型必须是可比较的,毕竟不可比较的话也就没有“最小”的概念了。

sift_downsift_up 是相反的一组用于调整堆的操作,sift_down 用于将一个元素向下调整至满足最小堆的条件,sift_up 则相反,由此可以知道对于任何一个完全二叉数我们只需要上到下执行 sift_down 或者从下到上执行 sift_up 就可以使得堆中的每一个节点都满足最小堆的条件

1
2
3
  5      sift_down(5)      2   
/ \ ==============> / \
2 6 sift_up(2) 5 6

可以看到不管是对 2 所在的节点执行 sift_up 还是对 5 所在的节点执行 sift_down 都能完成我们想要的效果。

接着在给出二者的定义前先给出对最小堆的一些基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public:
void push (T val)
{
std::unique_lock lock {mtx};
vec.push_back(val);
sift_up(vec.size()-1);
}
void pop()
{
assert(!vec.empty());
std::unique_lock lock {mtx};
vec[0] = vec.back();
vec.pop_back();
sift_down(0);
}
const T& top()
{
assert(!vec.empty());
std::shared_lock lock {mtx};
return vec.front();
}

pushpop 就是对堆调整以符合最小堆定义的过程,因为每次只有一个节点发生变化(新增或者删除),所以只需要对涉及的节点调整即可

下面给出 sift_upsift_down 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void sift_up(size_t index)
{
if (index == 0) return;
size_t parent = (index - 1) / 2;
if (vec[index] < vec[parent]) {
std::swap(vec[index], vec[parent]);
sift_up(parent);
}
}
void sift_down(size_t index)
{
if (index >= vec.size()) return;
size_t left = index * 2 + 1;
size_t right = index * 2 + 2;
std::optional<size_t> swap_to;
if (left < vec.size() && vec[left] < vec[index]) {
swap_to = left;
}
if (right < vec.size() && vec[right] < vec[index]) {
if (!swap_to || vec[right] < vec[swap_to.value()]) {
swap_to = right;
}
}
if (swap_to) {
std::swap(vec[index], vec[swap_to.value()]);
sift_down(swap_to.value());
}
}

应该还是比较好理解的,因为只有一个节点不(一定)满足最小堆条件,因此对于 sift_up 只需要判断自身和其父节点的大小关系并交换即可,而对于 sift_down 则是需要选出两个子节点中更小的那个并与之交换。

注意这里的 sift_upsift_down 都是一个递归的过程,直到抵达堆的根节点或者叶子节点

以上我们就已经实现了一个通用的、线程安全的最小堆,这里给出堆的完整定义(为了方便使用包含 dump):

完整的 MinHeap 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
template <std::three_way_comparable T>
class MinHeap
{
std::shared_mutex mtx;
std::vector<T> vec;
public:
void push (T val)
{
std::unique_lock lock {mtx};
vec.push_back(val);
sift_up(vec.size()-1);
}
void pop()
{
assert(!vec.empty());
std::unique_lock lock {mtx};
vec[0] = vec.back();
vec.pop_back();
sift_down(0);
}
const T& top()
{
assert(!vec.empty());
std::shared_lock lock {mtx};
return vec.front();
}
void sift_up(size_t index)
{
if (index == 0) return;
size_t parent = (index - 1) / 2;
if (vec[index] < vec[parent]) {
std::swap(vec[index], vec[parent]);
sift_up(parent);
}
}
void sift_down(size_t index)
{
if (index >= vec.size()) return;
size_t left = index * 2 + 1;
size_t right = index * 2 + 2;
std::optional<size_t> swap_to;
if (left < vec.size() && vec[left] < vec[index]) {
swap_to = left;
}
if (right < vec.size() && vec[right] < vec[index]) {
if (!swap_to || vec[right] < vec[swap_to.value()]) {
swap_to = right;
}
}
if (swap_to) {
std::swap(vec[index], vec[swap_to.value()]);
sift_down(swap_to.value());
}
}
void dump()
{
std::shared_lock lock {mtx};
for (auto iter = vec.begin(); iter != vec.end(); ++iter) {
std::cout << *iter;
if (std::distance(iter, vec.end()) != 1) {
std::cout << ", ";
} else {
std::cout << std::endl;
}
}
}
};

用法示例以及运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto main(void) -> int {
auto heap = MinHeap<int> {};
heap.push(177);
heap.push(3);
heap.push(110);
heap.push(19);
heap.pop();
heap.push(21);
heap.push(112);
heap.push(18);
std::cout << "heap's top is " << heap.top() << std::endl;
heap.dump();
heap.pop();
heap.pop();
std::cout << "heap's top is " << heap.top() << std::endl;
heap.dump();
return 0;
}

运行结果:

1
2
3
4
heap's top is 18
18, 21, 19, 177, 112, 110
heap's top is 21
21, 112, 110, 177

可以看到不论我们以任何顺序插入和删除元素都能够保证 MinHeap 始终是一个最小堆