优先队列
优先队列: 满足收集一些元素,处理当前键值最大的元素.然后再收集更多的元素,再处理当前键值最大的元素
需求.
支持两种操作:删除最大元素 和 插入元素
API
最重要的操作就是 删除最大元素delMax() 和插入元素insert().
public class MaxPQ <Key extends Comparable<Key>> {
// 创建一个优先队列
public MaxPQ()
// 创建一个初始容量为 max 的优先队列
public MaxPQ(int max)
// 用 a[] 中的元素创建一个优先队列
public MaxPQ(Key[] a)
// 向优先队列中插入一个元素
public void Insert(Key v)
// 返回最大元素
public Key max()
// 删除并返回最大元素
public Key delMax()
// 返回队列是否为空
public boolean isEmpty()
// 返回优先队列中的元素个数
public int size()
}
二叉堆的定义
数据结构二叉堆,能够很好地实现优先队列的基本操作。
在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素。
定义: 当一棵二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序 。
相应地,在堆有序的二叉树中,每个结点都小于等于它的父结点(如果有的话)。
从任意结点向上,我们都能得到一列非递减的元素;
从任意结点向下,我们都能得到一列非递增的元素。
根结点是堆有序的二叉树中的最大结点。
二叉堆表示法
如果用指针来表示堆有序的二叉树,那么每个元素都需要三个指针来找到它的上下结点(父结点和两个子结点各需要一个)。
如果使用完全二叉树,表达就会变得特别方便。
要画出这样一棵完全二叉树,可以先定下根结点,然后一层一层地由上向下、从左至右,在每个结点的下方连接两个更小的结点,直至将 N 个结点全部连接完毕。
完全二叉树只用数组而不需要指针就可以表示。
具体方法就是将二叉树的结点按照层级顺序 放入数组中,根结点在位置 1,它的子结点在位置 2 和 3,而子结点的子结点则分别在位置 4、5、6 和 7,以此类推。
定义: 二叉堆 是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级储存(不使用数组的第一个位置)。
在一个二叉堆中,位置 k 的结点的父结点的位置为 k/2,而它的两个子结点的位置则分别为 2k 和 2k+1 。
这样在不使用指针的情况下,可以通过计算数组的索引在树中上下移动:从 a[k] 向上一层就令 k 等于 k/2 ,向下一层则令 k 等于 2k 或 2k+1 。
堆的算法
用长度为 N+1 的私有数组 pq[] 来表示一个大小为 N 的堆,我们不会使用 pq[0] ,堆元素放在 pq[1] 至 pq[N] 中。
堆的有序化
堆的操作会首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。
在有序化的过程中会遇到两种情况。
当某个结点的优先级上升(或是在堆底加入一个新的元素)时,我们需要由下至上 恢复堆的顺序。
当某个结点的优先级下降(例如,将根结点替换为一个较小的元素)时,需要由上至下 恢复堆的顺序。
由下至上的堆有序化(上浮)
// 由下至上的堆有序化(上浮)的实现
private void swim(int k)
{
// k的父节点是 k/2.若父节点值小于当前节点
while (k > 1 && less(k/2, k))
{
// 交换父节点和当前节点
exch(k/2, k);
// 继续比较当前层和上一层
k = k/2;
}
}
如果堆的有序状态因为某个结点变得比它的父结点更大而被打破,那么我们就需要通过交换它和它的父结点来修复堆。
交换后,这个结点比它的两个子结点都大(一个是曾经的父结点,另一个比它更小,因为它是曾经父结点的子结点),但这个结点仍然可能比它现在的父结点更大。
可以一遍遍地用同样的办法恢复秩序,将这个结点不断向上移动直到我们遇到了一个更大的父结点。
由上至下的堆有序化(下沉)
如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小了而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆。
交换可能会在子结点处继续打破堆的有序状态,因此我们需要不断地用相同的方式将其修复,将结点向下移动直到它的子结点都比它更小或是到达了堆的底部。
private void sink(int k) { // 节点k
while (2 * k <= n) { // 2 * k <= n 表示还有下一层. k节点的两个子结点的位置则分别为 2k 和 2k+1
int j = 2 * k; // 左子节点
if (j < n && less(j, j + 1)) { // 当前k节点的左子节点j的值小于右子节点j+1的值.右子节点更大,则需要上浮成父节点
j++;
}
if (!less(k, j)) { // 当前节点k的值不小于左子节点j的值,则跳出循环,不需要交换
break;
}
exch(k, j); // 交换k和子节点j处的值
k = j;
}
}
基于堆的优先队列
插入元素: 将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置。
删除最大元素: 从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。
// 基于堆的优先队列
public class MaxPriorityQueue<Key extends Comparable<Key>>
优先队列由一个基于堆的完全二叉树表示,存储于数组 pq[1…N] 中,pq[0] 没有使用。
在 insert() 中,将 N 加一并把新元素添加在数组最后,然后用 swim() 恢复堆的秩序。
在 delMax() 中,从 pq[1] 中得到需要返回的元素,然后将 pq[N] 移动到 pq[1] ,将 N 减一并用 sink() 恢复堆的秩序。
/**
* 基于堆实现的优先队列
*/
@Test
public void testMaxPQ() {
Integer[] arr = {8, 15, 4, 3, 11, 10, 12, 1, 9, 13, 6, 5};
MaxPriorityQueue<Integer> pq = new MaxPriorityQueue<>();
for (Integer i : arr) {
// 插入到 优先队列
pq.insert(i);
}
// 从优先队列中移除最大元素
if (!pq.isEmpty()) {
StdOut.print(pq.delMax() + " ");
}
}
优先队列插入操作
/** 将key插入到优先队列中
* Adds a new key to this priority queue.
*
* @param x the new key to add to this priority queue
*/
public void insert(Key x) {
// double size of array if necessary 自动2倍扩容
if (n >= pq.length - 1) {
resize(2 * pq.length);
}
// add x, and percolate it up to maintain heap invariant
pq[++n] = x; // 添加元素到n+1位置
swim(n);
assert isMaxHeap();
}
// 由下至上的堆有序化(上浮)的实现
private void swim(int k) {
while (k > 1 && less(k / 2, k)) { // k的父节点是 k/2.若父节点值小于当前节点
exch(k, k / 2); // 交换父节点和当前节点
k = k / 2; // 继续比较当前层和上一层
}
}
插入:
插入8. n=1
插入15.n=2. 15大于父节点8,交换 8,15.结果: 15,8
插入4.n=3. 4的父节点下标k/2为1.4小于8,不交换.结果:15,8,4
插入3.n=4. 父节点下标2,值8.不交换.结果:15,8,4,3
插入11.n=5. 父节点下标2,值8. 11>8. 交换,结果:15,11,4,3,8. 11<15.不交换.结果 15,11,4,3,8
插入10.n=6. 父节点下标3,值4. 10>4. 交换,结果:15,11,10,3,8,4. 10<15.不交换. 结果 15,11,10,3,8,4
插入12.n=7. 父节点下标3.值10. 12>10. 交换,结果: 15,11,12,3,8,4,10
插入1.n=8. 父节点下标4.值3. 1<3,不交换.结果: 15,11,12,3,8,4,10,1
插入9.n=9. 父节点下标4.值3. 9>3,交换.结果: 15,11,12,9,8,4,10,1,3. k=9/2=4. 9<11,不交换. 结果 15,11,12,9,8,4,10,1,3
插入13.n=10.父节点下标5.值8. 13>8,交换.结果: 15,11,12,9,13,4,10,1,3,8. k=5/2=2. 13>11.交换,结果 15,13,12,9,11,4,10,1,3,8
插入6.n=11.父节点下标5.值11. 6 < 11,不交换,结果: 15,13,12,9,11,4,10,1,3,8,6
插入5.n=12.父节点下标6.值4. 4 >4,交换,结果: 15,13,12,9,11,5,10,1,3,8,6,4
优先队列获取删除最大元素操作
/**
* 删除并返回优先队列中的最大元素
*
* 从 pq[1] 中得到需要返回的元素,然后将 pq[N] 移动到 pq[1] ,将 N 减一并用 sink() 恢复堆的秩序
* Removes and returns a largest key on this priority queue.
*
* @return a largest key on this priority queue
* @throws NoSuchElementException if this priority queue is empty
*/
public Key delMax() {
if (isEmpty()) {
throw new NoSuchElementException("Priority queue underflow");
}
// 从根结点得到最大元素
Key max = pq[1];
// 将其和最后一个结点交换,交换后,元素数量n减1
exch(1, n--);
// 恢复堆的有序性.将第一个元素下沉到合适的位置
sink(1);
// help gc
pq[n + 1] = null; // to avoid loiterig and help with garbage collection
// 堆自动扩容
if ((n > 0) && (n == (pq.length - 1) / 4)) {
resize(pq.length / 2);
}
assert isMaxHeap();
return max;
}
private void sink(int k) { // 节点k
while (2 * k <= n) { // 2 * k <= n 表示还有下一层. k节点的两个子结点的位置则分别为 2k 和 2k+1
int j = 2 * k; // 左子节点
if (j < n && less(j, j + 1)) { // 当前k节点的左子节点j的值小于右子节点j+1的值.右子节点更大,则需要上浮成父节点
j++;
}
if (!less(k, j)) { // 当前节点k的值不小于左子节点j的值,则跳出循环,不需要交换
break;
}
exch(k, j); // 交换k和子节点j处的值
k = j;
}
}
获取删除最大元素
1 = {Integer@807} 4
2 = {Integer@816} 13
3 = {Integer@812} 12
4 = {Integer@815} 9
5 = {Integer@810} 11
6 = {Integer@818} 5
7 = {Integer@811} 10
8 = {Integer@813} 1
9 = {Integer@808} 3
10 = {Integer@802} 8
11 = {Integer@817} 6
12 = {Integer@805} 15