算法基础:堆排序

堆排序巩固

序言:
作为时间复杂度为 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)的成员之一,堆排序亦是一种重要的排序算法。而且,相较于归排和快排,堆排的实现更为复杂一些(包含3个过程),若对其没有一个全面的理解,则很难在面试中现场写出来。本文将详细记录其算法思想(参考《算法导论》第3版)。

1. 堆
堆示意
(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树(除了底层外,该树完全充满,底层从左往右填充)。所谓堆排序,是我们把待排序的数组,由层次遍历而假想成一个堆,并假想成我们是在堆上进行操作,而实际上是对数组在进行操作。
这里,我们假定数组名为 A A A A . l e n g t h A.length A.length表示数组的长度, A . h e a p − s i z e A.heap-size A.heapsize表示堆的大小(我们不一定把整个 A A A都看成堆,我们可能只对前 A . l e n g t h − i A.length - i A.lengthi 个元素看成是堆),且 0 ≤ A . h e a p − s i z e ≤ A . l e n g t h 0 \le A.heap-size \le A.length 0A.heapsizeA.length
此外,给定一个结点的下标,我们可以容易地计算出
p a r e n t ( i ) = ⌊ ( i − 1 ) / 2 ⌋ l e f t ( i ) = 2 i + 1 r i g h t ( i ) = 2 i + 2 \begin{aligned} parent(i) &= \left \lfloor {(i - 1)/ 2} \right \rfloor \\ left(i) &= 2i + 1\\ right(i) &= 2i + 2 \end{aligned} parent(i)left(i)right(i)=(i1)/2=2i+1=2i+2
注意了,此处下标 i i i 是从 0 开始的(导论里是从1开始的)。

  • C++实现
int parent(int i) {
    return (i - 1) >> 1;
}
int left(int i) {
    return (i << 1) + 1;
}
int right(int i) {
    return left(i) + 1;
}

对于堆,其结点需要满足一定的性质。对于最大堆,父结点的值一定大于等于子结点的值;对于最小堆,则反之。其中最大堆一般用于排序,而最小堆一般用于构造优先队列

2. 维护堆的性质
上文已经提及,堆排序使用的是最大堆,而最大堆的父结点的值一定大于等于子结点的值。倘若不满足这个条件,即父结点的值小于它结点的值,则我们需要让这个父节点与其儿子作换位(逐级下沉),直到使得该结点为根的子树满足最大堆的性质。

  • C++实现
void maxHeapify(vector<int>& a, int i, int heapSize) {
    int lchd = left(i), rchd = right(i), largest = i;
    if (lchd < heapSize && a[lchd] > a[i]) largest = lchd;
    if (rchd < heapSize && a[rchd] > a[largest]) largest = rchd;

    if (largest != i) {
        //说明有变化
        swap(a[i], a[largest]);
        maxHeapify(a, largest, heapSize);//递归调用
    }
}

过程参阅下图,即所谓逐级下沉。
在这里插入图片描述

  1. 父结点下降一级,可能其与之孙结点仍不满足最大堆的性质,因此需要递归调用;
  2. 最坏情况下,我们需要递归调用次数为“该二叉堆的深度”次,因此时间复杂度为 O ( lg ⁡ n ) O(\lg n) O(lgn) n n n 表示二叉堆的元素个数。或者说是 O ( h ) O(h) O(h) h h h 表示二叉堆的高度。

3. 建堆
我们可以根据自底向上的方法,将一个数组转换为最大堆(一个数组可以看做一个二叉堆的层次遍历,但该堆并不一定是最大堆,我们需要交换数组某些元素的位置,使之为某个最大堆的层次遍历)。我们根据下标自大向小的顺序对每个元素执行维护堆函数 maxHeapify 即可。

  • C++实现
int buildMaxHeap(vector<int>& a) {
    int heapSize = a.size(); 
    //自底向上
    for (int i = (heapSize >> 1) - 1; i >= 0; --i) 
    	maxHeapify(a, i, heapSize);
    //这里 i 不是从 heapSize 开始取的,
    //而是从第一个非叶子结点 (heapSize >> 1) - 1 开始的
    //当然,我们也可以从 heapSize 开始
    //但是叶子结点只会进入 维护堆函数 maxHeapify 一次便跳出
    return heapSize;
}

这个函数的的时间复杂度非常有意思。我们刚才已经知道,对于高度为 h h h 的结点,其执行 maxHeapify 的复杂度为 O ( lg ⁡ n ) O(\lg n) O(lgn),而外部循环执行了 A . h e a p − s i z e A.heap-size A.heapsize 次,最坏情况下理应是 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)。但是,对于maxHeapify ,只有是根结点的复杂度为 O ( lg ⁡ n ) = O ( h ) O(\lg n) =O(h) O(lgn)=O(h),每下降一级,其最深递归层次将递减。

对于高度为 h h h 的层次,包含的结点数至多为 2 lg ⁡ n − h = n 2 h 2^{\lg n - h} = \frac{n}{2^h} 2lgnh=2hn (根节点的高度为 h h h, 结点数为 2 0 = 1 2^0=1 20=1)。即,对于高度为 h h h 的层次,外循环次数为 n 2 h \frac{n}{2^h} 2hn,内部maxHeapify 的复杂度为 O ( h ) O(h) O(h)buildMaxHeap 总的时间复杂度为:
∑ h = 0 ⌊ lg ⁡ n ⌋ O ( h ) ⋅ n 2 h = O ( n ∑ h = 0 ⌊ lg ⁡ n ⌋ h 2 h ) \begin{aligned} \sum_{h=0}^{\left \lfloor{\lg n} \right \rfloor}{O(h)·\frac{n}{2^h}} = O(n\sum_{h=0}^{\left \lfloor{\lg n} \right \rfloor}\frac{h}{2^h}) \end{aligned} h=0lgnO(h)2hn=O(nh=0lgn2hh)
其中
∑ h = 0 ∞ h 2 h = 1 / 2 ( 1 − 1 / 2 ) 2 = 2 ( 对 几 何 级 数 ∑ h = 0 ∞ x h 求 导 可 以 推 得 ) \begin{aligned} \sum_{h=0}^{\infty}{\frac{h}{2^h}} = \frac{1/2}{(1-1/2)^2} = 2\\ (对几何级数\sum_{h=0}^{\infty}{x^h}求导可以推得) \end{aligned} h=02hh=(11/2)21/2=2h=0xh
因此,buildMaxHeap 总的时间复杂度为 O ( n ) O(n) O(n)

4. 堆排序
以上,我们已经完成了维护堆函数,以及建堆函数的编写。堆排序算法已经呼之欲出:
首先,利用建堆函数完成对原数组的重排,构建成一个标准的最大堆。此时,堆的大小 A . h e a p − s i z e A.heap-size A.heapsize 等于数组长度 A . l e n g t h A.length A.length
此时,堆顶元素(也是数组首元素)是数组中最大的元素,我们与数组的尾元素作交换,那么该最大元素便正确入位了;
一次交换后,最大堆的性质被破坏(堆顶被换成了数组中的尾元素),我们要重新维护堆。注意,此时的堆大小应当减去1(砍掉最后一个元素),然后再去维护堆,因为我们已经不需要再去处理尾元素了(它已经被排好序)。不断重复交换,维护堆的过程, 直至堆大小为1而停止。

  • C++实现
void heapSort(vector<int>& a) {
    int heapSize = buildMaxHeap(a);
    for (int i = a.size() - 1; i > 0; --i) {
        swap(a[i], a[0]);
        maxHeapify(a, 0, --heapSize);
    }
}

heapSort 的时间复杂度是 O ( n lg ⁡ n ) O(n\lg n) O(nlgn),因为每次调用 buildMaxHeap 的复杂度为 O ( n ) O(n) O(n)(低阶小量),而 n − 1 n - 1 n1 次调用 maxHeapify ,每次的时间是 O ( lg ⁡ n ) O(\lg n) O(lgn)(高阶主导量)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值