1. 程式人生 > >排序--堆排序分析與實現

排序--堆排序分析與實現

何為堆

一個數組序列我們可以將其用完全二叉樹或近似完全二叉樹(不是滿二叉樹的完全二叉樹)表示出來,當陣列下標為i時,它的父節點為(i-1)/2,左孩子為(2i+1),右孩子為(2i+2),這種對應關係說明陣列下標為0的地方也要儲存資料。(關於完全二叉樹和滿二叉樹我在這裡不做介紹)

堆是在完全二叉樹的基礎上遞迴定義的,堆分為大頂堆和小頂堆。

大頂堆:根節點的數值大於孩子節點,完全二叉樹的左右子樹同時滿足這個條件。
小頂堆:根節點的數值小於孩子節點,完全二叉樹的左右子樹同時滿足這個條件。

從這種資料結構中我們可以發現:大頂堆的根節點也就是陣列的第一個元素必定是最大值,而小頂堆必定是最小值,看到這,我想大家已經大概能感覺的到堆這種資料結構為什麼可以用來排序了。

在來看個大頂堆和小頂堆的圖解吧:
這裡寫圖片描述

堆排序的過程

要想寫出堆排序的程式碼,首先我們一定要清楚堆排序的過程,根據堆這種資料結構的特性,我總結了一下堆排序的過程:

  • 首先我們需要將一個數組初始化為堆
  • 在初始化堆的過程中我們必定要移動陣列中元素的位置
  • 初始化完成之後,如果我們建立的是大頂堆,那麼陣列中的第一個元素就是陣列的最大值,小頂堆就是最小值
  • 然後我們將最大值(最小值)和堆中的最後一個葉子節點(陣列中的最後一個元素)進行交換,是不是類似於選擇排序。可以預測到,如果是大頂堆,那麼我們將會進行升序排序,如果是小頂堆,我們將會進行降序排序
  • 在進行了上一個步驟之後我們需要重新初始化堆,然後重複以上步驟,直到迴圈結束

在上面的排序過程中,有很多細節沒有說,只是在腦中大致建立起一個堆排序的過程,下面我們仔細研究一下其中的細節。

1.堆的初始化

堆的初始化實際上就是陣列元素的移動與交換,只不過這種交換髮生在孩子節點與父節點之間。假設我們要建立的是大頂堆,我們只要保證每棵左右子樹都是堆並且都是大頂堆那麼最後整棵完全二叉樹必然是大頂堆。根據完全二叉樹的結構我們可以得到,假設我們的陣列有n個元素,那麼對應的完全二叉樹的葉子節點就有(n+1)/2個,每棵子樹的根節點的下標(0單元進行儲存)都是從(n/2)-1開始。葉子節點已經有序,可以單獨看做已經初始化好的子堆,也就說我們只要從節點(n/2)-1處開始,分別計算出當前節點的左右孩子,先拿出值最大的孩子,然後將此孩子與父節點進行比較,如果孩子節點小於父節點,說明此子樹已經是一子堆,直接考慮前一個非葉子節點,如果此孩子大於父節點,則需要將孩子節點與父節點互換後再考慮後面的結點,直至以這個節點為根的子樹是一個堆!就相當於將這個比較小的節點不斷下沉的畫面。然後再考慮前一個非葉子節點。

我再給大家一張圖,很直觀:
這裡寫圖片描述

如圖,我們不對葉子節點進行考慮,直接從36處進行調整,而我上面著重標註的那段話,就是圖d和圖e。

2.根節點的刪除

與其叫做根節點的刪除,不如說是根節點與n-i (i=1,2,3… …)處節點的互換,這樣我們就相當於每次將當前陣列的最大值放到陣列的最後面,也就是實現了升序。可以看到,每建立一次堆,下次重新初始化堆的時候節點數量都會少一,那麼當整個陣列有序的時候也就是當只有一個節點進行堆的初始化的時候。

程式碼實現

好了,堆排序的思想至此已經完全清楚了,按照這個思路我實現了大頂堆的排序:

#include<iostream>
using namespace std;

#define N 10

class Heap {
public:
    void sort(int array[], int size);
    void createHeap(int array[], int i, int size);
    void swap(int array[], int local);
};

void Heap :: swap(int array[], int local) {
    int temp = array[local];
    array[local] = array[0];
    array[0] = temp;
}

void Heap :: createHeap(int array[], int i, int size) {
    //先找到當前節點的左右孩子節點
    int l = 2*i+1;
    int r = l+1;
    int k;
    //儲存當前節點的值
    int temp = array[i];
    cout << "l: " << l << " r: " << r << endl;

    while(l < size) {
        //先找到數值較大的孩子
        if(l == size-1) {
            k = l;
        } else {
            k = (array[l] >= array[r] ? l : r);
        }

        //將孩子和父節點進行比較
        if(array[k] <= temp) {
            break ;
        } else {
            array[i] = array[k];
            i = k;
            l = 2*i+1;
            r = l+1;
        }
        array[k] = temp;
    }
}

void Heap :: sort(int array[], int size) {
    //先找到第一個非葉子節點
    int not_leafP = size/2-1;
    int local = size;

    //初始化堆
    for(int i = not_leafP; i >= 0; i--) {
        //建立子堆
        createHeap(array, i, size);
    }
    //將堆頂元素插入到陣列尾的有序區間中
    swap(array, --local);
}

int main()
{
    int array[N];
    Heap heap;

    for(int i = 0; i < N; i++) {
        cin >> array[i];
    }

    //當只有一個節點進行初始化堆的時候,陣列有序
    for(int size = N; size > 1; size--) {
        heap.sort(array, size);
    }

    for(int i = 0; i < N; i++) {
        cout << array[i] << ' ';
    }
    cout << endl;

    return 0;
}

小頂堆的實現程式碼和大頂堆沒有區別,故不在列出。

效率分析

時間複雜度:

堆排序的時間代價主要花費在建立初始堆和調整為新堆時所反覆進行的“篩選上”,由程式碼可知,我們總共建立了n-1次堆,建立新堆時總共進行的比較次數最多為{2[log2(n-1)+log2(n-2)+log2(n-3)… … +log2]} < 2n[(log2(n)],所以堆排序的時間複雜度為O(nlog2(n))。

空間複雜度:
只需要一個輔助空間,為O(1)。

最後,堆排是一種不穩定的排序。