堆排序 HeapSort的C實現

堆的結構與特性

堆排序,是通過想象一種類似二叉樹結構的數組而來對數據進行排序的算法,這種堆也叫二叉堆。二叉堆中的數據排列可以想象成從頂端開始,逐層遍歷一個完全二叉樹得到的結果。

HeapStruct

與完全二叉樹相同,高度爲h的二叉堆允許且只允許在第h層的最右邊缺少節點。也就是說,二叉堆不要求必須填滿整個數組,但所有的節點都必須是連續的。

二叉堆有兩個特性:

  • 父節點的鍵值總是大於(或小於)所有子節點的鍵值
  • 每個節點的子節點都是二叉堆

當一個二叉堆的父節點總是大於它的子節點時,這是一個最大堆,反之則是最小堆。

二叉堆通常用數組來表示,因此它聯繫父節點與子節點的方式也是數組下標的計算。上圖中的數組空出了0號下標,但這並不是必須的,二叉堆可以從0開始也可以從1開始,只是兩種儲存方式的下標計算略有不同。以下標爲i的節點爲例:

  • 從下標0開始的堆:
int getLeft(int i) { // left = i * 2 + 1
    return i * 2 + 1;
}
int getRight(int i) { // right = i * 2 + 2
    return i * 2 + 2;
}
int getRoot(int i) { // root = (i - 1) / 2
    return (i - 1) / 2;
}
  • 從下標1開始的堆:
int getLeft(int i) { // left = i * 2
    return i * 2;
}
int getRight(int i) { // right = i * 2 + 1
    return i * 2 + 1;
}
int getRoot(int i) { // root = i / 2
    return i / 2;
}

當然,所有的下標計算都是向下取整的整數運算。

另外,在堆中有兩個可能易於混淆的概念:堆的length和size。通常堆的length是指數組被聲明的長度,通常用變量n表示,而堆的size則是指數組中的有效數據,也就是堆中實際存在的節點數量。

構造一個新的堆

要建立一個堆,最直觀的辦法就是建立一個空堆,隨後依次插入所有元素。本文中將以從下標0開始的最大堆爲例,首先來看堆的插入操作。

由於數組的特性,堆只能在尾部插入新元素。又根據最大堆的性質:父節點的鍵值總是大於所有子節點的鍵值。想象在堆的尾部插入一個新節點的過程:插入節點,將它與父節點比較,因爲父節點總是大於所有子節點,所以若新節點大於該父節點,則它也會大於該父節點當前所有的子節點,則新節點取代該父節點(新節點與父節點位置互換),再繼續與上一層父節點比較。而二叉堆中除了父子節點之間相對的大小關係之外並不存在其他關係(如左右子節點之間沒有大小關係),因此完成這個從底向上的比較更新(也稱爲up-heap,heapify-up等)之後,節點便完成插入了。

// 兩個節點的交換函數
void swap(int heap[], int a, int b) {
    int temp = heap[a];
    heap[a] = heap[b];
    heap[b] = temp;
}
​
// 需要注意的是,heapifyUp函數只執行更新節點位置的操作,單純的節點插入需要在此之前完成
void heapifyUp(int heap[], int i) { // i爲已被插入堆的新節點的下標
    int temp;
    while (i > 0 && heap[i] > heap[getRoot(i)]) { // 當i爲0時到達堆頂
        swap(heap, i, getRoot(i));
        i=getRoot[i];
    }
}

不如試試原址建堆

建立空堆再逐個插入新節點雖然是一個辦法,但是這樣也花費了一倍的空間,當數組比較大的時候就不太合適了。此時則可以直接對整個原始數組進行堆排序。

對整個亂序數組進行排序是一個大工程,不如先考慮一個最大堆中混入了一個無序元素i的情況:heap[]本身是一個最大堆,則不和諧分子i的左右子節點getLeft(i)與getRight(i)都是最大堆,但是我們仍需要確定i是否真的大於它的兩個子節點,如果不,就需要調整它們的位置來維護最大堆的性質。我們可以構造一個叫做maxHeapify的函數來完成這個操作,它接受堆數組heap[],heap[]中的節點數size和新節點的下標i作爲輸入:

// 維護最大堆性質:在i與它的兩個子節點中選出最大的節點作爲父節點
void maxHeapify(int heap[], int size, int i) {
    int largest = i;
    if (getLeft[i] < size && heap[getLeft(i)] > heap[largest]) largest = getLeft(i);
    if (getRight[i] < size && heap[getRight(i)] > heap[largest]) largest = getRight(i);

    if (largest != i) { // 如果i不是合格的父節點,則被較大的子節點largest所替代
        swap(heap, largest, i);
        maxHeapify(heap, largest); // 繼續比較i(此時爲largest)與它新的子節點們
    }
}

可見maxHeapify函數每一次運行都可以逐層向下維護一個根節點(原本的子節點必須有序)。可以想到,如果我們自底向上地使用這個方法遍歷一個無序數組中所有的非葉節點(葉節點可以被視爲只有單個元素的最大堆),就可以將數組轉換爲最大堆了。

從0開始長度爲n的數組的葉節點,也就是二叉堆的最底層,可以通過簡單的計算知道是下標從[n / 2]到[size - 1]的子數組,則非葉節點即是[0]到[n / 2 - 1]的所有節點。那麼現在就來遍歷它們吧:

void buildMaxHeap(int heap[], int n, int size) {
    for(int i = n / 2 - 1; i >= 0; i --) {
        maxHeapify(heap, size, i);
    }
}

堆排序

說完了堆的性質、構造與建立,終於到了本章的主題:堆排序。堆排序的過程主要如下:

  • 建立最大堆(buildMaxHeap)
  • 取出根節點,即交換根節點(heap[0])和尾節點(heap[size - 1])後,從堆中刪除尾節點(可通過將size值減一來完成)
  • 對新的根節點進行維護(maxHeapify(heap, size - 1, 0))

代碼實現如下:

void HeapSort(int heap[], int n, int size) {
    bulidMaxHeap(heap, n, size);
    for(int i = size - 1; i > 0; i --) {
        swap(heap, 0, i);
        size --;
        maxHeapify(heap, size, 0);
   }
}

因爲不斷取出最大堆的根節點(最大節點)並且從數組尾部開始,依次向前排列放置,最終將得到一個升序數組。同理,最小堆將得到降序數組。

終於!!寫完了!!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章