优先队列,堆与堆排序
1 优先队列
有时我们在处理有序元素时,并不一定要求他们全部有序. 很多情况下我们会收集一些元素, 处理当前最大的元素, 然后再收集更多元素, 再处理当前最大元素 …
这种情况下, 一个合适的数据结构一个支持两种操作 : 删除最大元素和 插入元素. 这种数据类型叫做 优先队列.
我们可以使用有序或无序的数组或链表实现.使用无序序列是解决这个问题的 惰性方法, 我们仅在必要的时候才会找出最大元素(比如在pop的时候).
而使用有序序列是解决问题的积极方法, 在插入insert元素时就保持列表有序.
对于上述的对优先队列的所有实现中 , 插入元素和删除最大元素这两个操作之一在最坏情况下需要线性时间完成:
- 有序数组插入元素是O(n)的, 删除是O(1)
- 无序数组插入元素是O(1)的, 删除是O(n)
而二叉堆能保证这两种操作能够更快地运行.
2 堆的定义与算法
2.1定义
二叉堆能够很好地实现优先队列的基本操作. 在二叉堆数组中,每个元素都要大于等于另外两个特定位置的元素.(默认大顶堆)
- 定义堆有序: 当一个二叉树的每个结点都大于等于它的两个子结点时, 被称为堆有序.
所以 不难得出根结点是堆有序的二叉树中的最大结点
2.2 二叉堆表示法
- 定义二叉堆是一组能够用堆有序的完全二叉树排序的元素, 并在数组中按层级储存(第一个位置不用)
以下二叉堆简称 堆 . 在一个堆中 , 位置k的 结点的父节点位置为 k/2 (整型类型, 小数不保留, 比如 5 的父节点在 2), 而它的两个子节点的位置分别在 2k 和 2k+1.
由此我们可以通过计算数组的索引在树中上下移动: a[k]向上一层就 k = k /2 ; 向左子树就 k = 2k 或 2k +1
2.3堆的一些算法
我们用长度为N + 1 的数组来保存一个大小为N的堆, 我们不使用a[0] , 元素全放在a[1] ~ a[N]中
- 辅助函数less() 和 exch()
/* a 为实现了Comparable的类型的堆 , i和j为数组下标 */
/* 交换下标为 i , j的两个元素 */
void exch(Comparable[] a , int i , int j ){
Comparable temp = a[i]; a[i] = a[j]; a[j] = temp;
}
/*判断下标为i的元素是否小于下标为j的函数*/
boolean less(Comparable[] a , int i , int j){
return (a[i].compareTo(a[j]) < 0) ;
}
-
由下至上的堆有序swim( )
若某结点比他的父节点还要大, 则他需要通过交换他和他的父节点来修复堆. 但交换后的父节点可能比父节点的父节点还大 , 所以还得一遍遍地向上恢复.
private void swin (Comparable[] a , int k , int N){ //N 初始表示最后一个元素下标 N = a.length - 1 , 也是堆中实际元素个数 while (k > 1 && less(a , k /2 , k)){ exch(a , k/2 , k); k /= 2; } }
-
由下至上的堆有序化sink( )
如果某结点比他的两个子节点或者其中之一还要小 , 则需要通过将他和他的子节点中的较大值进行交换. 但交换后依然可能比子节点的子节点还小, 所以也得一遍遍地向下恢复
private void sink(Comparable[] a , int k , int N){ /* 向下遍历, 但是只在前一半元素中考虑 */ while ( 2 * k <= N){ int j = 2 * k ; /* 如果他的右孩子比他大 , 而且也比左孩子大 , 则选择与右孩子进行交换 */ if( j < N && less(a , j, j + 1)) j ++; /* 如果此时k已经不小于他的孩子了 , 跳出循环 */ if( ! less(a ,k , j )) break; /* 进行交换 */ exch(a , k , j); k = j; } }
-
向堆中插入一个元素v , 并且调整堆保持堆有序
public void intsert(Comparable[] a , Comparable v , int N){ a[++N] = v; swim(N); }
-
删除堆中的最大元素,调整堆保持其有序
public Comparable delMax(Comparable[] a , int N ){ /* 得到最大元素 */ Comparable max = a[1]; /* 和最后一个元素进行交换, 交换完后将N减1 */ exch(a , 1 , N -- ); a[N + 1] = null; sink(a , 1 , a.length) }
2.4 堆排序
在上述构造了堆的数据结构后 , 堆排序就比较简单了.
堆排序主要是两个步骤:
-
将一个无序的数组a (还是默认a[0]不存值 )变得堆有序(成为一个堆) – 构造堆
-
将该最大元素与最后一个元素交换(交换后堆顶为最小元素) , 同时用sink将最小元素下沉, 用while对每个元素进行该步骤 – 交换+下沉排序
public void heapSort(Comparable[] a){ int N = a.length - 1; /* 对前一半的元素进行sink下沉 , 得到一个堆 */ for (int k = N / 2 ; K >= 1 ; k --) sink(a , k , N ); /* 对每个元素进行操作 */ while( N > 1){ /* 交换最大的元素与最后一个元素 , 此时最大元素跑到a[N] , 然后N-- , 表示当前a[N]排序完成, */ exch(a , 1 , N --); /* 上面的N--相当于从堆中拿出了排好的元素 , 现在把最小的元素沉下去, 当前堆顶又是当前最大值了 */ sink(a , 1 , N ); } }
附堆排序实现:
public void heapSort(Comparable[] a){
int N = a .length -1 ;
for(int k = N /2 ; k >= 1 ; k --)
sink(a , k , N);
while ( N > 1){
exch(a , 1 ,N -- );
sink(a , 1 ,N );
}
}
void exch(Comparable[] a , int i , int j ){
Comparable temp = a[i]; a[i] = a[j]; a[j] = temp;
}
boolean less(Comparable[] a , int i , int j){
return (a[i].compareTo(a[j]) < 0) ;
}
private void sink(Comparable[] a , int k , int N){
while ( 2 * k <= N){
int j = 2 * k ;
if( j < N && less(a , j, j + 1)) j ++;
if( ! less(a ,k , j )) break;
exch(a , k , j);
k = j;
}
}
private void swin (Comparable[] a , int k , int N){
while (k > 1 && less(a , k /2 , k)){
exch(a , k/2 , k);
k /= 2;
}
}
@Test
public void test(){
Integer[] a = {null , 33 ,5,78,23,4 , 354 ,78,114, 6};
for (int i = 1; i < a .length ; i ++ ) {
System.out.print( a[i] + " ");
}
System.out.println();
heapSort(a);
for (int i = 1; i < a .length ; i ++ ) {
System.out.print( a[i] + " ");
}
}