优先队列(Java版)
引入优先队列
说明:
优先队列是一种抽象数据类型,它是一种排序的机制,它有两个核心操作:找出键值最大(优先级最高)的元素、插入新的元素,效果就是他在维护一个动态的队列。
可以收集一些元素,并快速取出键值最大的元素,对其操作后移出队列,然后再收集更多的元素,再处理当前键值最大的元素,如此这般。
比如我们有一台能够运行多个程序的计算机。计算机通过给每个应用一个优先级属性,将应用根据优先级进行排列,计算机总是处理下一个优先级最高的元素。
泛型优先队列的API
优先队列最重要的操作是删除最大元素和插入元素。
优先队列的初级实现
数组实现(无序)
?思想:
我们维护一个数组,因为不考虑数组顺序,所以我们的插入算法就很简单了。
对于查找最大值,我们利用了选择排序,在找到最大值后,将其与最后一个元素交换,并使长度-1.
? 只给出最简单核心实现步骤:
package queueDemo; public class QueueANO<T extends Comparable<T>> { private T[] array; private int n; public QueueANO(int capacity) { array=(T[]) new Comparable[capacity]; n=0; } ........ public void insert(T t) { array[n]=t;n++; } public T delMax() { int max=0; for(int i=1;i<n;i++) //找出最大元素 { if(less(max,i)) max=i; } exch(max,n-1); //将最大元素交换到最后 n--; //长度-1 return array[n]; } }
数组实现(有序)
?思想:
由于我们维护一个有序数组,所以每次插入元素的时候都要给他找到一个合适位置,来保证数组有序性,删除操作就会很简单了。
?代码:
public class OrderArrayPriorityQueue <Key extends Comparable<Key>>{ private Key[] pq; // elements private int n; // number of elements public OrderArrayPriorityQueue(int capacity) { pq = (Key[]) (new Comparable[capacity]); n = 0; } public boolean isEmpty() { return n == 0; } public int size() { return n; } public Key delMax() { return pq[--n]; } public void insert(Key key) { int i = n-1; while (i >= 0 && less(key, pq[i])) { pq[i+1] = pq[i]; i--; } pq[i+1] = key; n++; } private boolean less(Key v, Key w) { return v.compareTo(w) < 0; } public static void main(String[] args) { OrderArrayPriorityQueue<String> pq = new OrderArrayPriorityQueue<String>(10); pq.insert("this"); pq.insert("is"); pq.insert("a"); pq.insert("test"); while (!pq.isEmpty()) System.out.println(pq.delMax()); } }
堆的定义
说明:
二叉堆能够很好的实现优先队列的基本操作,二叉堆就是一颗二叉树,但是是按一种特定的组织结构排列。
即在二叉堆中每一个元素都要保证大于等于另外两个特定位置的元素(子节点)。
图示:
说明:
这是一个堆有序的二叉树。所谓堆有序就是一颗二叉树的每个节点都大于等于它的两个子节点。
二叉堆表示法:
?我们来组织一种堆有序的二叉树,这种有序的结构便于我们实现了优先队列。
我们可以使用指针来表示,但是这并不是最方便的。通过观察二叉有序堆,我们会发现它是一种完全二叉树,并且完全二叉树可以用数组来表示。
用数组实现二叉有序堆:
具体方法就是将二叉树的节点按照层序顺序放入数组中,根节点位置在1,它的子节点位置在2,3.依次类推。
?两条重要的性质:
1.在一个二叉堆中,位置为K的节点的父节点的位置为|_K/2_|,而它的两个子节点位置为2K和2K+1
2.一颗大小为N的完全二叉树的高度为|_LgN_|
用堆实现优先队列
由下至上的堆有序化
?说明:
如果堆的有序化因为某个节点X变得比它的父节点更大而打破,我们需要交换它和它的父节点来修复堆,但是可能交换后X还是很大,所以我们需要X一次次的它的祖先节点进行比较,直到找打它最合适的位置。
根据二叉堆的性质,我们不难发现只要记住位置为K的节点的父节点为 |_K/2_|,一切都很简单了。
?图示:
?代码
private void swim(int k) { while (k > 1 && less(k/2,k)) { exch(k/2, k); k = k/2; }}
由上至下的堆有序化
?说明:
如果堆的有序话因为某个节点X变得比它的两个子节点或其一更小而打破,我们需要交换它和它的子节点中较大的节点来修复堆,但是可能交换后X还是很小,所以我们需要X一次次的它的子节点进行比较并交换,直到找打它最合适的位置。
?图示:
?代码
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; } }
基于堆的优先序列
?思路:
我们每次插入元素到数组尾,这样可能会破坏堆的有序化,所以我们对其进行上浮操作。
当我们取出并删除最大元素的时候,第一个位置不能空着,我们的做法是将让最后一个元素移到第一个,然后进行下沉操作。
?这样子我们就可以给出完整代码
class MaxPQ<Key extends Comparable<Key>> { private Key[] pq; private int N = 0; public MaxPQ(int maxN) { pq = (Key[]) new Comparable[maxN + 1]; } public static void main(String[] args) { MaxPQ<Integer> maxPQ = new MaxPQ<Integer>(10); for(int i = 0; i < 10; i++) { maxPQ.insert((int)(Math.random() * 10 + 1)); } while(!maxPQ.isEmpty()) { System.out.println(maxPQ.delMax()); } } public int size() { return N; } public boolean isEmpty() { return N == 0; } public void insert(Key v) { pq[++N] = v; swim(N); } public Key delMax() { Key max = pq[1]; exch(1,N--); pq[N + 1] = null; sink(1); return max; } private boolean less(int i, int j) { return pq[i].compareTo(pq[j]) < 0; } private void exch(int i, int j) { Key temp = pq[i]; pq[i] = pq[j]; pq[j] = temp; } 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; } } private void swim(int k) { while (k > 1 && less(k/2,k)) { exch(k/2, k); k = k/2; } } }