堆排序

1. 什么是堆

堆的概念主要有 2 点:

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

如果根节点是最大值则是大顶堆,如果根节点是最小值则是小顶堆。

2. 堆的2种常用操作

由于堆是一个完全二叉树,所以堆可以用一个数组来表示。如果数组 A 的下标从 0 开始存储数据,则 A[0] 表示根节点,A[1] 是其左子节点,A[2] 是其右子节点。进而可以得到:对于节点 A[i],其左子节点为 A[i*2+1],右子节点为 A[i*2+2],其父节点为 A[(i-1)/2]。

堆有 2 种常用操作:增加一个元素、删除堆顶元素。这里涉及到一个堆化的概念,堆化就是调整二叉树的节点的值,让其满足堆的特性的过程。

具体可见代码及说明(kotlin编写):

class Heap(private val capacity: Int) {

    private val array = IntArray(capacity)
    //当前堆里的数据个数
    private var count: Int = 0

    /**
     * 构造的是大顶堆
     * 自下而上堆化:先将数据插入到叶子节点上,然后将该节点与其父节点比较交换,直到满足堆的特性为止
     */
    fun insert(value: Int) {
        if (count == capacity) {
            return
        }
        array[count++] = value
        var p = count - 1     //插入的节点在数组中的索引
        var pp = (p - 1) / 2  //父节点索引
        while (p > 0 && array[pp] < value) {
            array[p] = array[pp]
            p = pp
            pp = (p - 1) / 2
        }
        array[p] = value
    }

    /**
     * 删除堆顶元素
     * 自上而下堆化:将节点与其子节点比较交换,直到满足堆的特性为止
     */
    fun removeTop(): Int? {
        if (count == 0)
            return null
        //堆顶元素就是数组的第一个元素
        var top = array[0]
        //将最后一个元素与堆顶元素交换
        array[0] = array[--count]
        //直接删除最后一个元素
        array[count] = 0
        if (count == 0)
            return top
        //自上而下进行堆化
        var p = 0
        var target = array[0]
        while (p < count) {
            var maxIndex = p
            //左子节点索引
            var left = p * 2 + 1
            //右子节点索引
            var right = left + 1
            if (left < count && array[left] > target) {
                maxIndex = left
            }
            if (right < count && array[right] > target) {
                maxIndex = right
            }
            if (p == maxIndex)
                break
            array[p] = array[maxIndex]
            p = maxIndex
        }
        array[p] = target
        return top     
    }

}

堆的插入、删除操作时间复杂度都为 O(logn):插入、删除节点都是一个堆化的过程,其实就是节点进行比较交换的过程,如果从根节点开始操作,一直到叶子节点才结束,则比较交换的次数与完全二叉树的高度相关,而完全二叉树的高度为 log₂n,所以说其时间复杂度为 O(logn)

3. 堆排序

1.先在原数组内建堆,如果要升序排列则建一个大顶堆。建堆有 2 种方式:
  1. 假设堆里只有1个元素,我们将数组里剩余 n-1 个元素依次插入到堆里,插入完成后则建堆完毕;
  2. 对于一个堆来说,任意一个节点以及其子树节点合在一起点肯定也是一个堆,每个叶子节点单独来说肯定是一个堆。从堆的最后一个非叶子节点起到根节点,我们依次对每个节点进行堆化操作,操作完成后堆就形成了。
2.大顶堆的第一个元素肯定是最大值,我们每次删除堆顶元素,然后再重新堆化,一直到堆里只有一个元素为止,这时候数组排序就排好了。

代码如下(kotlin编写):

/**
 * 堆排序
 */
fun heapSort(array: IntArray?) {
    array ?: return
    if (array.isEmpty())
        return
    //如果是升序排列,则构造大顶堆,如果是降序排列,则构造小顶堆
    buildHeap(array)
    //每次从大顶堆中取出最大的数,放到最后面,共需要 n - 1 次
    var c = 0
    //对于数组A[0, 1, 2, ..., n-1],现在是一个大顶堆
    //1.交换A[0]与A[n-1],将A[0, 1, 2, ..., n-2]进行堆化操作
    //2.交换A[0]与A[n-2],将A[0, 1, 2, ..., n-3]进行堆化操作
    //3.依次反复操作,直到只有一个元素时停止,整个数组就排序完成了
    while (c < array.size - 1) {
        c++
        swap(array, 0, array.size - c)
        //剩下的数据继续堆化
        heapify(array, 0, array.size - c - 1)
    }
}

//先原地建堆:叶子节点本身就是一个堆,所以我们从非叶子节点开始往前到根节点,依次进行堆化操作
private fun buildHeap(array: IntArray) {
    var n = array.size
    //用数组表示的完全二叉树中,非叶子节点索引值范围约为[0, n/2]
    //从 n/2 节点开始从后往前,依次对每个节点都执行堆化操作,最终可完成建堆,这种建堆方式效率高一点
    for (i in n / 2 downTo 0) {
        heapify(array, i, n - 1)
    }
}

/**
 * 进行堆化
 *
 * @param array 数组
 * @param start 起始索引
 * @param end 结束索引
 */
private fun heapify(array: IntArray, start: Int, end: Int) {
    var p = start
    while (p < end) {
        //在堆中左子节点对应数组中的索引,注意这里我们数组索引是从0开始存数据的
        var left = p * 2 + 1
        //在堆中右子节点对应数组中的索引
        var right = left + 1
        //将当前节点值与左、右子节点的值进行比价,找出最大的
        var maxIndex = p
        if (left <= end && array[left] > array[maxIndex]) {
            maxIndex = left
        }
        if (right <= end && array[right] > array[maxIndex]) {
            maxIndex = right
        }
        if (p == maxIndex)
            break
        //将大的节点值交换上去
        swap(array, p, maxIndex)
        p = maxIndex
    }
}

private fun swap(array: IntArray, i: Int, j: Int) {
    var tmp = array[i]
    array[i] = array[j]
    array[j] = tmp
}

4. 小结

堆排序首先需要了解堆的概念,而堆是一个完全二叉树,完全二叉树又可以直接用数组来表示。所以要了解堆排序算法,必须先掌握二叉树的相关知识,以及树的相关概念操作等,否则理解起来会很费劲。

堆排序空间复杂度 O(1),我们是直接在原数组里建堆,不需要额外空间
堆排序时间复杂度 O(nlogn)
堆排序时会涉及到堆顶节点跟最后一个节点的交换,可能会使得相同值的节点先后顺序改变,所以堆排序是不稳定的。

堆排序与快排时间复杂度、空间复杂度是相同的,但是堆排序涉及到大量的比较交换操作,相对来说堆排序没有快排速度快。

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

推荐阅读更多精彩内容