优先队列
许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或是不一定要一次就将他们排序。很多情况下我们会收集一些元素,处理当前键值最大的元素,然后再收集更多元素,再处理当前键值最大的元素,如此这般。
在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这种数据类型叫做优先队列。
API
interface MaxPQ<Key extends Comparable<Key>>{
void insert(Key k); //向优先队列中插入一个元素
Key max(); //返回最大元素
Key delMax(); //删除并返回最大元素
boolean isEmpty(); //返回队列是否为空
int size(); //返回队列中的元素个数
}
实现
- 有序数组:在insert中进行排序
- 无序数组:在delMax时查找最大元素并删除。
- 堆:在insert中构造堆,在delMax时需要恢复堆状态。
对比
堆
定义:当一颗二叉树的每个节点都大于等于它的两个子节点时,它被称为堆有序
使用数组保存堆:在一个堆中,位置k的节点的位置为k/2,而它的两个子节点的位置则分别为2k和2k+1.这样在不使用指针的情况下我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层就令k等于k/2,向下一层则令k等于2k或2k+1。如图
堆的上浮和下沉
使堆维持有序状态的操作叫做堆有序化。
由下至上的堆有序化(上浮)
如果堆的有序状态因为某个节点变得比它的父节点更大而被打破,那么我们需要通过交换它和它的父节点来修复堆。将这样的节点不断的向上移动直到遇到一个更大的父节点。
private void swim(int k){
while (k > 1 && less(k/2,k)){
exch(k/2,k);
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;
}
}
基于堆的优先队列
代码:
abstract class MaxPQ<Key extends Comparable<Key>>{
private Key[] pq;
private int N = 0;
public MaxPQ(int maxN){
pq = (Key[]) new Comparable[maxN+1];
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public void insert(Key k){
pq[++N] = k;
swim(N);
}
public Key delMax(){
Key max = pq[1];
exch(1,N--);
pq[N+1] = null;
sink(1);
return max;
}
//实现见前面代码
protected abstract boolean less(int i,int j);
protected abstract void exch(int i,int j);
protected abstract void swim(int k);
protected abstract void sink(int k);
}
复杂度
对于一个含有N个元素的基于堆的优先队列,插入元素操作只需要不超过(lgN+1)次比较,删除最大元素的操作需要不超过2lgN次比较。
堆的拓展
- 多叉堆
- 根据输入数据长度调整数组大小
- 索引优先队列:实现如下
下边代码参考自博客数据结构实现之最小索引优先队列
/**
* 使用二叉堆实现的索引最小优先队列
* class IndexMinPQ是一个支持泛型的索引优先队列。
* IndexMinPQ支持普通的insert、delete-the-minimum、delete以及change-the-key方法。
* 用户可以使用队列中的0到maxN-1号索引执行删除和修改方法。
* IndexMinPQ支持获取队列最小元素,队列最小元素索引操作。
* IndexMinPQ支持迭代器迭代所有插入的索引号。
*
* IndexMinPQ的实现使用二叉堆。
* The <em>insert</em>, <em>delete-the-minimum</em>, <em>delete</em>,
* <em>change-key</em>, <em>decrease-key</em>, and <em>increase-key</em>
* 操作时间复杂度为O(lgN).
* The <em>is-empty</em>, <em>size</em>, <em>min-index</em>, <em>min-key</em>, and <em>key-of</em>
* operations 时间复杂度为O(1).
* @author xwq
*
* @param <Key>
*/
public class IndexMinPQ <Key extends Comparable<Key>> implements Iterable<Integer> {
private int maxN; //索引优先队列中元素的最大个数
private int N; //当前索引优先队列中元素的个数
private int[] pq;//使用一级索引的二叉堆
private int[] qp;//pq的对称映射 qp[pq[i]] = pq[qp[i]] = i,用于映射key索引对应pq二叉堆中的位置
private Key[] keys; //keys[i] = priority of i
/**
* 初始化索引区间为(0到maxN-1)的空索引优先队列
* @param capacity
*/
public IndexMinPQ(int capacity) {
if(capacity <= 0)
throw new IllegalArgumentException();
maxN = capacity;
N = 0;
pq = new int[capacity+1];
qp = new int[capacity+1];
keys = (Key[])new Comparable[capacity+1];
//初始每个索引都没用过
for(int i=0;i<=maxN;i++)
qp[i] = -1;
}
/**
* 如果队列为空返回true
* @return 如果队列为空返回true,否则返回false
*/
public boolean isEmpty() {
return N==0;
}
/**
* 队列的当前元素个数
* @return 队列的当前元素个数
*/
public int size() {
return N;
}
/**
* 判断优先队列是否已存在索引i
* @param i 索引i
* @return 如果索引i之前已插入,返回true,否则false
*/
public boolean contains(int i) {
return qp[i] != -1;
}
/**
* 返回最小值的索引号
* @return 最小值的索引号
*/
public int minIndex() {
if(isEmpty())
throw new NoSuchElementException("IndexMinPQ underflow.");
return pq[1];
}
/**
* 返回优先队列最小值,即二叉堆根节点
* @return 优先队列最小值
*/
public Key minKey() {
if(isEmpty())
throw new NoSuchElementException("IndexMinPQ underflow.");
return keys[pq[1]];
}
/**
* 索引i对应优先队列中的键值
* @param i 索引i
* @return 索引i对应的键值
*/
public Key keyOf(int i) {
if(!contains(i))
throw new NoSuchElementException("IndexMinPQ has not contains index i");
return keys[i];
}
/**
* 将索引i与键值key关联
* @param i 索引i
* @param key 键值key
*/
public void insert(int i,Key key) {
if(i<0 || i>=maxN)
throw new IllegalArgumentException("index i out of boundary.");
if(contains(i))
throw new IllegalArgumentException("index i has allocted");
N++;
qp[i] = N;
pq[N] = i;//pq,qp互为映射
keys[i] = key;
adjustUp(N);
}
/**
* 删除最小键值并返回其对应的索引
* @return 最小键值对应的索引
*/
public int delMin() {
if(isEmpty())
throw new NoSuchElementException("IndexMinPQ underflow.");
int min = minIndex();
delete(min);
return min;
}
/**
* 删除索引i以及其对应的键值
* @param i 待删除的索引i
*/
public void delete(int i){
if(!contains(i))
throw new NoSuchElementException("IndexMinPQ has not contains index i");
int pqi = qp[i];
swap(pqi,N--);
adjustUp(pqi);
adjustDown(pqi);
qp[i] = -1; //删除
keys[i] = null; //便于垃圾收集
pq[N+1] = -1; //不是必须,但是加上便于理解
}
/**
* 改变与索引i关联的键值
* @param i 待改变键值的索引
* @param key 改变后的键值
*/
public void changeKey(int i,Key key) {
if(!contains(i))
throw new NoSuchElementException("IndexMinPQ has not contains index i");
if(keys[i].compareTo(key) == 0)
throw new IllegalArgumentException("argument key equpal to the original value.");
if(key.compareTo(keys[i]) > 0)
increaseKey(i,key);//原键值增加
else
decreaseKey(i,key);//原键值减小
}
/**
* 减小与索引i关联的键值到给定的新键值
* @param i 与待减小的键值关联的索引
* @param key 新键值
*/
public void decreaseKey(int i,Key key) {
if(!contains(i))
throw new NoSuchElementException("IndexMinPQ has not contains index i");
if(key.compareTo(keys[i]) > 0)
throw new IllegalArgumentException("argument key more than the original value.");
keys[i] = key;
int pqi = qp[i];
adjustUp(pqi);
adjustDown(pqi);
}
/**
* 增加与索引i关联的键值到给定的新键值
* @param i 与待增加的键值关联的索引
* @param key 新键值
*/
public void increaseKey(int i,Key key) {
if(!contains(i))
throw new NoSuchElementException("IndexMinPQ has not contains index i");
if(key.compareTo(keys[i])<0)
throw new IllegalArgumentException("argument key less than the original value");
keys[i] = key;
int pqi = qp[i];
adjustUp(pqi);
adjustDown(pqi);
}
/***************************************************************************
* General helper functions.
***************************************************************************/
/**
* 交换一级索引值,以及其对称映射中的值
* @param i 使用一级索引的二叉堆的索引i
* @param j 使用一级索引的二叉堆的索引j
*/
private void swap(int i,int j) {
int t = pq[i]; pq[i] = pq[j]; pq[j] = t;
int qpi = pq[i];
int qpj = pq[j];
qp[qpi] = i;
qp[qpj] = j;
}
/**
* 判断键值的大小关系
* @param i 使用一级索引的二叉堆的索引i
* @param j 使用一级索引的二叉堆的索引j
* @return keys[ki] < keys[kj] 返回true,否则返回false
*/
private boolean less(int i,int j) {
int ki = pq[i];
int kj = pq[j];
return keys[ki].compareTo(keys[kj]) < 0;
}
/***************************************************************************
* Heap helper functions.
***************************************************************************/
/**
* 向下调整最小二叉堆
* @param i 一级索引的二叉堆的索引i,pq数组的数组位置
*/
private void adjustDown(int i) {
while(2*i <= N) {
int l = 2*i;
while(l<N && less(l+1,l))
l++;
swap(l,i);
i = l;
}
}
/**
* 向上调整最小二叉堆
* @param i 一级索引的二叉堆的索引i,pq数组的数组位置
*/
private void adjustUp(int i) {
while(i>1) {
int p = i/2;
if(less(p,i))
break;
swap(p,i);
i = p;
}
}
@Override
public Iterator<Integer> iterator() {
return new HeapIterator();
}
/***************************************************************************
* Iterators.
***************************************************************************/
/**
* Returns an iterator that iterates over the keys on the
* priority queue in ascending order.
* The iterator doesn't implement <tt>remove()</tt> since it's optional.
*
* @return an iterator that iterates over the keys in ascending order
*/
private class HeapIterator implements Iterator<Integer> {
// create a new pq
IndexMinPQ<Key> copy;
// add all elements to copy of heap
// takes linear time since already in heap order so no keys move
public HeapIterator() {
copy = new IndexMinPQ<Key>(maxN);
for(int i=1;i<=N;i++) {
int ki = pq[i];
Key key = keys[ki];
copy.insert(ki, key);
}
}
@Override
public boolean hasNext() {
return !copy.isEmpty();
}
@Override
public Integer next() {
if(!hasNext())
throw new NoSuchElementException("IndexMinPQ underflow.");
return copy.delMin();
}
@Override
public void remove() {
throw new UnsupportedOperationException("unsupported remove operation.");
}
}
/**
* Unit tests the <tt>IndexMinPQ</tt> data type.
*/
public static void main(String[] args) {
// insert a bunch of strings
String[] strings = { "it", "was", "the", "best", "of", "times", "it", "was", "the", "worst" };
IndexMinPQ<String> pq = new IndexMinPQ<String>(strings.length);
for (int i = 0; i < strings.length; i++) {
pq.insert(i, strings[i]);
}
StdOut.print();
// print each key using the iterator
for (Integer i : pq) {
StdOut.println(i + " " + pq.keyOf(i));
}
StdOut.println();
// increase or decrease the key
for (int i = 0; i < strings.length; i++) {
if (StdRandom.uniform() < 0.5)
pq.increaseKey(i, strings[i] + strings[i]);
else
pq.decreaseKey(i, strings[i].substring(0, 1));
}
// delete and print each key
while (!pq.isEmpty()) {
String key = pq.minKey();
int i = pq.delMin();
StdOut.println(i + " " + key);
}
StdOut.println();
// reinsert the same strings
for (int i = 0; i < strings.length; i++) {
pq.insert(i, strings[i]);
}
// delete them in random order
int[] perm = new int[strings.length];
for (int i = 0; i < strings.length; i++)
perm[i] = i;
StdRandom.shuffle(perm);
for (int i = 0; i < perm.length; i++) {
String key = pq.keyOf(perm[i]);
pq.delete(perm[i]);
StdOut.println(perm[i] + " " + key);
}
}
}
堆排序
有两种方式:
- 可以从左到右遍历数组,用swim()保证指针左侧的所有元素已经是一颗对有序的完全树
- 可以从右到左使用sink()函数构造最小子堆,然后找到每个已经有序的子堆的父节点构造下一层堆,这样递归的构造直到到达最终的根结点。
sink()的方法效率很高,因为我们事实上只需要遍历整个数组的一半即可使整个堆有序。举个例子假设我们现在要排序16个元素,先将他们填装到长度为17的数组里(0位为空)第一次构造的根结点为5-8,第二次的根结点为3-4,第三次的根结点为2,第四次的根结点为1,堆构造完成(这里的每次指的是构造同一高度的堆)。这时我们调用sink()的次数正好是元素个数的一半。然后交换根结点和数组最右边未排好序的节点,sink堆,如此直到堆的长度减小为1,整个数组即有序。
public static void sort(Comparable[] a){
int N = a.length;
for(int k = N/2 ; k >= 1 ; k--)
sink(a,k,N);
while (N > 1){
exch(a,1,N--);
sink(a,1,N);
}
}
如图为堆排序轨迹:
复杂度
用下沉操作由N个元素构造堆只需要少于2N次比较以及少于N次交换。将N个元素排序,堆排序只需少于(2NlgN+2N)次比较(以及一半次树的交换)。
缺点
在现代系统的许多应用中很少用,因为无法利用缓存。数组元素很少和相邻的其他元素进行比较,因此缓存未命中的次数远远高于大多数比较都在相邻元素间进行的算法,如快速排序,归并排序,甚至希尔排序。
优先队列在java中的实现
见java.util.PriorityQueue