计算机

二叉堆

二叉堆是一棵完全二叉树,树上每个节点都定义了一个权值,并且保证每个节点的权值都小于等于(或大于等于)其子节点的权值。为了区分,我们将权值大于等于子节点权值的堆称为大根堆,反之称为小根堆。

C++ STL 中的 priority_queue 实质上就是一个大根堆。

原理

存储

因为二叉堆是满二叉树,所以可以直接使用数组存储。从 1 开始存储时,节点 i 的左孩子下标为 2i,右孩子下标为 2i + 1。

于是存储只需在一个数组上储存数据,并维护堆大小(堆最大元素下标)即可。

以下使用结构体 HeapNode 代表一个堆节点。

const size_t MAXN = 1e5 + 5;
struct HeapNode {
    int data;
    friend bool operator<(HeapNode u, HeapNode v)
    {
        return u.data < v.data;
    }
} nodes[MAXN];
size_t n_nodes = 0;

操作

以下操作均以小根堆为例。

取最小元

显然堆顶就是最小元,直接返回即可。

HeapNode top()
{
    return nodes[1];
}

插入

插入时,直接将新节点插入到堆末尾。

这样做可能会破坏堆性质,所以插入后需要向上调整:将插入的节点与其父节点节点比较,若节点的权值比父节点小,则交换两个节点。重复以上操作,直至堆的序关系正确。

代码实现时,循环比较直至不等式成立或者 u 已经是堆顶即可。

void push(HeapNode node)
{
	n_nodes++;
	nodes[n_nodes] = node;
	// Push up
	for (size_t u = n_nodes; u != 1; u /= 2)
	{
		size_t v = u / 2;
		if (nodes[u] < nodes[v])
		{
			auto node = nodes[v];
			nodes[v] = nodes[u];
			nodes[u] = node;
		}
		else
		{
			return;
		}
	}
}

删除堆顶

删除堆顶时,类似于插入,首先交换堆顶与堆末节点,然后直接删除堆末尾节点即可。调整后的堆顶可能破坏了堆性质,所以需要向下调整。

向下调整:每次选择节点的两个子节点权值较大者,然后交换两个节点位置,直到堆的序性质已满足或者节点已经没有子节点。

实现时,因为可能出现堆有左子节点而没有右子节点的情况,先选出左、右子节点较大者(顺便判断右子节点是否存在),然后再比较原节点和选出的子节点。

void pop()
{
	nodes[1] = nodes[n];
	n--;

	// push down
	size_t u;
	for (u = 1; 2 * u <= n;)
	{
		size_t v = 2 * u;
		if (v + 1 <= n && nodes[v + 1] < nodes[v])
		{
			v++;
		}
		if (nodes[v] < nodes[u])
		{
			auto node = nodes[u];
			nodes[u] = nodes[v];
			nodes[v] = node;
			u = v;
		}
		else
		{
			return;
		}
	}
}

修改

在小根堆中,如果需要增加或减少一个节点的权重,直接修改节点即可。修改完后,如果增加了权重,则向下调整;如果减少了权重,则向上调整。

但已知值并不能直接寻找到堆节点,通常需要数组或 Hash 表辅助记录。

建堆

朴素的建堆可以选择逐个元素插入,复杂度是 O(n \log n)。但是,如果已知全部数据,还有更快速的 O(n) 建堆方法。

快速建堆的方法是:从堆的第 n 个节点,从后往前插入堆节点,同时逐个向下调整。这种情况下每个节点 i 至多向下调整 \log_2 n - \log_2 i 次,总操作次数为:

\sum_{i = 1}^n \log_2 n - \log_2 i = n \log_2 n - (\log_2 1 + \cdots + \log_2 n) \le n - 1.

于是时间复杂度为 O(n)。

应用

离散事件模拟

使用小根堆存储事件和事件发生的时刻,每次取出下一个发生事件(即发生时刻最小的事件),这就是离散时间模拟(Discrete Event Simulation,DES)。

DES 问题非常适合使用优先队列存储,以时刻作为小根堆的排序依据,则小根堆每次取出的就是下一个事件;同时事件可能会导致新的事件产生,只需直接将新的事件插入小根堆即可。

例题:

BEYOND THE ARTICLE

读完了,有什么想法?

阅读已保存的问答示例,并循着引用回到原文。

查看公开问答示例
— 本文完 — 返回文章列表