1. 程式人生 > 其它 >簡單探尋堆排序

簡單探尋堆排序

技術標籤:演算法c++資料結構排序

堆排序

堆排序

堆排序是排序演算法中的一種,演算法時間複雜度是 O ( n l o g ( n ) ) O(n log(n)) O(nlog(n))

視訊地址

堆是電腦科學中一類特殊的資料結構的統稱,堆通常可以被看做是一棵完全二叉樹的陣列物件。
堆的例子

性質

  • 如果一個節點的位置為 k k k ,則它的父節點的位置為 k 2 \cfrac{k}{2} 2k ,而它的兩個子節點的位置則分別為 2 × k 2 \times k 2×k 2 × k + 1 2 \times k + 1
    2×k+1
  • 每個節點的值都大於或等於其左右孩子節點的值,稱為大頂堆
  • 每個節點的值都小於或等於其左右孩子節點的值,稱為小頂堆

堆排序

基本思想

  1. 將待排序序列構造成一個堆。
  2. 進行堆調整使其變為大根堆,整個序列的最大值就是堆頂的根節點。
  3. 將其與末尾元素進行交換,此時末尾就為最大值。
  4. 然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n-1個元素的次小值。如此反覆執行,便能得到一個有序序列了。(剪枝)

程式碼實現

C程式碼

#include <stdio.h>
#include <stdlib.h>

void swap(int a[],int i,int j){
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

void heapify(int tree[],int n ,int i ){
    if(i >= n)     //遞迴出口
        return;
    int c1 = 2 * i + 1;
    int c2 = 2 * i + 2;
    int max = i;
    if(c1 < n && tree[c1] > tree[max] )
        max = c1;
    if(c2 < n && tree[c2] > tree[max])
        max = c2;
    if(max != i){
        swap(tree,max,i);  //將最大值移動至父節點
        heapify(tree,n,max); //遞迴維護下面的節點
    }
}

void build_heap(int tree[],int n){   //構造堆
    int last_node = n - 1;
    int parent = (last_node - 1) / 2; //求出最後一個父節點
    for(int i = parent;i >= 0;i--){ //向上維護節點
        heapify(tree, n , i);
    }
}

void heap_sort(int tree[],int n){
    build_heap(tree,n);f
    for(int i = n - 1;i >= 0;i--){  //每次取出根節點後,堆減少一個節點
        swap(tree,i,0);
        heapify(tree,i,0);
    }
}

int main(){
    int tree[] = {2, 5, 3, 1, 10, 4};
    int n = 6;
    heap_sort(tree,n);
    for(int i = 0;i < n;i++){
        printf("%d ",tree[i]);
    }

    return 0;
}

思路

一、heapify:堆調整

  1. 針對節點 i,將其兩個子節點找出來,此三個節點構成一個最小單位的完全二叉樹(越界的忽略)
  2. 找到這個最小單位的完全二叉樹 的最大值,並將其交換至父節點的位置
  3. 遞迴呼叫,維護交換後 子節點與其子節點被破壞的堆關係,遞迴出口為葉節點

堆調整是自頂向下進行調整,無需考慮下標i小於0的情況

二、 build_heap:構造堆

  1. 用一維陣列表示:堆

    a [ i ] a[ i ] a[i] 的父節點為 a [ i − 1 2 ] a[\cfrac{i-1}{2}] a[2i1],兩個子節點為 a [ 2 × i + 1 ] a[2 \times i + 1]

    a[2×i+1] a [ 2 × i + 2 ] a[2 \times i + 2] a[2×i+2]

  2. 先找到最後一個父節點,再從最後一個父節點開始向上呼叫 heapify維護節點。

三、heap_sort:利用堆進行堆排序

儘管我們可以構造出大根堆,但是我們也發現,大根堆並沒有排好序

大根堆是根節點的值最大,這時我們可以將根節點移走,即剪去根節點,並將剩餘的再次構造一個新堆,迴圈往復,就進行了排序操作。

那麼,拿走根節點後,根節點的數值又應該由誰取代?

結合剛剛的分析,我們知道,該節點需滿足:移動到根節點對堆的破壞最小。那麼,結果便很明瞭了,最後一個節點是最好的選擇。

我們可以用最後一個節點替換根節點,此時根節點和第二層的兩個節點構成一個最小單位的最小二叉樹,此時就可以直接呼叫 heapify函式,形成一個新的堆。但是我們並沒有必要使用 build_heap函式,因為此時最大值就在第二層,根節點會因為遞迴維護被替換到最後一層,其他節點也會因為遞迴維護而得到調整。

最後,經過堆排序,數值便從小到大排序,同時也構成了小根堆。

後記

C語言需要我們自行手寫出程式碼。而實際上,許多程式語言並非需要我們手寫堆排序。

在這裡,我們可以引入一個名為優先佇列的資料結構。

優先佇列

優先佇列可以完成以下操作:

  • 插入一個數值
  • 取出最小的數值(獲得數值,並且刪除)

經過剛剛堆排序的知識,我們很容易就會發現優先佇列與堆排序的相似性。可以說,優先佇列進行了大根堆的堆排序,在輸出時按從大到小的順序輸出。

在C++中,STL裡的priority_queue就是其中之一。

#include <queue>
#include <cstdio>
using namespace std;
priority_queue<int> p;

int main{
    //插入元素
    p.push(3);
    p.push(5);
    p.push(1);

    //不斷迴圈直至空為止
    while(!p.empty()){
        //獲取並刪除最大值
        printf("%d\n",p.top());
        p.pop();
    }

    return 0;
}