排序算法大杂烩之堆排序

版权声明:本文为博主原创文章,若需转载,请评论说明,并在文章显著位置标明原文出处。 https://blog.csdn.net/lishang6257/article/details/83047381

排序算法大杂烩主干文章传送门

堆排序

#include <iostream>
#include <vector>
/*
非降序排序
时间复杂度:o(nlgn)
空间复杂度:o(nlgn)
建堆:o(n)
维护堆:o(lgn)
不稳定
*/
using namespace std;

void swap(int &a,int &b){int c = a;a = b;b = c;}

int Max(const vector<int> &a,int n,int i){
    if(2*i + 2 > n) return i;//该节点左孩子不存在
    if(2*i + 3 > n) {
        //该节点右孩子不存在
        if(a[i] >= a[2*i + 1]) return i;
        else return 2*i + 1;
    }
    if(a[i] >= a[2*i + 1] && a[i] >= a[2*i + 2]) return i;
    if(a[2*i + 1] >= a[i] && a[2*i + 1] >= a[2*i + 2]) return 2*i + 1;
    return 2*i + 2;
}

void heapify(vector<int>&a,int n,int i)//lgn,即堆高度
{
    //最大堆
    //n 为堆大小
    //i 是指下标
    int cur = i;
    while(true){
        int tmp = Max(a,n,cur);
        if(tmp == cur) break;
        swap(a[cur],a[tmp]);
        cur = tmp;
    }
}

//建堆过程
void buildHeap(vector<int> &a) //n
{
    for(int i = a.size()/2 - 1;i >= 0;i --){
        heapify(a,a.size(),i);
    }
}

//堆排序过程
void heapSort(vector<int> &a)
{
    buildHeap(a);
    for(auto c : a) cout << c << " ";
    cout << endl;
    for(int i = a.size() - 1;i > 0;i --){
        swap(a[i],a[0]);
        heapify(a,i,0);
        for(auto c : a) cout << c << " ";
        cout << endl;
    }
}

int main()
{
    vector<int> a = {4,1,3,2,16,9,10,14,8,7};
    heapSort(a);
    for(auto c : a) cout << c << " ";
    return 0;
}

  • 基本概念
    • 最大堆:对于任何一个结点的键值总大于孩子结点的键值
    • 最小堆:对于任何一个结点的键值总小于孩子节点的键值
  • 思维引擎
    堆排序顾名思义,利用这种数据结构达到排序的目的。这里主要运用了三个主要操作:建堆,维护堆,堆排序
    • 问题描述:对序列 A [ 1.. n ] A[1..n] 进行非降序排序。
    • 建堆:将原序列生成最大(小)堆的过程。
      n 2 节点 \frac{n}{2} 1 节点 1 依次维护以该节点为根节点的子树的最大(小)堆性质。
    • 维护堆
      由于仅有根节点破坏了堆的性质,调整方案如下:
      1. 将子树的根节点作为起始节点, C u r 节点Cur
      2. (以最大堆为例)若 C u r 节点Cur 小于孩子结点,则与孩子结点交换,并将 C u r 节点Cur 用此孩子更新。
      3. 重复操作 2 2 直至 C u r 节点Cur 大于孩子结点,那么此时形成的堆就为最大堆。
    • 堆排序
      1. 将原始序列 A [ 1.. n ] A[1..n] 建成最大堆。
      2. 将 A [ 1 ] A[1] A [ n ] A[n] 中的元素交换,那么 A [ n ] A[n] 保存了序列的最大值,而 A [ 1.. n 1 ] A[1 .. n-1] 则成了仅 A [ 1 ] A[1] 不满足最大堆性质的堆,对堆 A [ 1.. n 1 ] A[1 .. n-1] 进行堆维护。
      3. 在依次将 A [ 1 ] A[1] A [ n 1 ] A[n-1] A [ 2 ] A[2] 交换,重复操作 2 2 ,使得 A [ 1.. n ] A[1..n] 全部排序。
    • 值得注意的是在对维护的过程中,发现以 i 节点i 为根节点的子堆中仅有 i 节点i 不满足最大(小)堆性质
  • 时间复杂度
    堆排序的时间复杂度是一个十分有趣的问题的,里面的一些数学背景我会在后面一一指出,并给出相关的证明。整体的思路来源于算法导论第三版相应内容。
    1. 维护堆:这里很容易可以观察到维护堆的时间复杂度与子树的高度有关(子树根节点与孩子结点置换时最多只能置换子树高度次,所以推测时间复杂度 o ( l o g n ) o(logn) ,下面是数学证明:
      T ( n ) T ( 2 3 n ) + o ( 1 ) , T ( n ) = o ( l o g n ) T(n) \leq T( \frac{2}{3} n) + o(1), \\当且仅当为三层最大堆且最后一层半满时取等 \\ \Rightarrow T(n) = o(logn)
      这边我们要指出为什么偏偏是 2 3 n \frac{2}{3}n 不是其他的呢,这里我们首先假设最后一层半满,那么我就可以得出最后一层(记为第 h h 层)的节点个数为 1 3 n \frac{1}{3}n ,那么根节点的左子树的的节点总个数就是 2 3 n \frac{2}{3}n ,并且在维护堆的过程中我们只能堆根节点的左子树( 2 3 n \frac{2}{3}n 个节点)或者右子树( 1 3 n \frac{1}{3}n 个节点)进行操作,显然上述不等式成立。
    2. 建堆
      这个时间复杂度是很坑的,当直接从代码本身去观察,对于 A [ 1.. i . . 1 2 n ] A[1.. i ..\frac{1}{2}n] 个节点,每一个节点都需要 o ( l o g n ) o(logn) 左右的复杂度,所以是 o ( n l o g n ) o(nlogn) 然而啪啪打脸。
      H = l o g n T ( n ) = Σ h = 1 H l o g ( 2 H h + 1 1 ) 2 h 1 Σ h = 1 H l o g ( 2 H h + 1 ) 2 h 1 , l o g ( 2 H h + 1 1 ) h 2 h 1 h T ( n ) = Σ h = 1 H [ ( H + 1 ) 2 h 1 h 2 h 1 ] = ( H + 1 ) ( 2 H 1 ) ( H 1 ) 2 H + 1 = 2 H + 1 H    = n l o g n = o ( n ) 令 堆的总高度为 H = logn,则 \\ T(n) = \Sigma_{h=1}^Hlog(2^{H-h+1}-1)2^{h-1} \approx \Sigma_{h=1}^Hlog(2^{H-h+1})2^{h-1}, \\ 其中log(2^{H-h+1}-1)表示高度为h的节点的所需的时间复杂度, 2^{h-1}表示h层节点总个数 \\ \Rightarrow T(n) = \Sigma_{h=1}^H[(H+1)2^{h-1}-h2^{h-1}] \quad \quad\quad\quad\quad\quad \\ =(H+1)(2^H-1)-(H-1)2^H+1 \\ =2^{H+1}-H \quad \quad\quad\quad\quad\quad\quad\quad\quad\quad\ \ \\ =n - logn \quad \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad \\ =o(n)\quad \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad
      这里的证法和算法导论有些出入,但是方法是没有问题的,有兴趣的笔友可以验证一下。但为什么没有达到 o ( n l o g n ) o(nlogn) 呢?从公式的结果来看,大多数的节点根本没有达到 o ( l o g n ) o(logn) 的复杂度,这里导致缩水。那为什么仅少了 l o g n logn 了,这里我推测是 l i m n l o g n n / 2 l o g ( n / 2 ) ! lim_{n\rightarrow \infin }\frac{logn^{n/2}}{log(n/2)!} 极限造成的。
    3. 堆排序
      这里面没什么花露水,直接外层 o ( n ) o(n) ,内层 o ( l o g n ) o(logn) ,至于为什么没发生上述情况,我没有做详细的证明,但在个人推断中,建堆是从 n / 2 n/2 开始,堆排是 n n 开始,上述的 l o g n logn 无法再消掉造成的,证明和建堆证明雷同。
    4. 堆排序不会因为数据分布造成像快排一样退化,是一个优异的算法,虽然是个不稳定的,但是人家是原址的,在考虑为什么不替代快排中,考虑好后填坑(总感觉挖了很多坑)
  • 空间复杂度
    堆排序属于原址性排序,不需要额外空间
  • 稳定性
    是不稳定,原因同快速排序和简单的选择排序。

排序算法大杂烩主干文章传送门

猜你喜欢

转载自blog.csdn.net/lishang6257/article/details/83047381