无图无真相, 先上个图:
![最大堆](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 9;
9 -- 7;
}
)
上图就是一颗特殊的二叉树, 著名的堆; 在C++, Java等语言中又叫优先队列.
堆的基本性质:
- 堆分为最大堆和最小堆, 它们主要的差异就是: 最大堆是每个节点比它的每个子女节点大(或相等), 最小堆是每个节点比它的每个子女节点小(或相等); 本文以最大堆为例来介绍堆;
- 首先它是一颗完全二叉树, 也就是说: 除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点(如上图所示, 只有7的右边位置是缺少节点的);
- 要么根节点没有左子女节点, 要么它比左子女节点大;
- 要么根节点没有右子女节点, 要么它比右子女节点大;
- 它的左子树也要符合这样的定义;
- 它的右子树也要符合这样的定义;
- 父节点和子女节点之间有严格大小关系. 但是, 如果两个节点之间, 任意一个节点都不是另外一个节点的祖先节点, 那么它们之间的大小关系是不确定的.
堆的基本操作:
- 插入新元素;
- 删除堆顶元素;
一些约定:
- 动态扩容: 由于本人能力有限, 在模仿java的ArrayList等的扩容策略时发现, 跟c++的vector对比效率太低, 故该功能就交给vector来实现了;
- 由于堆是一颗完全二叉树, 所以我们可以用数组模拟, 数组下标为0的节点不用, 根节点下标为1, 它的左子女下标为2, 右子女下标为3. 整体上可以这样描述: 下标为i的节点, 它的左子女节点下标为i * 2, 右子女下标为i * 2 + 1, 父节点下标为i / 2(取整), 下面是一张7个节点的堆的下标图:
![堆的下标](http://g.gravizo.com/g?
graph G {
label = "(节点内的数字代表节点所在数组的下标, 不是节点的值)"
1 -- 2;
1 -- 3;
2 --4;
2 --5
3 -- 6;
3 -- 7;
}
)
代码实现
- 先看成员参数:
vector<T> data; //节点信息载体
int _size = 0; //堆的大小
static const int MIN_SIZE = 15; //堆最小的长度
static const int MAX_SIZE = 1 << 29; //堆最大的长度
- 两个构造方法:
PriorityQueue() {
ensureCapacity(MIN_SIZE);
}
PriorityQueue(int size) {
ensureCapacity(max(MIN_SIZE, size));
}
它们都调用了:
void ensureCapacity(int size) {
assert(size < MAX_SIZE && size >= 0);
// only data[1, data.size()-1] available
data.resize(size + 1);
}
这边稍微解释一下: 原本data的下标使用范围是[0, data.size()-1], 但是下标0弃用, 所以需要size+1长度的data才能存放下size个节点, vector::resize(size)的意思是, 将数组长度重置为size.
- 两个简单的方法:
void clear() {
_size = 0;
}
int size() {
return _size;
}
清空堆的时候不需要多余操作(如果是Java实现, 则需要将data里的每个对象置空, 方便GC), 只要将_size置为0即可; 获取堆长度时返回_size可以了.
- push(T t):
void push(T t) {
ensureCapacity(++_size);
data[_size] = t;
siftUp(_size);
}
它调用了siftUp(index):
void siftUp(int index) {
while (index > 1) {
int parentIndex = index >> 1;
if (data[parentIndex] < data[index]) {
swap(data[parentIndex], data[index]);
index = parentIndex;
} else {
break;
}
}
}
向上调整策略: 当堆新加入一个元素时, 我们先将它放在最后一层的从左往右数的第一个空位(如果最后一层满了, 则放在下一层的最左边的位置). 这时候, 它暂时不满足堆的定义, 我们通过调整, 不断跟父节点比较, 如果父节点比它小, 就跟父节点交换位置, 并向上递归调整, 直到父节点不小于它为止, 下面是一个简单的演示:
初始时的堆:
![初始堆](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
}
)
插入16, 由于第一个空位是6的右子女位置, 所以加入之后:
![中间状态1](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 6;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
6 -- 16[label = "6 < 16" color=yellow];
16[color=red];
}
)
由于16的父节点6比它小, 所以要交换位置, 这时候树变成了:
![中间状态2](http://g.gravizo.com/g?
graph G {
20 -- 15;
15 -- 10;
15 -- 16[label="15 < 16", color=yellow];
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
16 -- 2;
16 -- 6;
16[color=red];
}
)
调整之后, 16的父节点是15, 还是比它小, 16继续跟父节点交换位置:
![调整完成后的堆](http://g.gravizo.com/g?
graph G {
20 -- 16;
16 -- 10;
16 -- 15;
20 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
15 -- 6;
}
)
调整之后, 父节点为20, 已经比它大了, 至此, 调整结束, 这颗二叉树又已经符合堆的定义了.
- 取堆顶元素:
T top() {
assert(_size > 0);
return data[1];
}
很简单, 堆顶元素所处数组的下标永远是1;
- 删除堆顶元素:
void pop() {
assert(_size > 0);
if (--_size > 0) {
data[1] = data[_size + 1];
siftDown(1);
}
}
它调用了siftDown(index):
void siftDown(int index) {
while (true) {
int leftChildIndex = index << 1;
int rightChildIndex = index << 1 | 1;
if (leftChildIndex > _size) {
break;
}
//find max child node
int maxChildIndex = leftChildIndex;
if (rightChildIndex <= _size && data[leftChildIndex] < data[rightChildIndex]) {
maxChildIndex = rightChildIndex;
}
if (data[index] < data[maxChildIndex]) {
swap(data[index], data[maxChildIndex]);
index = maxChildIndex;
} else {
break;
}
}
}
向下调整策略: 当删除了堆顶元素之后, 若当前堆非空, 则将最右边的节点放到堆顶位置, 这时也暂时不满足了堆的定义. 我们将不断向下调整, 每次找到这个节点的子女节点中最大的跟其比较, 如果比它大, 则交换位置, 然后继续递归下去调整, 直到它的所有子女节点都不比它大为止. 以上图中刚刚插入新节点的堆为例:
将最右边的节点(6)放入堆顶位置:
![中间状态1](http://g.gravizo.com/g?
graph G {
6 -- 16[label="16 > 6"];
16 -- 10;
16 -- 15;
6 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
6[color=red]
16[color=yellow]
}
)
由于6的最大的子女节点为16, 比它大, 交换位置:
![中间状态2](http://g.gravizo.com/g?
graph G {
16 -- 6;
6 -- 10;
6 -- 15[label="15 > 6"];
16 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
15 -- 2;
6[color=red]
15[color=yellow]
}
)
这时候, 6的最大子女节点为15, 还是比它大, 继续调整:
![t调整完整后的堆](http://g.gravizo.com/g?
graph G {
16 -- 15;
15 -- 10;
15 -- 6;
16 -- 11;
11 -- 7;
11 -- 8;
10 -- 9;
10 -- 4;
6 -- 2;
}
)
最终, 这颗二叉树又满足堆的定义了. 思考: 为什么向下调整时要选择较大的节点?
最后, 用一个SGU上的题目来验证自己写的堆.