排序问题之堆排序

排序问题

算法问题的基础问题之一,便是排序问题:

  输入:n个数的一个序列,<a1, a2,..., an>。

  输出:一个排列<a1',a2', ... , an'>,满足a1' ≤ a2' ≤... ≤ an' 。(输出亦可为降序,左边给出的例子为升序)

一.算法描述

    (1)堆

  二叉堆:是一个数组,能近似的看做完全二叉树。除了最底层,树是完全充满的,而且从左至右填充。

  最大堆:除了根节点外,所有的节点i都满足A[parent(i)] ≥A[i]的堆。

  最小堆:除了根节点外,所有的节点i都满足A[parent(i)] ≤A[i]的堆。

  例如下图中,如果我们将数组记作A[1~6],那么将数组中的六个元素依次由上至下由左至右得添加到完全二叉树中,就能得到下面的堆,而且恰好是一个最大堆。         

           使用这种方法可以总结出如下规律:

    i的父节点 parent(i) = ⌊i / 2⌋

    i的左子节点 left(i) = 2i

          i的右子节点 right(i) = 2i + 1

    (这里由于书本上是用起始下标为1来讲解的,本文的讲解和代码都基于arr[1]来存放序列的第一个元素,且主要说明最大堆的排序过程)

    (2)维护堆的性质Max-Heapify

      对每一个非叶子节点i,假设以其左子节点和右子节点为根的两个堆都是最大堆,如果要保持以i为根的堆也为最大堆,就要保证A[i]不小于A[left(i)]和A[right(i)],如果A[i]不是三者最大,就要将A[i]与largest = max(A[left(i)], A[right(i)])交换,但是交换后可能破坏A[largest]为根的堆是最大堆的性质,所以还需要对交换后的节点调用Max-Heapify。由于最多将根节点交换到最底层的叶子节点,时间复杂度为O(H),H为树的高度。

  (3)建堆

  自底向上地对所有的非叶子节点使用Max-Heapify,最终就能得到一个最大堆。

  (4)堆排序

  由于最大堆的根节点是堆中的最大值,所以我们每次将根取出后,补上一个叶子节点,然后使用Max-Heapify让剩余元素的最大值继续交换到根节点,循环下去取出的值就是降序排列的。

二.代码实现

      下面是插入排序的C++实现:

#include<iostream>
#include<vector>
using namespace std;
//三个内联函数,用来求每个节点的父、左子、右子节点
inline int left(int parent)
{
    return parent << 1;
}

inline int right(int parent)
{
    return 1 + (parent << 1);
}

inline int parent(int child)
{
    return child >> 1;
}

//维护最大堆的性质
void Max_Heapify(vector<int> &v, int i)
{
    int size = v.size() - 1;   
    int largest;
    int l = left(i);
    int r = right(i);
    //左子大于父节点,large = l
    if(l <= size && v[l] > v[i])    
        largest = l;
    else                            
        largest = i;
    
    //large取右子和左子、父中的较大值节点
    if(r <= size && v[r] > v[largest]){
        largest = r;
    }

    if(largest != i){
        swap(v[i],v[largest]);
        Max_Heapify(v,largest);
    }
}

//从最大parent节点往上维护最大堆性质,就能形成最大堆
void Build_Max_Heap(vector<int> &v)
{   
    //最后一个子节点v.size()/2向下取整是号码最大的parent节点,所以直接从这里count down
    for(int i = v.size()/2; i > 0; i--)
    {
        Max_Heapify(v,i);
    }
}
//堆排序
void Heapsort(vector<int> &v)
{
    vector<int> ret;
    
    Build_Max_Heap(v);
    
    for(int i = v.size()-1; i > 1; i--)
    {
        swap(v[i], v[1]);
        ret.push_back(v[i]);
        v.pop_back();
        Max_Heapify(v,1);
    }
    ret.push_back(v[1]);
    
    v = ret;
}

int main()
{
    
    //arr为要排序的数组
    int arr[] = {5,2,4,7,10,9,8,1,6,3};

    //把数组放入向量中,首部添0模拟图示中的序号
    vector<int> v(arr, arr + sizeof(arr)/sizeof(int));
    v.insert(v.begin(),0);
    
    //对向量使用堆排序
    Heapsort(v);

    //按顺序打印排序后向量中的所有元素
    copy (v.begin(), v.end(), ostream_iterator<int> (cout, " "));
    cout << endl;
    
}
View Code

 

三.算法分析

(1)时间复杂度

   由于建堆时对所有的非叶子节点都要进行一次Max-Heapify,复杂度为O(h),而高度为h的节点不超过⌈n/pow(2,h+1)⌉,可计算出建堆的复杂度为O(n)。 然后要进行n-1次取最大值并Max-Heapify操作,每一次的复杂度为O(H) = O(logn),所以算法的总共时间复杂度为O(nlogn)。

(2)稳定性

   不稳定。

(3)适合范围

        适合于相对有序的序列。还可以用最大堆实现最大优先队列,最小堆实现k个非降序有序链表的k路归并。