[老实李] 算法初探:堆排序 Heap Sort

1、为什么要使用堆?

首先我们可以理解一下堆和栈之间的区别,第一从空间的分配来看的话,堆是手动申请和释放空间,一般用new来分配;栈,空间由操作系统自动分配和适配,空间有限。比如Object a = null;就是在栈中分配空间,Object a = new Object()就是在堆中分配空间了。第二从对任务的调度策略来看的话,队列是先进先出,栈是先进后出,而堆是每个任务分配了一个优先权,根据优先权进行任务的执行。
因此,堆又被称为优先队列。 大家知道普通的队列是先进先出,后进后出,而优先队列则是出队顺序和入队顺序无关,而只和优先级有关。在计算机领域就有大量的地方使用到了优先队列。
典型的应用比如在操作系统中执行任务,大家知道操作系统可以同时执行很多任务,实际上操作系统是将CPU的执行周期划分为了时间片,在每个时间片里只能执行一个任务。那究竟执行哪一个任务呢?操作系统就会动态的每一次选择优先级最高的任务来执行,也就是说操作系统中的每一个任务都会动态的加到这样一个优先队列之中。这里请大家一定注意“动态”两个字,如果是静态的任务队列那么很简单,只要我们对这个队列排一次序然后按照优先级大小依次执行即可,但是实际在我们使用优先队列的时候情况是非常复杂的。例如操作系统中的任务是会随时增加的,而如果使用排序的方式那么只要增加一个任务,我们就需要重新排序一次,甚至当一个任务运行结束的时候我们也要重新的排序一次,而这么做耗时是巨大的。
优先队列这种模型不仅仅适用于操作系统,在生活中其他方面使用此模型的也很多。比如用户访问同一个网页的时候,而服务器端就要依次回应这些请求,回应的顺序是怎样的呢,一般就会使用优先队列,在媒体服务,视频播放,音乐播放等领域都要使用优先队列。再比如在人工智能领域的应用,我们在打枪战游戏的时候,视野前有很多的敌人,使用优先队列我们就可以优先攻击血少的敌人,有的敌人被消灭了就执行出队操作,而有新的敌人进入视野了就执行入队操作。
之前我们一直强调使用优先队列在处理动态的数据上是很有优势的,但其实在一些静态问题上也是很有优势的。比如“在100000000个元素中选出前100名?“这样的问题。按照正常的排序,在N个元素中选出前M个元素,这个算法是NlogN复杂度的,而使用优先队列则就变成了NlogM的复杂度。

2、优先队列的实现方式

ScreenClip.png

优先队列的实现方式有哪些呢?根据之前本文对优先队列的一个讲解,有了如下几个思路
第一个思路是使用一个普通数组,这样元素入队的时候就直接插入到数组的末尾,算法复杂度是O(1),而出队的时候要排一下序选择优先级最高的出队,复杂度就是O(n);
第二个思路是使用一个顺序的数组,这样元素入队的时候要进行一个排序让其插入到合适的位置,出队的时候就直接将数组最头的出队即可;
但是上面两种思路都是有局限性的,我们伟大的计算机科学家发明了一种新的数据结构,那就是堆。可以看到,在入队的事件复杂度上面堆不如普通数组,在出队的复杂度上不如顺序数组,但是堆可以很好的平衡入队和出队的时间复杂度。在极端情况下,使用数组来实现优先队列最差的情况是O(n^2)的时间复杂度,而使用堆则稳定在O(lgn)的时间复杂度上面。

3、堆的基本存储

堆所对应的插入和删除都是lgN级别的,所以堆一定相应的是一个树形结构。最为经典的一个堆的实现就是二叉堆。
首先二叉堆就是一个二叉树结构的,二叉树的特点就是一个父节点两个子节点,且子节点要小于自己的父节点。

ScreenClip.png
ScreenClip.png

其次,二叉堆又是一棵完全二叉树。完全二叉树就是除了最后一层节点,其余层级的节点都必须是满的,并且最后一层节点要靠左边。

满足这两个条件就构造了一棵二叉堆。

ScreenClip.png

4、一个堆的基本实现

class MaxHeap{
     private int[] data;
     private int count;
     public MaxHeap(int capacity){
           data = new int[capacity];
           count = 0;
     }
     public int getSize(){
           return count;
     }
     public boolean isEmpty(){
           return count==0;
     }
}

其实就是在构造函数里面new了一个数组而已。

5、在堆中插入一个数据Shift up

ScreenClip.png

比如我们在数组的末尾加上一个52,此时这个堆就不满足一个二叉堆的定义了,我们就需要让这个二叉堆满足二叉堆的定义,也就是要让这个52放到合适的位置。
首先和其父节点16比较,比16大,交换位置,再与41比较,比41大,交换位置,再与62比较,比62小,ShiftUp结束。这个过程就是ShiftUp。
具体代码实现:

class MaxHeap {
     private int[] data;
     private int count;
     private int capacity;

     public MaxHeap(int capacity) {
           data = new int[capacity];
           count = 0;
           this.capacity = capacity;
     }

     public void print() {
           for (int i = 0; i < getSize(); i++) {
                System.out.print(data[i] + ",");
           }
     }

     public int getSize() {
           return count;
     }

     public int[] getData() {
           return data;
     }

     public boolean isEmpty() {
           return count == 0;
     }

     public void insert(int item) {
           assert (count + 1 <= capacity);
           data[count + 1] = item;
           count++;
           shiftUp(count);
     }

     private void shiftUp(int k) {
           while (k > 1 && data[k / 2] < data[k]) {
                int temp = data[k / 2];
                data[k / 2] = data[k];
                data[k] = temp;
                k /= 2;
           }
     }

}

测试代码:

public class Heap {
     public static void main(String[] args) {
           MaxHeap maxHeap = new MaxHeap(100);
           Random rand = new Random();
           for (int i = 0; i < 10; i++) { 
                int randNum = rand.nextInt(100) + 1;
                maxHeap.insert(randNum);
           }
               
           System.out.println(maxHeap.getSize() + "");
           maxHeap.print();

     }
}

输出结果:

10
0,99,71,47,42,63,44,17,42,39,

6、从堆中移除一个元素Shift Down

ScreenClip.png

从堆中只能移除根节点的元素,比如上面的例子中第一次移除的元素是索引为1的元素62,然后由堆中最底层的元素16填补空缺。然后对此时的根节点16进行ShiftDown操作。ShiftDown操作就是将16放到合适的位置,首先另其左右子节点比较,较大的一个与自己再进行比较,然后交换位置,直到16到达它合适的位置停止。
具体代码实现:

public int extractMax() {
        assert (count > 0);
        int ret = data[1]; // 1、获取堆中的第一个元素

        // 2、swap第一个元素和最后一个元素
        int temp = data[1];
        data[1] = data[count];
        data[count] = temp;
        // 3、数组大小减1
        count--;
        // 4、对第一个元素执行ShiftDown
        shiftDown(1);
        return ret;
    }
    public int getMax() {
        assert (count > 0);
        return data[1];
    }

    private void shiftDown(int k) {
        while (2 * k <= count) {// 1、 如果此节点有子节点
            int j = 2 * k; // 2、j代表左节点索引
            if (j + 1 <= count && data[j + 1] > data[j])// 3、如果右节点大于左节点,且右节点在索引范围内
                j++; // 4、j变成代表右节点索引

            // data[j] 是data[2*k]和data[2*k+1]中的最大值
            if (data[k] >= data[j]) {
                break;
            }
            // 5、swap
            int temp = data[k];
            data[k] = data[j];
            data[j] = temp;
            // 6、下一次从原右索引位置处开始比较
            k = j;
        }
    }

测试代码:

public static void main(String[] args) {
        MaxHeap maxHeap = new MaxHeap(100);
        Random rand = new Random();
        for (int i = 0; i < 10; i++) {
            int randNum = rand.nextInt(100) + 1;
            maxHeap.insert(randNum);
        }

        while (!maxHeap.isEmpty()) {
            int num = maxHeap.extractMax();
            System.out.print(num + ",");
        }
    }

7、HeapSort

基础堆排序:

public static void heapSort(int arr[], int n) {
        MaxHeap maxHeap = new MaxHeap(n+1);
        for (int i = 0; i < n; i++) {
            maxHeap.insert(arr[i]);
        }
        for (int i = n - 1; i >= 0; i--) {
            arr[i] = maxHeap.extractMax();
        }
    }

测试用例:

Random rand = new Random();
        int arr[] = new int[9000000];
        for (int i = 0; i < 9000000; i++) {
            int randNum = rand.nextInt(9000000) + 1;
            arr[i] = randNum;
        }
        long currentTimeMillis = System.currentTimeMillis();
        heapSort(arr, 9000000);
        long currentTimeMillis2 = System.currentTimeMillis();
        System.out.println("基础堆排序所用时间:"+(currentTimeMillis2-currentTimeMillis));

输出:

基础堆排序所用时间:2488

可以看到这个基础堆排序的速度还是可以接受的。

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

推荐阅读更多精彩内容

  • 堆排序是一种树形选择排序,是对直接选择排序的有效改进。 基本思想: 堆顶元素(即第一个元素)必为最小项(小顶堆)或...
    NEXTFIND阅读 867评论 0 1
  • 堆排序算法,基于选择排序的思想,利用堆结构的性质来完成对数据的排序。 前提准备: 什么是堆结构:堆数据结构是一种数...
    noonbiteun阅读 568评论 0 3
  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    RichardJieChen阅读 5,062评论 0 12
  • 其实 晚上十点半 夜并不浓深 路上车来人往 热闹非凡 其实 南方的冬季 还没有阴冷 街道灯火阑珊 灯红酒绿 定目凝...
    鄧虢阅读 128评论 1 3
  • 大三马上就要结束了,开学两个月将近,熬了将近两个月的夜,一天天晚上不知道干什么,以前是为了玩球球这个游戏,现在明明...
    单手控i阅读 157评论 0 0