优先队列(堆)

  1. 1. 6.1 模型
  2. 2. 6.2 一些简单实现
  3. 3. 6.3 二叉堆
    1. 3.1. 6.3.1 结构性质
    2. 3.2. 6.3.2 堆序性
    3. 3.3. 6.3.3 堆的基本操作
  4. 4. 6.4 完整代码

《数据结构与算法分析Java语言描述》第六章6.1节读书笔记

优先队列又称为堆,是用来处理具有优先级的元素的数据结构。

6.1 模型

优先队列分为两种。
一种是小顶堆实现的优先队列,在堆顶是最小的元素,每次可以直接访问最小的元素。
另一种是大顶堆实现的优先队列,在堆顶是最大元素。每次可以直接访问最大的元素。

对于小顶堆而言,至少支持两种操作,insert插入,将元素按优先级插入到指定位置,deletemin删除,删除堆顶的最小元素。insert操作等价于入队,deletemin等价于出队。(由堆的优先级,可以引出堆排序)。

后续以小顶堆为例。

6.2 一些简单实现

使用有序链表来实现,表头的元素最小,则每次deleteMin的时间复杂度是O(1),对于N个元素,则是O(N)。对于每次insert操作,由于需要遍历找到插入的位置,时间复杂度是O(N),对于N个元素,则是O(N2)O(N^2)

使用二叉查找树实现,二叉查找树两种操作实现皆是O(logN)O(logN),对于N个元素,实现是O(NlogN)O(NlogN)。不过二叉查找树的实现较为复杂,而且许多支持的操作我们并不需要。将实现二叉堆,来支持这个操作,并且时间复杂度都是O(NlogN)O(NlogN)

6.3 二叉堆

堆一般指二叉堆。堆具有结构性堆序性。对堆的一次操作,可能会破坏这两个性质中的一个。因此对于堆一次操作,必须还原这两个性质,才算作一次操作终止。

6.3.1 结构性质

堆是一棵完全二叉树,除最底层外,每一层都被填满。容易证明一棵高为h的完全二叉树有2h2h+11个节点2^h 到 2^{h+1}-1个节点。即节点个数N=2h2h+11个节点,因此,高度N = 2^h 或 2^{h+1}-1个节点,因此,高度h=O(logN)h = O(logN)

堆性质:堆大多都由数组实现,而不是链表。对于数组上的任一位置i的元素,其左儿子在2i,右儿子在2i+1。它的父亲在i/2上(java的整除的性质,对于节点i,无论他是左儿子,还是右儿子,整除得到的结果都是i/2)。

由于是用数组实现,当我们遍历所有元素时,可以直接通过计算,来得知访问的下标。例如下标1的左儿子在下标2,右儿子在下标3。按顺序访问。

**本节我们图示皆以二叉树展示,但是实现仍然是数组。**实现的是小顶堆。

注意:我们的堆数组,下标为0的位置是不使用的,因为2i = 0,得到的结果和父节点是一样的位置。所以有效节点从1开始。(线段树也是如此,使用数组实现树,且0的位置不使用)。

6.3.2 堆序性

对于小顶堆,根节点应该是最小的元素。同时,对于二叉堆的任意子树,也是一个小顶堆,子树的根节点也应该是子树的最小元素。

因此,对于一个小顶堆,对于任意节点i,如果其有左右子节点,则应该满足,num[i] <= num[2i],num[i] <= num[2i + 1]。

根据堆序性,最小的元素总是可以以O(1)的时间在堆的根处找到。实现为findMin。

6.3.3 堆的基本操作

插入(insert)

为了将一个元素X插入到堆中,可以先在下一个可用的位置创建一个空节点。如果X可以放入该节点而不破化堆序性,那么插入完成。否则空节点应该朝着根的方向冒一步。继续该过程,知道X能被放入空节点为止。如下图,为了插入14,先创建空节点,插入14不满足,将空节点上冒,知道将14插入正确的位置。

尝试插入14:创建一个空节点,再将空节点上冒

尝试插入14:后续

这种策略一般叫做上滤,新元素在堆中上滤知道找出正确的位置。使用如下代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void insert(T x) {
// currentSize:被使用的数组大小
if (currentSize == array.length - 1) {
// 下标0不用,所以需要减去1
// 2倍扩容,同时可以使用的空间为2的倍数个
this.enlargeArray(array.length * 2 + 1);
}
// 使用的位置加1(currentSize + 1后为即将尝试使用的下标)
currentSize++;
// 空节点下标
int hole = currentSize;
// 先将x存到下标0的位置(该位置不使用),hole = hole / 2:意思是访问hole的父节点,空节点上滤
for (array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2) {
// 理解的为x在空节点hole处,
// x.compareTo(array[hole / 2]) < 0 : x小于父节点的元素。将父节点的值下浮到空节点(等同于将空节点上冒到父节点)
array[hole] = array[hole / 2];
}
array[hole] = x;
}

如果要插入的元素是最小的元素,那么他将一直被推向顶端。

deleteMin(删除最小元素)

最小的元素在顶端,即下标为1处。删除最小的元素,要在根节点(下标1处)建立一个空穴。将空穴中的两个字节的的较小值移入空穴,则空穴下移动一层。重复这一过程。最后再将堆中的最大元素(如图31)被移入空穴。

在根处建穴

deleteMin_接下来两步

deleteMin_最后两步

代码实现

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
public T deleteMin() {
// 堆为空
if (currentSize == 0) {
throw new RuntimeException();
}
// 最小的元素在根处
T minItem = array[1];
// 将最大的元素放置到根处,相当于删掉了根处的元素,同时使用抽象出来的方法下浮最大值。
array[1] = array[currentSize];
currentSize -= 1;
// 将节点1处的值下浮
percolateDown(1);
return minItem;
}

private void percolateDown(int hole) {
int child;
// 要下浮的元素的值
T temp = array[hole];
for (; hole * 2 <= currentSize; hole = child) {
// 左儿子的下标
child = hole * 2;
// 如果左儿子不是最后一个元素,并且右儿子小于左儿子,
if (child != currentSize && array[child + 1].compareTo(array[child]) < 0) {
// 要移动的则是较小的右儿子
child++;
}
if (array[child].compareTo(temp) < 0) {
// 如果子节点的值比要下浮的值小,那么子节点向上移动(相当于下浮节点下浮)
array[hole] = array[child];
} else {
// 否则终止
break;
}
}
// temp(最大值)移动到空穴位置,完成下浮
array[hole] = temp;
}

建堆

通过初始构造集合建堆,可以考虑建堆使用N个insert方法来建堆,每个insert的平均时间是O(1),而最坏的时间是O(lgN)。因此总的平均时间为O(N),总的最坏时间为O(NlgN)。

一般的算法是将N项以任意顺序放入堆中,保持结构性,此时percolateDown(i)从节点i下滤,使用如下的程序创建一棵堆序的树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 使用数组建堆
*/
public BinaryHeap(T[ ] items ) {
currentSize = items.length;
// 开辟空间数组
array = (T[]) new Comparable[ ( currentSize + 2 ) * 11 / 10 ];
int i = 1;
// 初始化堆数组
for(T item : items ) {
array[i++] = item;
}
buildHeap();
}

private void buildHeap() {
for (int i = currentSize / 2; i > 0; i--) {
// 循环遍历来下浮建堆
percolateDown(i);
}
}

6.4 完整代码

堆完整代码
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
public class BinaryHeap<T extends Comparable<? super T>> {

private static final int DEFAULT_CAPACITY = 10;

/**
* 当前堆中的元素数量
*/
private int currentSize;
/**
* 存储堆的数组
*/
private T[] array;

/**
* Construct the binary heap.
*/
public BinaryHeap() {
// 调用本类的另一个构造方法
this(DEFAULT_CAPACITY);
}

public BinaryHeap(int capacity) {
currentSize = 0;
// 堆数组的0的位置是不使用的,所以长度需要加1
array = (T[]) new Comparable[capacity + 1];
}

/**
* 使用数组建堆
*/
public BinaryHeap(T[] items) {
currentSize = items.length;
// 开辟空间数组
array = (T[]) new Comparable[(currentSize + 2) * 11 / 10];
int i = 1;
// 初始化堆数组
for (T item : items) {
array[i++] = item;
}
buildHeap();
}

private void buildHeap() {
for (int i = currentSize / 2; i > 0; i--) {
// 循环遍历来xia'fu'shen'sh
percolateDown(i);
}
}

public void insert(T x) {
if (currentSize == array.length - 1) {
// 下标0不用,所以需要减去1
// 2倍扩容,同时可以使用的空间为2的倍数个
this.enlargeArray(array.length * 2 + 1);
}
// 将X上冒到正确的位置
currentSize++;
// 空节点下标
int hole = currentSize;
// 先将x存到下标0的位置(该位置不使用),hole = hole / 2:意思是访问hole的父节点,空节点上冒
for (array[0] = x; x.compareTo(array[hole / 2]) < 0; hole /= 2) {
// 理解的为x在空节点hole处,
// x.compareTo(array[hole / 2]) < 0 : x小于父节点的元素。将父节点的值下浮到空节点(等同于将空节点上冒到父节点)
array[hole] = array[hole / 2];
}
array[hole] = x;
}

public T deleteMin() {
// 堆为空
if (currentSize == 0) {
throw new RuntimeException();
}
// 最小的元素在根处
T minItem = findMin();
// 将最大的元素放置到根处,相当于删掉了根处的元素,同时使用抽象出来的方法下浮最大值。
array[1] = array[currentSize];
currentSize -= 1;
// 将节点1处的值下浮
percolateDown(1);
return minItem;
}

public T findMin() {
if (isEmpty()) {
throw new RuntimeException();
}
return array[1];
}

public boolean isEmpty() {
return currentSize == 0;
}

private void percolateDown(int hole) {
int child;
// 要下浮的元素的值
T temp = array[hole];
for (; hole * 2 <= currentSize; hole = child) {
// 左儿子的下标
child = hole * 2;
// 如果左儿子不是最后一个元素,并且右儿子小于左儿子,
if (child != currentSize && array[child + 1].compareTo(array[child]) < 0) {
// 要移动的则是较小的右儿子
child++;
}
if (array[child].compareTo(temp) < 0) {
// 如果子节点的值比要下浮的值小,那么子节点向上移动(相当于下浮节点下浮)
array[hole] = array[child];
} else {
// 否则终止
break;
}
}
// temp移动到空穴位置
array[hole] = temp;
}

/**
* 扩容
*/
private void enlargeArray(int newSize) {
T[] old = array;
array = (T[]) new Comparable[newSize];
for (int i = 0; i < old.length; i++) {
array[i] = old[i];
}
}

public static void main(String[] args) {
int numItems = 10000;
BinaryHeap<Integer> h = new BinaryHeap<>();
int i = 37;

for (i = 37; i != 0; i = (i + 37) % numItems) {
h.insert(i);
}
for (i = 1; i < numItems; i++) {
System.out.println(h.findMin());
h.deleteMin();
}
}
}