堆排序

什么是堆?

“堆”是一种常用的数据结构,也是一种特殊的树。我们来看看,到底什么样的树才是堆。堆有如下两点要求,满足这两点要求的,就是一个堆。

1.堆是一个完全二叉树
2.堆中的每个节点值都必须大于等于(或小于等于)其子树每个节点的值。

第一点,堆是完全二叉树。那就意味除了树的最后一层,其他层节点个数都是满的,最后一层的节点都靠左排列。
第二点,换种说法就是,堆中的每个节点都大于等于(或小于等于)其左右节点的值。

对于每个节点值都大于等于子树每个节点的值的堆,称之为“大顶堆”。对于每个节点值都小于等于子树中每个节点值的堆,称之为“小顶堆”。“大顶堆”的第一个元素为最大值,“小顶堆”的第一个元素为最小值。

堆有哪些操作?

完全二叉树比较适合用数组来存储,这样比较节省存储空间。因为不需要存储左右子节点的指针,只要通过数组的下标,就可以很方便的找到一个节点的左右子节点和父节点。

下面是一个大顶堆的例子,数组下标从1开始。


从图中可以看出,对于任意一个下标为i的节点,这个节点的左子节点是下标为 i*2 的节点,右子节点是下标为 i * 2 + 1的节点,父节点是 i / 2 的节点。

知道了如何存储一个堆后,接着看看堆有哪些操作。堆有两个非常核心的操作:往堆中插入一个元素、删除堆顶元素。

1.往堆中插入一个元素
往堆中插入一个元素后,堆结构需要继续满足堆的两个特性。将新元素放到堆最后,如果不符合堆的特性,那就需要进行调整重新满足堆的特性,这个调整的过程叫做“堆化”。

堆化有两种,“从上往下”和“从下往上”。下面使用“从下往上”的方式来实现往堆中插入元素。

“从下往上”这种堆化方式非常简单,新插入的节点与父节点比较大小。如果不满足子节点小于等于父节点,那么就互换两个节点。重复这个过程,直到父子节点之间满足这种关系为止。


根据上面的思路,插入元素的代码可以结合着图示一起看。

  public class Heap{
    private int[] arr;  //二叉树数组
    private int count;  //堆中已经存储的数据个数
    public Heap(int capacity){
      arr = new int[capacity + 1];
      n = capacity;
      count = 0;
    }

  public void insert(int data){
       if(count + 1 >= arr.length){
            throw new IllegalArgumentException();
       }
       count++;
       arr[count] = data;
       int pos = count;
       while (pos / 2 > 0 && arr[pos / 2] < arr[pos]){
            swap(pos,pos / 2);
            pos = pos / 2;
       }
   }
}

2.删除堆顶元素
删除堆顶元素后,同样也需要保证堆能够满足那两个特性。删除堆顶元素后,我们可以把堆中最后一个元素放到堆顶。然后利用父子节点对比方法,对于不满足父子节点大小关系的节点,互换两个节点,重复进行这个过程,直到父子节点之间满足二叉堆的特性为止。这里使用的是“从上往下”的方法进行堆化。


根据上面的思路,删除堆顶元素的代码可以结合着图示一起看。

public void removeTop(){
    if(count == 0 ){
        retun;
    }
    a[1] = arr[count];
    count--;
    int  pos = 1;
    int maxPos = 1;
    while(true){
        if(pos * 2 <= count && arr[pos * 2] > arr[pos]){
            maxPos = pos * 2;
        }
        if(pos * 2 + 1 <= count && arr[pos * 2 + 1] > arr[pos]){
            maxPos = pos * 2 + 1;
        }
        if(maxPos == pos){
            break;
        }
        swap(arr,pos,maxPos);
        pos = maxPos;
    }
}

一颗包含n个节点的完全二叉树,树的高度不会超过\log_2n。堆化的过程是顺着节点的路径进行比较交换,所以堆化的时间复杂度跟树的高度成正比,也就是O(log n)。因为插入和删除都是做的堆化操作,所以他们的时间复杂度都是O(log n)。

堆排序怎么实现?

我们可以借助堆这种数据结构实现的排序算法,就叫堆排序。这种排序算法的时间复杂度很长稳定,是O(n log n)。堆排序的过程可以分为两个步骤,构建堆和排序。

1.构建堆
将待排序的数据构建成堆,构建堆有两种思路。

第一种就是借助前面说的,在堆中插入一个元素。我们可以假设最开始堆中只包含一个下标为1的数据。然后我们调用插入操作,将下标从2到n的数据依次插入到堆中。这样我们就将包含n个数据的数组,组织成了堆。

第二种实现,和第一种思路相反。第一种建堆处理过程是从前往后处理数组数据,每个数据插入堆中,都是从下往上堆化。而第二种实现思路是从后往前处理数组,并且每个数据都是从上往下堆化。

可以参照下面的例子,叶子节点堆化只能和自己比较,所以我们直接从第一个非叶子节点开始,依次堆化就可以。


对照着图示,将建堆的过程翻译成代码。

    /**
     * 构造堆
     */
    private void buildHeap(){
        for(int i = count / 2; i>0; i--){
            downAdjust(i,count);
        }
    }

    /**
     * "从上往下"调整
     * @param pos:下沉节点下标
     * @param n:堆有效大小
     */
    private void downAdjust(int pos,int n){
        int maxPos = pos;
        while (true){
            if(pos * 2 <= n && arr[pos * 2] > arr[pos]){
                maxPos = pos * 2;
            }
            if(pos * 2 + 1 <= n && arr[pos * 2 + 1] > arr[maxPos]){
                maxPos = pos * 2 + 1;
            }

            if(maxPos == pos){
                break;
            }
            swap(pos,maxPos);
            pos = maxPos;
        }
    }

2.排序
经过建堆这个步骤后,数组就是一个标准的大顶堆了。数组中第一个元素是堆顶,也是数组中最大的元素。我们把它和最后一个元素交换,那么最大元素就放到下标为count的位置了。

这个过程类似于“删除堆顶元素”,当堆顶元素删除之后,我们把最后一个元素放到堆顶,然后通过“从上往下”方式堆化,将剩下count - 1个元素重新构建成堆。堆化完成后,再取堆顶元素放到下标是 count - 1位置,一直重复这个过程,直到堆中只剩下下标为1的一个元素,完成排序。


将上面的排序过程,翻译成排序代码。

    /**
     * 堆排序
     */
    public void sort(){
        int n = count;
        for (int i = count; i > 1 ; i--){
            swap(1,i);
            downAdjust(1,--n);
        }
    }

时间复杂度分析

在整个堆排序的过程中,需要极少的临时存储空间。堆排序包括建堆和排序两个操作,构建堆过程的时间复杂度是O(n),排序过程的时间复杂度是O(n log n),所以堆排序整体的时间复杂度是O(n log n)。

GitHub 代码地址: 二叉堆

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

推荐阅读更多精彩内容