堆排序的比较次数_快速入门堆排序

36b3a293aab70d6235db46b6e4d37b0d.png

堆排序是一种原地、时间复杂度

equation?tex=nlogn 的排序算法,它借助了一种数据结构-堆(heap)。

什么是堆

首先堆是一种树,一种满足以下特质的树结构:

  • 堆是一个完全二叉树
  • 堆中每一个节点的值必须大于或者等于(小于或者等于)其左右子树的值。【大顶堆、小顶堆】

接下来看看同一组数据的不同堆的形态:

666b4365a8fd204c33ad79d9dadf2959.png

如何实现一个堆

对于一个ADT,我们需要知道它有哪些操作方法、以及数据存储的方式。

存储一个完全二叉树,最适合使用数组,因为它相比链表不需要存储左、右子树的指针,更加节省内存空间,通过数组索引即可以随机访问到对应元素。

1907631e52cf94d4e0f565d95c2a1fb2.png

【数组第一个位置留空,方便计算】

假设你知道某个节点下标为

equation?tex=x ,那么其左子树下标为
equation?tex=x%2A2 ,其右子树下标为
equation?tex=x%2A2%2B1 ,其父节点下标为
equation?tex=x%2F2

常见的堆操作

往堆里面插入一个元素

当我们往堆里面插入一个元素后,我们需要对新的堆进行调整,使其能够满足上述的两个特性,那么这个过程我们称为堆化。

堆化实际上有两个操作,一个是从下往上、一个是从上往下。

从下往上

a1072c1deaca9fc242d833275f43ef67.png

从下往上堆化就是指把该元素与其父节点对比、满足条件后交换位置、继续对比、交换,直到不能交换了。

161be23eab8a8d8aecc283ec6ce3700b.png

删除堆顶元素

我们都知道大顶堆、小顶堆的堆顶位置存储的分别是最大值、最小值。

假设我们构造的是一个大顶堆,当我们删除堆顶元素后,我们需要把之前堆内第二大元素移到堆顶位置,依次类推,我们依次删除第二大节点、第三大节点、直到叶子节点。

120ef8ecb91d740444ab10e346d8b062.png

上面的思路其实就是:当我们删除堆顶元素时候,直接把最后一个节点放到堆顶位置,然后从上往下进行堆化。如果父节点的值小于其子节点的值,则进行交换、继续往下对比。

接下来我们分析下堆的插入元素操作【从下往上堆化】、删除堆顶元素操作【从上往下堆化】的时间复杂度:

由于一个完全二叉树的高度不会超过

equation?tex=log2%5En ,而堆化操作其实就是沿着节点路径进行对比、交换。因此它的时间复杂度与树的高度成正比,所以往堆里面插入一个元素、删除堆顶元素的时间复杂度是
equation?tex=logn

代码实现:基于堆的排序

堆排序如此优秀,那么它是如何做到的呢?其过程包括两点:建堆、排序。

建堆

我们把无序数组构建成一个二叉堆:

如果你需要对无序数组进行从小到大排序,那么你应该构建为大顶堆;

如果你需要对无序数组进行从大到小排序,那么你应该构建为小顶堆。

假设我们对无序数组[4, 4, 6, 5, 3, 2, 8, 1]进行升序排序。

当建堆结束后,会得到这样一个大顶堆。

           8
          /   
         5   6
        /  / 
       4  3 2 4
      /
     1

分析堆化操作时间复杂度:我们可以画出每一层完全二叉树的高度以及该层的节点个数【这里去除了叶子节点】

6db5c543cdf208195207cc06942f85ea.png

每个节点堆化的过程中,它需要对比、交互的次数与这个节点所在高度成正比,因此我们只需要计算出每个节点所在高度之和就是时间复杂度了。

169be5322fc0556eddac4fde7db517ea.png

dc7abe23b3fa0774026b8b06b68b5296.png

因为 h=log2​n,代入公式 S,就能得到 S=O(n),所以,建堆的时间复杂度就是 O(n)。

排序

建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。

数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。

堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。

d76fa7b95f4a751581238fe8d4152dea.png
/**
 * 下沉操作
 * @param {array} arr 待调整的堆 
 * @param {number} parentIndex 要下沉的父节点
 * @param {number} length 堆的有效大小
 */
function downAdjust(arr, parentIndex, length) {
    // temp保存父节点的值,用于最后赋值
    let temp = arr[parentIndex]
    let childrenIndex = 2 * parentIndex + 1
    while(childrenIndex < length) {
        // 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
        // 这里其实是比较左、右子树的大小,选择更大的
        if (childrenIndex + 1 < length && arr[childrenIndex + 1] > arr[childrenIndex]) {
            childrenIndex++
        }
        // 如果父节点大于任何一个孩子得值,则直接跳出
        if (temp >= arr[childrenIndex]) {
            break
        }
        // 当左、右子树比父节点更大,进行交换
        arr[parentIndex] = arr[childrenIndex]
        parentIndex = childrenIndex
        childrenIndex = 2 * childrenIndex + 1
    }
    arr[parentIndex] = temp
}

/**
 * 堆排序(升序)
 * @param {array} arr 待调整的堆 
 */
function heapSort(arr) {
    // 把无序数组构建成最大堆, 这里-2,是因为从索引0开始、另外就是叶子节点【最后一层是不需要堆化的】
    for(let i = (arr.length - 2)/2; i >= 0; i--) {
        downAdjust(arr, i, arr.length)
    }
   
    // 循环删除堆顶元素,并且移到集合尾部,调整堆产生新的堆顶
    for(let i = arr.length - 1; i > 0; i--) {
        // 交换最后一个元素与第一个元素
        let temp = arr[i]
        arr[i] = arr[0]
        arr[0] = temp
        // 下沉调整最大堆
        downAdjust(arr, 0, i)
    }
    return arr
}
// test case
console.log(heapSort([4, 4, 6, 5, 3, 2, 8, 1]))

整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。

堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。

堆排序 VS 快排

对于同样的数据,堆排序对于数据的交换次数要多于快排,因为堆排序第一步是建堆,这个过程会打乱数据的有序度。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值