优先队列是在实际工程中被普遍应用的一种数据结构,无论是在操做系统的进程调度中,仍是在相关的图算法好比Prim算法和Dijkstra算法中,咱们均可以看到优先队列的身影,本文咱们就来分析一下优先队列的实现原理。java
以操做系统的进程调度为例,好比咱们在使用手机的过程当中,手机分配给来电的优先级都会比其它程序高,在这个业务场景中,咱们不要求全部元素所有有序,由于咱们须要处理的只是当前键值最大的元素(优先级最高的进程)。在这种状况下,咱们须要实现的只是删除最大的元素(获取优先级最高的进程)和插入新的元素(插入新的进程),这种数据结构就叫作优先队列。git
咱们先来定义一个优先队列,下面咱们将使用pq[]
来保存相关的元素,在构造函数中能够指定堆的初始化大小,若是不指定初始化大小值,默认初始化值为1。p.s: 在下面咱们会实现相关的resize()
方法用来动态调整数组的大小。github
public class MaxPQ<Key> implements Iterable<Key> { private Key[] pq; // store items at indices 1 to n private int n; // number of items on priority queue private Comparator<Key> comparator; // optional Comparator /** * Initializes an empty priority queue with the given initial capacity. * * @param initCapacity the initial capacity of this priority queue */ public MaxPQ(int initCapacity) { pq = (Key[]) new Object[initCapacity + 1]; n = 0; } /** * Initializes an empty priority queue. */ public MaxPQ() { this(1); } }
在正式进入优先队列分析以前,咱们有必要先了解一下对于堆的相关操做。咱们定义当一棵二叉树的每一个结点都要大于等于它的两个子结点的时候,称这棵二叉树堆有序。以下图就是一棵典型的堆有序的彻底二叉树。算法
对了保证堆有序,对于堆咱们要对它进行上浮和下沉操做,咱们先来实现两个经常使用的工具方法,其中less()
用于比较两个元素的大小,exch()
用于交换数组的两个元素:数组
private boolean less(int i, int j) { if (comparator == null) { return ((Comparable<Key>) pq[i]).compareTo(pq[j]) < 0; } else { return comparator.compare(pq[i], pq[j]) < 0; } } private void exch(int i, int j) { Key swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; }
根据下图咱们首先来分析一下上浮操做,以swim(5)
为例子,咱们来看一下上浮的过程。对于堆咱们进行上浮的目的是保持堆有序性,即一个结点的值大于它的子结点的值,因此咱们将a[5]
和它的父结点a[2]
相比较,若是它大于父结点的值,咱们就交换二者,而后继续swim(2)
。数据结构
具体的实现代码以下:less
private void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k, k/2); k = k/2; } }
根据下图咱们来分析一下下沉操做,以sink(2)
为例子,咱们先将结点a[2]
和它两个子结点中较小的结点相比较,若是小于子结点,咱们就交换二者,而后继续sink(5)
。函数
具体的实现代码以下:工具
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; } }
咱们来分析一下插入一个元素的过程,若是咱们要在堆中新插入一个元素S
的话,首先咱们默认将这个元素插入到数组中pq[++n]
中(数组是从1开始计数的)。当咱们插入S
后,打破了堆的有序性,因此咱们采用上浮操做来维持堆的有序性,当上浮操做结束以后,咱们依然能够保证根结点的元素是数组中最大的元素。this
接下来咱们来看一下删除最大元素的过程,咱们首先将最大的元素a[1]
和a[n]
交换,而后咱们删除最大元素a[n]
,这个时候堆的有序性已经被打破了,因此咱们继续经过下沉操做来从新维持堆的有序性,保持根结点元素是全部元素中最大的元素。
插入的实现代码以下:
/** * 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 if (n >= pq.length - 1) resize(2 * pq.length); // add x, and percolate it up to maintain heap invariant pq[++n] = x; swim(n); assert isMaxHeap(); }
删除的实现代码以下:
/** * Removes a maximum key and returns its associated index. * * @return an index associated with a maximum key * @throws NoSuchElementException if this priority queue is empty */ public Key delMax() { if (isEmpty()) throw new NoSuchElementException("Priority queue underflow"); Key max = pq[1]; exch(1, n); n--; sink(1); 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; }
上面咱们在insert()
过程当中用到了resize()
函数,它用于动态数组的大小,具体的实现代码以下:
// helper function to double the size of the heap array private void resize(int capacity) { assert capacity > n; Key[] temp = (Key[]) new Object[capacity]; for (int i = 1; i <= n; i++) { temp[i] = pq[i]; } pq = temp; } public boolean isEmpty() { return n == 0; }
而isMaxHeap()
则用于判断当前数组是否知足堆有序原则,这在debug的时候很是的有用,具体的实现代码以下:
// is pq[1..N] a max heap? private boolean isMaxHeap() { return isMaxHeap(1); } // is subtree of pq[1..n] rooted at k a max heap? private boolean isMaxHeap(int k) { if (k > n) return true; int left = 2*k; int right = 2*k + 1; if (left <= n && less(k, left)) return false; if (right <= n && less(k, right)) return false; return isMaxHeap(left) && isMaxHeap(right); }
到此咱们的优先队列已经差很少完成了,注意咱们上面实现了Iterable<Key>
接口,因此咱们来实现iterator()
方法:
/** * Returns an iterator that iterates over the keys on this priority queue * in descending order. * The iterator doesn't implement remove() since it's optional. * * @return an iterator that iterates over the keys in descending order */ public Iterator<Key> iterator() { return new HeapIterator(); } private class HeapIterator implements Iterator<Key> { // create a new pq private MaxPQ<Key> copy; // add all items to copy of heap // takes linear time since already in heap order so no keys move public HeapIterator() { if (comparator == null) copy = new MaxPQ<Key>(size()); else copy = new MaxPQ<Key>(size(), comparator); for (int i = 1; i <= n; i++) copy.insert(pq[i]); } public boolean hasNext() { return !copy.isEmpty(); } public void remove() { throw new UnsupportedOperationException(); } public Key next() { if (!hasNext()) throw new NoSuchElementException(); return copy.delMax(); } }
将上面的优先队列稍微作一下改进,咱们即可以实现堆排序,即对pq[]
中的元素进行排序。对于堆排序的具体实现,下面咱们分为两个步骤:
堆排序的实现代码很是的简短,咱们首先来看一下具体的代码实现,而后咱们再具体分析它的实现原理:
/** * Rearranges the array in ascending order, using the natural order. * @param pq the array to be sorted */ 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) { exch(pq, 1, n--); sink(pq, 1, n); } }
首先咱们来看一下堆的构造过程(下图中的左图)。咱们采用的方法是从右至左用sink()
方法构造子堆。咱们只须要扫描数组中的一半元素,即5, 4, 3, 2, 1。这样经过这几个步骤,咱们能够获得一个堆有序的数组,即每一个结点的大小都大于它的两个结点,并使最大元素位于数组的开头。
接下来咱们来分析一下下沉排序的实现(下图中的右图),这里咱们采起的方法是每次都将最大的元素删除,而后从新经过sink()
来维持堆有序,这样每一次sink()
操做咱们均可以的到数组中最大的元素。
GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site
本文为做者原创,转载请与开头明显处声明博客出处:)