优先队列
优先队列是提供从一堆数据中获取最大(当然最小也是一样的)数据能力的数据类型。
下文要用的less 、 exch方法的源码:
private static boolean less(Comparable v, Comparable w) {
return (v.compareTo(w) < 0);
}
private static void exch(Comparable[] a, int i, int j) {
Comparable swap = a[i];
a[i] = a[j];
a[j] = swap;
}
二叉堆
- 二叉堆是实现优先队列的一种非常有效率的方式;它基于二叉树的数据结构,且每一个节点都比它的子节点大:
所以它的根节点就是最大的节点。 - 二叉堆一般使用数组来存储数据,它按照节点的层来排序:首先第一层(根节点),然后是第二层(根节点的子节点),然后是第三层(第二层节点的子节点),以此类推:
注意,图中第0个位置被保留下来了,这是有特殊含义的。因为这样一来,对于一个位置为k的节点,它的父节点的位置为k/2,子节点的位置为 2k 和 2k+1,计算非常方便。 - 可以通过两个基本动作使一个数组达到堆排序状态:
a. 上浮操作;当一个节点比它的父节点大时,可以将它与它的父节点交换来满足二叉堆的数据结构,当交换后依然比它的父节点大时,可以递归执行这个过程。代码如下:
b. 下沉操作;当一个节点比它两个或一个子节点小时,将它与最大的子节点交换;同样这个过程也可以是递归的。代码如下:private void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k, k/2); k = k/2; } }
private void sink(int k) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; //选取两个子节点中大的那个 if (!less(k, j)) break; exch(k, j); k = j; } }
基于堆的优先队列
-
初始化;使用已有数据构造一个优选队列时,需要先将数据变的堆有序的:
public MaxPQ(Key[] keys) { n = keys.length; pq = (Key[]) new Object[keys.length + 1]; for (int i = 0; i < n; i++) pq[i+1] = keys[i]; for (int k = n/2; k >= 1; k--) sink(k); //通过下沉操作使得每一个根节点都比子节点大 }
-
插入新数据;插入新数据时,先将数据放入数组最后,然后通过swim操作将它交换到正确的位置:
public void insert(Key x) { // double size of array if necessary if (n == pq.length - 1) resize(2 * pq.length); //如果数组长度小于所需长度的两倍则重新分配数组资源 // add x, and percolate it up to maintain heap invariant pq[++n] = x; //把新元素添加到数组末尾 swim(n); //上浮到正确位置 }
-
删除最大值(优先队列最主要的功用);优先队列的最大值就是它的根节点,也就是数组的第一个位置,所以返回这个值就好了,但是删除根节点之后,数组将不再堆有序了,所以需要做一些操作使得它重新有序:
public Key delMax() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); Key max = pq[1]; //取到最大值 exch(1, n--); //把最后一个位置的节点交换到第一个位置,这样能保证除了根节点,其他节点的相对位置不变,只需要将当前第一个位置的节点下沉到正确位置就好了 sink(1); pq[n+1] = null; //清除被交换过来的根节点 if ((n > 0) && (n == (pq.length - 1) / 4)) resize(pq.length / 2); //重新计算数组容量 return max; }
插入和删除操作的图示:
堆排序
所谓堆排序就是首先将数据构造成二叉堆,然后依次执行deleMax操作,从而得到一个有序的数组的排序方法;由之前的delMax方法可以看出,我们会交换最后一个节点和根节点(当前最大的节点),当我们交换后不清除被交换的根节点,只通过记录当前数组长度来把这个节点排除在优先队列之外,那么当我们连续调用deleMax方法直到优先队列的长度变为0时,我们就得到了一个元素从小到大排列的数组:
public static void sort(Comparable[] pq) {
int n = pq.length;
for (int k = n/2; k >= 1; k--) //首先构造二叉堆
sink(pq, k, n);
while (n > 1) { //循环执行不删除的delMax操作
exch(pq, 1, n--);
sink(pq, 1, n);
}
}
堆排序需要交换根节点所以它是不稳定的原地排序,它的时间复杂度是O( N l o g N NlogN NlogN),而空间复杂度是O(1)。
参考资料
- 算法(第四版)Robert Sedgewick 和 Kevin Wayne 著
- https://algs4.cs.princeton.edu/21elementary/ (文中所有代码来源)