堆排序

选择排序

选择排序的基本思路是:每次都从原序列中顺序查找出最小的元素,放入新的序列的下一个位置(在具体实现中,一般是还是放在原序列中,采用依次交换位置的方法)。这种最简单实现的选择排序时间复杂度为O(n2)O(n^2)。有没有效率更高的基于选择的排序算法呢?堆排序就是一种,与选择排序一样,堆排序是每次从原序列中取出最大(或最小)的元素,不同的是堆排序使用了堆这一数据结构,每次取出最大(最小)元素仅需O(log(n)),效率有所提高。

堆排序

排序方法非常多,这里的堆排序很好理解,就是指利用堆这种数据结构所设计的一种排序算法。具体排序思路如下:假定是升序排序,利用堆的性质,先由待排序数组构造最大堆,满足堆性质后,因为根节点是最大值,所以每次弹出根节点,即为有序数组,但这样描述不是十分严谨,比较严谨的描述如下:

  1. 构造最大堆。
  2. 交换数组中第一个元素(堆中根节点元素)和数组中最后一个元素,数组长度减一。
  3. 执行“下移”操作,将第一个元素“下移”到满足堆性质。
  4. 不断执行步骤2,直到最后仅剩一个元素。

时间复杂度分析:构造堆O(n),依次弹出根节点,共n次,每次的时间复杂度O(log(n)),所以有O(n+nlog(n))=O(nlog(n))。空间复杂度O(1)

具体实现

算法描述如下:

// a为带排序数组, count元素个数
procedure heapsort(a, count)
    heapify(a, count);   // 构造堆

    end = count - 1;
    while end > 0
        swap(a[end], a[0]);
        end = end - 1;
        siftDown(a, 0, end);

// 将数组堆化, a为待排序数组, count元素个数
procedure heapify(a, count) 
    // 这里元素在数组中的位置从0开始, iParent(count-1)指的是最后一个元素的父节点
    start = iParent(count-1)
    while start >= 0 do
        siftDown(a, start, count-1)
        start = start - 1   //下一个非叶子节点

// 堆根节点为start的堆进行堆化(保证父节点大于等于子节点)
procedure siftDown(a, start, end)
    root = start
    while iLeftChild(root) <= end
        // 其实这块主要是选左右子节点中最大的一个,与之交换,代码实现有很多方法,这里只是其中一种
        child = iLeftChild(root)
        swap = root 

        if a[swap] < a[child]
            swap = child
        if child+1 <= end and a[swap] < a[child+1]  // 如果右子节点存在且大于左子节点大于父节点,就设置swap为右子节点
            swap = child + 1
        if swap = root
            return  // 已满足堆性质,返回
        else
            swap(a[root], a[swap])
            root = swap

具体实现代码见heapsort.cpp

与其他排序算法的比较

堆排序经常与快排进行比较,他们的平均时间复杂度都为O(nlog(n)),但一般情况下,快排是较堆排序快一些的,分析时比较容易忽视的一点是局部性原理,这也是cache设计的依据,堆排序相比快排对“局部性”不友好,这个从siftDown的过程就可以看出来,其总是需要父子节点之间进行比较,当元素非常多时,父子节点在数组中的位置会相距非常大。


Reference:
Heapsort

发布了166 篇原创文章 · 获赞 160 · 访问量 37万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章