算法笔记-排序03:优先队列

为什么需要优先队列

我们并不一是一直都需要所有的元素全部有序。很多情况下我们会选择收集一些元素,然后处理其中键最大的元素,然后再收集更多的元素,再处理其中键值最大的元素。例如:你拥有一台可以同时运行多个程序的电脑,这是通过为每一个应用程序的事件分配一个优先级,并总是先处理优先级最高的事件。例如手机来电事件和你正在玩的手机游戏事件,绝大多数手机都会给手机来电事件分配一个更高的优先级,优先处理,所以你的游戏被打断,只能先处理来电。
这种情况下,需要支持以下两种操作的数据结构:
1.删除最大元素。
2.插入元素。
这种数据结构叫做优先队列。

API

优先队列是一种抽象的数据类型,它表示了一组值和对这些值的操作,它的抽象层是我们能够方便的将用例和实现隔离开来。

public class  MaxPQ<Key extends Comparable<Key>>
                   MaxPQ()                              创建一个优先队列
                   MaxPQ(int max)                       创建一个初始容量为max的优先队列
                   MaxPQ(Key[] a)                       用a[]中的元素创建一个优先队列
             void  Insert(Key v)                        向优先队列中插入一个元素
              Key  max()                                返回最大元素
          boolean  isEmpty()                            返回队列是否为空
              int  size()                               返回优先队列中元素的个数


这份API可以有很多种实现,比如有序数组,无序数组,链表,但它们在最坏的情况下插入元素和删除元素两个操作需要线性时间来完成,但是基于数据结构"堆"的实现能够保证这两种操作都能更快的执行:

来自《Algorithms Fourth Edition》一书的截图,侵删
堆的定义
堆有序:当一棵二叉树的每个节点都大于等于它的两个字节点时,它被称为堆有序。
来自《Algorithms Fourth Edition》一书的截图,侵删

完全二叉树只用数组而不用指针就能表示。具体的方法是将二叉树的节点按照层级顺序放到数组中,根节点在位置一,它的子节点在2和3,而子节点的子节点则在4,5,6,7,以此类推。

来自《Algorithms Fourth Edition》一书的截图,侵删
二叉堆(简称堆):二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按层级存储。

用堆实现的完全二叉树的结构是非常严格的,但它的灵活性已经足以让我们高效的实现优先队列。用它们我们将能够实现对数级别的插入元素和删除最大元素的操作。

基于堆的优先队列的代码实现
public class MaxPQ <Key extends Comparable<Key>>{
    private Key[] pq; //堆有序的完全二叉树
    private int N = 0; //数据存储在pq[1...N]中,pq[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];  //从根节点得到最大的元素
        exchange(1,N--); //将其和最后一个节点交换
        pq[N+1] = null; //防止对象游离
        sink(1); //恢复堆的有序性
        return max;
    }
    
    private boolean less(int i, int j){
        return pq[i].compareTo(pq[j]) < 0;
    }
    
    private void exchange(int i, int j){
        Key t = pq[i];
        pq[i] = pq[j];
        pq[j] = t;
    }
    
    private void swim(int k){
        while (k>1 && less(k/2, k)){
            exchange(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;
            exchange(k,j);
            k = j;
        }
    }   
}

其中较为重要的是swim()和sink()方法,swim()方法在插入元素的时候调用,当新元素加入的时候,需要比较它与父节点的大小,如果新节点比父节点大,就要将其与父节点的元素交换,sink()方法在删除最大元素的时候调用,当删除了最大元素的时候,就数组最后一个元素放到第一个位置,然后将其与字节点比较,如果它比字节点小,就要将其下放,直到它被放到了合适的位置。
由代码实现我们很容易就可以得到对于一个含有N个元素的基于堆的优先队列,插入元素只需要不超过lgN+1次比较,而删除最大元素的操作需要不超过2lgN次比较。

堆排序

堆排序的思想:构造一个堆,然后依次从堆中拿出最大的元素,排序也就完成了。
实现代码:

public class Heap {
    
    private static Comparable[] a;
    
    public static void sort(Comparable[] array){
        
        a = new Comparable[array.length+1];
        
        for (int i = 0; i < array.length; i++) a[i+1] = array[i];
        
        int N = array.length;
        
        for (int k = N/2; k >= 1; k--){
            sink(k,N);
        }
        
        while (N>1){
            exchange(1, N--);
            sink(1,N);
        }
        
        for (int i = 0; i < array.length; i++) array[i] = a[i+1];
    }
    
    public static void main(String[] args){
        Integer[] a = {0,9,8,7,6,5,6,5,6,98,7,6,5,3};
        sort(a);
        for (int i = 0; i < a.length;i++){
            System.out.println(a[i]);
        }
    }
    
    private static boolean less(int i, int j){
        return a[i].compareTo(a[j]) < 0;
    }
    
    private static void exchange(int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    private static void swim(int k){
        while (k>1 && less(k/2, k)){
            exchange(k/2,k);
            k = k/2;
        }
    }
    
    private static void sink(int k, int N){
        while (2*k <= N){
            int j = 2*k;
            if (j<N && less(j,j+1)) j++;
            if (!less(k,j)) break;
            exchange(k,j);
            k = j;
        }
    }
}

在排序方法中,先创建一个比原数组多一个元素的数组,然后将原数组复制到新数组的1~N位上,开始构造一个二叉堆,构造完二叉堆后,每一次都将最大的元素(index为1)与后面的元素交换(1和N换,1和N-1换,1和N-2换 etc.)在换的过程中不断调用下沉方法保证堆有序,这样就能将数组从小到大排序,然后将元素复制回原数组,排序就完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容