第二章 排序(三) - 《算法》读书笔记

第二章 排序(三)

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. 最后把之前的堆尾元素放到堆底空缺的位置上浮

  • 这种方法可以将比较次数减少一半,但需要额外的空间,在比较操作代价较高时才有用

posted @ 2021-01-27 00:01  一天到晚睡觉的鱼  阅读(70)  评论(0)    收藏  举报