很多数据,我们并不需要保证他们全部有序,或是一次就把他们全部排序完成。我们可能很多时候仅仅是需要知道这些数据中最大的那一个,或者是最大的或者最小的前k个数据,对于这样的要求,如果我们还只是一味的全部对所有数据排序处理势必将浪费大量的时间,为了提高效率,我们可以通过使用定长的优先队列来实现此需求。
优先队列
顾名思义,就是队列中维护了元素的优先顺序;
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。
优先队列我们可以使用基于链表存储的形式进行构造,也可以采用数组进行存储的形式构造,在这里我们先采用数组存储进行实验,一个优先队列将能够完成创建,数据的插入,数据的删除,这也是它的最核心操作。下面以构建最大值优先队列为例子进行演示:
- MaxPQ类的api如下:
public class MaxPQ<key extends Comparable<key>>
MaxPQ(); // 创建一个优先队列;
MaxPQ(int max); // 创建一个容量为max的优先队列;
MaxPQ(Key[] a); // 用以a[]的元素创建一个优先队列;
boolean insert(Key v); // 向优先队列中插入一个元素;
// Key max(); // 返回最大元素;
void delMax(); // 返回最大元素并从队列中删除;
boolean isEmpty(); // 返回是否为空;
// int size(); // 返回优先队列中元素个数;
优先队列的数组实现
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] keys; //存储最大堆的元素数组
private int size; //堆中的元素个数,动态改变的参数
/**
* 根据一个元素数组构造最大堆
* @param arr
*/
public MaxPQ(Key[] arr) {
keys = (Key[])new Comparable[arr.length + 1];
for(int i=1; i<=arr.length; i++)
keys[i] = arr[i-1]; //先将元素数组复制到keys中
size = keys.length-1;
for(int i=arr.length/2; i>=1; i--) //这一步可以使得构造最大堆的复杂度降低到O(N)
siftDown(i);
}
/**
* 测试函数
* @param args
*/
public static void main(String[] args) {
Integer[] arr = {1, 2, 0, 5, 3, 9, 6, 7, 10, 11};
MaxPQ<Integer> m = new MaxPQ<>(arr);
List<Integer> list = new ArrayList<>();
while(!m.isEmpty())
list.add(m.delMax());
System.out.println(list);
}
/**
* 判别最大堆是否为空的方法
* @return
*/
private boolean isEmpty() {
return size == 0;
}
/**
* 删除并返回堆顶最大值
* @return
*/
private Key delMax() {
Key temp = keys[1];
keys[1] = keys[size --];
siftDown(1);
return temp;
}
/**
* 从任意位置下沉的函数,所谓下沉,就是将该位置的元素和它的两个儿子的元素对比,看谁更“优先”,不优先的往后挪
* @param index 初始位置
*/
private void siftDown(int index) {
int i;
while(index * 2 <= size) {
//如果index的key值比左儿子小
if(keys[index].compareTo(keys[2 * index]) < 0)
i = 2 * index;
else
i = index;
if(index * 2 + 1 <= size) { //如果存在右儿子
if(keys[i].compareTo(keys[2*index+1]) < 0)
i = 2*index+1;
}
if(i != index) {
Key temp = keys[i];
keys[i] = keys[index];
keys[index] = temp;
index = i;
} else
break;
}
}
/**
* 插入元素的函数
* @param next
* @return
*/
private boolean insert(Key next) {
if(size == keys.length - 1) return false; //队列已满
keys[++ size] = next;
siftUp(size); //将最后插入的元素上浮
return true;
}
/**
* 从index位置的元素进行上浮操作,和父亲比较,优先的上浮直到不优先为止。
* @param index
*/
private void siftUp(int index) {
while(index != 1) {
if(keys[index].compareTo(keys[index/2]) > 0) {
Key temp = keys[index];
keys[index] = keys[index / 2];
keys[index / 2] = temp;
index = index / 2;
} else
break;
}
}
}
堆排序算法的实现
public static void main(String[] args) {
Random r = new Random();
int n = 10;
int[] a = new int[n+1];
for (int i=1; i<n+1; i++) {
a[i] = r.nextInt(19);
}
System.out.println(Arrays.toString(a));
int len = n / 2;
for (int i=len; i>=1; i--) {
sink(a, i, n);
}
System.out.println(Arrays.toString(a));
for (int i=1; i<=n; i++) {
int val = a[n+1-i];
a[n+1-i] = a[1];
a[1] = val;
sink(a, 1, n-i);
}
System.out.println(Arrays.toString(a));
}
private static void sink(int[] a, int i, int n) {
int index = i;
if (2*i <= n && a[i] < a[2*i])
index = 2*i;
if (2*i+1<= n && a[index] < a[2*i+1])
index = 2*i+1;
if (index == i) return;
int temp = a[i];
a[i] = a[index];
a[index] = temp;
sink(a, index, n);
}
- 附上PAT一道习题,更好的了解堆排序;
- 附上部分源码:
public static void main(String[] args) throws IOException {
Read.init(System.in);
int n = Read.nextInt();
int[] a = new int[n+1], b = new int[n+1];
for (int i=1; i<=n; i++) {
a[i] = Read.nextInt();
}
for (int i=1; i<=n; i++) {
b[i] = Read.nextInt();
}
int p = 2, q;
while (p <= n && b[p-1] <= b[p]) p++;
q = p;
while (p <= n && b[p] == a[p]) p++;
if (p == n+1) {
System.out.println("Insertion Sort");
Arrays.sort(b, 0, q+1);
} else {
System.out.println("Heap Sort");
p = n;
while (p > 1 && b[p] >= b[p-1]) p--;
int temp = b[1];
b[1] = b[p];
b[p] = temp;
sink(b, 1, p-1);
}
System.out.print(b[1]);
for (int i=2; i<=n; i++)
System.out.print(" " + b[i]);
}
private static void sink(int[] a, int i, int n) {
int index = i;
if (2*i <= n && a[i] < a[2*i])
index = 2*i;
if (2*i+1<= n && a[index] < a[2*i+1])
index = 2*i+1;
if (index == i) return;
int temp = a[i];
a[i] = a[index];
a[index] = temp;
sink(a, index, n);
}
总结
- 其实一颗二叉堆即一颗堆有序的树,这颗树即为一颗完全二叉树,笔者使用完数组建立成这颗二叉堆后觉得真的很简洁啊,想着使用链表存储二叉堆应该更方便把,但实际进行过程中,稍加思考就发现,这样做反而不如数组来的简单,试想,你的树节点root,当决定插入一个新的节点时(假设此时构建的是最大堆),插入值比根节点值小,它是选择左子树还是右子树插入呢?其实是都可以的,如果比根节点大,那么是让根节点放到插入节点的左子树还是右子树呢?其实也是都可以的,然后再去调整整棵树直到它成为一颗完全二叉树,这有点让你想起了什么?没错,整这么复杂,我直接使用排序树不就成了?而数组为什么省事?因为数组的连续和紧凑的特性非常适合存储完全二叉树。想到这里,也许你会想,真费劲,每次都得自己写一个优先队列,好麻烦,其实不用,Java的类库里就提供了这个类PriorityQueue-最小堆,查看源码就会发现,里面也是采用数组存储这颗二叉堆,而TreeMap也可以实现排序,它是基于红黑树实现的排序,得到的是完整排序。
- 所以当你在写最小生成树或者dijkstra算法时想使用优先队列时,直接使用类库即可,不必重新自己写,所谓它山之石,可以攻玉,不过如此把。