第二章 排序(三) - 《算法》读书笔记
目录
第二章 排序(三)
2.4 优先队列
- 最重要的两种操作:删除最大元素和插入元素
- 基于二叉堆数据结构的有限队列,可以实现对数级别的操作
2.4.1 API
public class MaxPQ<Key extends Comparable<Key>>- 删除最大元素:
delMax() - 插入元素:
Insert()
2.4.3 堆的定义
当一颗二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序。
根结点是堆有序的二叉树中的最大结点。
二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级**存储(不使用数组的第一个位置)。
- 位置k的结点的父结点的位置为⌊k/2⌋,两个子结点的位置分别为2k和2k+1
一棵大小为N的完全二叉树的高度为⌊lgN⌋。
2.4.4 堆的算法
- 用长度为N+1的私有数组
pq[]来表示一个大小为N的堆 - 使用如下的比较和交换方法:
private boolean less(int i, int j){
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j){
Key t = pq[i]; pq[i] = pq[j]; pq[j] = t;
}
- 堆的有序化:堆的操作会首先进行一些简单的改动,打破堆的状态,然后在遍历堆并按照要求将堆的状态恢复
- 当某个结点的优先级上升(或是在堆底加入一个新的元素)时,我们需要由下至上恢复堆的顺序
- 当某个结点的优先级下降(例如将根结点替换为一个较小的元素时),我们需要由上至下恢复堆的顺序
2.4.4.1 由下至上的堆有序化(上浮)
- 如果结点比父结点大,就向上交换,直到遇到一个更大的父结点
- 实现如下:
private void swim(int k){
while(k > 1 && less(k/2, k)){
exch(k/2, k);
k = k / 2;
}
}
2.4.4.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;
k = j;
}
}
-
由此可得,两种重要操作的方法如下:
- 插入元素:将新元素加到数组末位,增加堆的大小,并让这个新元素上浮到合适的位置
- 删除最大元素:从数组顶端删去最大的元素,并将数组的最后一个元素放到顶端,减小堆的大小,并让这个元素下沉到合适的位置
-
基于堆的优先队列实现如下:
public 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 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);
private void exch(int i, int j);
private void swim(int k);
private void sink(int k);
}
- 这里省略了动态调整数组大小的代码
对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大元素的操作需要不超过2lgN次比较。
2.4.4.3 多叉堆
- 需要在树高(logdN)和在每个结点的d个子结点找到最大者的代价之间找到折中
2.4.4.6 索引优先队列
- 给每个元素一个索引
- 索引优先队列中的元素位置是固定不变的,然后维护一个优先队列的指针,对指针进行调整
2.4.5 堆排序
- 将所有元素插入一个查找最小元素的优先队列,然后重复调用删除最小元素的操作来将它们按顺序删去
- 堆排序分为两个阶段:
- 堆的构造阶段:将原始数组重新组织安排进一个堆中
- 下沉排序阶段:从堆中按递减顺序取出所有元素并得到排序结果
2.4.5.1 堆的构造
- 从右至左用sink()函数构造子堆
- 可以跳过大小为1的子堆,只需扫描数组中的一半元素
用下沉操作由N个元素构造堆只需少于2N次比较以及少于N次交换。
2.4.5.2 下沉排序
- 将堆中的最大元素删除,然后放入堆缩小后数组空出的位置
将N个元素排序,堆排序只需少于(2NlgN+2N)次比较(以及一半次数的交换)。
- 堆排序的实现过程如下:
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);
}
}
2.4.5.3 先下沉后上浮
-
大多数在下沉排序期间重新插入堆的元素会被直接加入到堆底,所以有如下方法
1. 将堆顶的元素放在数组尾部后,我们利用辅助空间暂时存储之前的堆尾元素
2. 这时堆顶空的,可以直接提升较大的子结点,直至到达堆底
3. 最后把之前的堆尾元素放到堆底空缺的位置上浮
-
这种方法可以将比较次数减少一半,但需要额外的空间,在比较操作代价较高时才有用

浙公网安备 33010602011771号