C語言堆排序(HeapSort)的思想和代碼實現

C語言堆排序(HeapSort)的思想和代碼實現

經過一晚上和有一早上的思考和學習,在Clion上反覆的單步調試之後,我總結了關於堆排序這個算法的一點體會。現在來記錄一下,如有錯誤,歡迎批評指出,謝謝!

首先:什麼是堆排序,爲什麼叫堆?

Heapsort是一種根據選擇排序的思想利用堆這種數據結構 所設計的一種排序算法

選擇排序的思想是什麼?每一趟比較找到這個序列中的最值,拿出來和最前面的元素交換,交換完之後,這個序列從前面開始減去一個(因爲前面放的是最值,不需要放在序列裏再次比較)

那麼這裏的堆是什麼意思呢?:堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

什麼是完全二叉樹?即,每個節點都一一有序對應的滿二叉樹,如下圖所示

當一個序列滿足 雙親位置的值 大於或者小於 孩子位置的值的時候,就滿足堆的關係,(這裏爲什麼要叫做位置?因爲一般都是在序列裏面排序,儲存是線性的,比大根堆和小根堆如順序表)

#堆的種類,大根堆和小根堆

這個好理解,就是對應上面的圖來說,雙親位置的值 大於 孩子位置的值 就是大根堆

雙親位置的值 小於 孩子位置的值 就是小根堆

 

實現堆排序,我們需要解決什麼問題?

  1. 怎麼創建一個初始的堆?即滿足 雙親位置的值 大於或者小於 孩子位置的值,我們只需要關注每一個雙親的孩子是不是大於或者小於自己孩子,而不需要去管“別人家的孩子”是不是比自己家孩子大或者小
  2. 有了初始的堆之後,我們怎麼調整剩下的元素,這個時候就需要看看“別人家的孩子”,這樣處理之後,這個二叉樹就滿足完全二叉樹的特點,按照序列排列下來就是一個有序的序列

 

Part1:創建初始堆,我們要考慮什麼?

既然我們不需要去管整個序列是否有序,不需要去管“別人家的孩子”怎麼樣,那麼我們先要

找到所有雙親節點。

怎麼找呢?根據完全二叉樹的性質:

觀察每個雙親節點的序號,我們不難發現,他們的孩子節點的序號都是滿足:比如雙親節點是i,那麼他的左孩子就是2*i,右孩子就是2*i+1。

找到之後我們就開始調整每一個雙親位置的值和她的孩子的值:

我們這裏以創建一個小根堆爲例子:

我們遍歷調整每個雙親節點的順序是:

從最大的雙親節點(非終端節點)((整個順序表的長度)/2)一直倒着來,直到下標爲1的根節點

爲什麼是這個順序?爲什麼不能倒着來?從1~length/2不是一樣的麼?

其實是不一樣的,我們創建小根堆的目的,就是爲了將最小的交換到根節點,也就是說,最後調整完初始堆

我們的根位置的值一定是整個序列中最小的值  —— 這是很重要的性質

我們從length/2開始對每個雙親位置進行堆的調整,那麼到了最後,最小的元素會出現在根位置

如果從1開始一直調整到length/2的雙親位置,那麼整個序列中最小的元素,不一定會出現在根位置,因爲第一次調整之後根位置的值就不再變了,只是第一個雙親位置的最小的元素。

這裏有一個根據無序序列(62,25,49,25,16,8)創建小根堆的例子,順序如下

Part2:得到了初始小根堆,我們怎麼調整剩下的堆使得它有順序

根據前面提到的選擇排序的思想:

我們在這個heapsort裏面怎麼體現這種思想呢?

前面創建初始堆的時候,我們已經把最小的元素排出來,放在根位置了。那麼我們就相當於是拿到了選擇排序中的最值,這個時候我們只需要把他放在某個位置上之後,接下去就不再管它了,我們把它從序列中隔過去,在接下去的“找最值”的過程中把它忽視過去。這個“找最值”的過程就是上面Part 1 所說的,創建初始堆的過程

我們這裏算法的操作過程就是:

  1. 拿到最上面的根位置的值,和序列(長度n)最後一個元素交換位置。
  2. 然後把這個序列從後面縮小一個(序列長度n-1),也就是說,把剛剛那個元素隔過去
  3. 對剩下的這個被打亂的堆,再次進行Part 1的初始堆調整,我們還是想要得到剩下序列中最小的值

 ......(循環往復)直到 這個序列的長度變成1 這個堆排序就執行完畢,得到了一個有序的序列。

還是上面那個(62....)的序列,我們從上面得到的小根堆開始調整到有序序列的例子

 

#到此爲止,這個堆排序就算是理解完畢了,具體怎麼實現,在下面的代碼中根據代碼再次理解一次

1,創建順序表,由一個int數組和一個指示長度的元素構成:

注意:這個數組是從下標爲1的地方開始儲存數據的!

注意:這個數組是從下標爲1的地方開始儲存數據的!

注意:這個數組是從下標爲1的地方開始儲存數據的!

#include "stdio.h"
#define Max_Num 100

typedef struct {
    int record[Max_Num];
    int length;
}OrderList;

2,還需要一個創建順序表的函數

這個比較簡單,也就是數組的賦值,別忘了給長度的元素賦值

OrderList CreatOrderList(int n){
    int i;
    OrderList orderList;
    orderList.length = n;
    for(i=1;i<=n;i++){
        scanf("%d",&orderList.record[i]);
    }
    return orderList;
}

3,先簡單看一下main函數的調用結構吧

首先輸入長度,然後進入創建順序表的函數之後得到一個無序的順序表。

對這個順序表進行核心的 堆排序操作 ,這裏傳送一個指針過去

然後我們把這個順序表輸出查看一下就行,printOrderList這個函數的代碼會在後面給出

int main( )
{
    int i,j;
    int n;
    printf("輸入序列長度");
    scanf("%d",&n);
    printf("輸入序列元素");

    OrderList orderList = CreatOrderList(n);
    HeapSort(&orderList);
    printOrderList(orderList);
    return 0;
}

4,最最最核心的堆排序代碼部分

這個部分分成兩個函數,一個是 void HeapSort(OrderList *list)這個函數控制整個堆排序算法的流程,也就是上面所說的part1,2

先創建初始堆,再遞歸調整剩餘堆的這樣兩個操作。

HeapAdjust(OrderList *list, int s, int m)這個函數功能就很清楚明白,對傳入的順序表,以及傳送的參數index(對應這次調整的開始位置),參數length(對應這次調整的順序表的長度)。HeapAdjust在整個流程中有兩種調用,一個是開始的創建初始堆,一個是後面的遞歸調整。

/**
 * 這個函數有兩個功能,一個是創建堆,一個是調整剩下節點
 * @param list
 * @param s
 * @param m
 */
void HeapAdjust(OrderList *list, int index, int length) {
    //保存傳入節點的值
    int rc;
    int j;
    rc = list->record[index];
    for(j = 2*index;j<=length;j*=2){
        //如果左子樹(j=s*2)比右子樹j+1的大,說明右子樹更需要和雙親節點交換,則移動到record[j+1];
        if((j<length)&&(list->record[j]>list->record[j+1])){
            j++;  //下標移動
        }
        //如果孩子節點的值比雙親節點的值大,說明順序正確,不用交換,退出循環
        if(rc<list->record[j]){
            break;
        }
        //否則說明孩子節點值比雙親節點的小,交換
        list->record[index] = list->record[j];
        //如果換了,說明原來的雙親節點的數值被j的值覆蓋,
        //s的下標應該指向原來交換的地方(子節點)
        index = j;
    }
    //原來交換的地方(子節點)應該是原來雙親節點的值,之前被rc保存,現在取出
    list->record[index] = rc;
}

void HeapSort(OrderList *list){
    int i;
    int temp;
    //循環第一次找到最後一個非葉子節點,循環下一次找到倒數第二個非葉子節點......
    for(i=list->length/2 ; i>0;--i){
        HeapAdjust(list,i,list->length);
    }
    /**
     * 把堆底元素和堆頂元素進行交換之後,刪除最後一個節點,對剩下的節點進行堆調整
     */
    for(i=list->length;i>1;--i){
        temp = list->record[1];
        list->record[1] = list->record[i];
        list->record[i] = temp;
        HeapAdjust(list,1,i-1);
    }
}

 

#完整代碼如下:

包括順序表的創建,輸出,HeapSort和HeapAdjust

能實現的功能就是給定長度的順序表進行堆排序並輸出

#include "stdio.h"
#define Max_Num 100

typedef struct {
    int record[Max_Num];
    int length;
}OrderList;

void printOrderList(OrderList list){
    int i;
    for(i = 1;i<=list.length;i++){
        printf("%d ",list.record[i]);
    }
}
OrderList CreatOrderList(int n){
    int i;
    OrderList orderList;
    orderList.length = n;
    for(i=1;i<=n;i++){
        scanf("%d",&orderList.record[i]);
    }
    return orderList;
}

/**
 * 這個函數有兩個功能,一個是創建堆,一個是調整剩下節點
 * @param list
 * @param s
 * @param m
 */
void HeapAdjust(OrderList *list, int index, int length) {
    //保存傳入節點的值
    int rc;
    int j;
    rc = list->record[index];
    for(j = 2*index;j<=length;j*=2){
        //如果左子樹(j=s*2)比右子樹j+1的大,說明右子樹更需要和雙親節點交換,則移動到record[j+1];
        if((j<length)&&(list->record[j]>list->record[j+1])){
            j++;  //下標移動
        }
        //如果孩子節點的值比雙親節點的值大,說明順序正確,不用交換,退出循環
        if(rc<list->record[j]){
            break;
        }
        //否則說明孩子節點值比雙親節點的小,交換
        list->record[index] = list->record[j];
        //如果換了,說明原來的雙親節點的數值被j的值覆蓋,
        //s的下標應該指向原來交換的地方(子節點)
        index = j;
    }
    //原來交換的地方(子節點)應該是原來雙親節點的值,之前被rc保存,現在取出
    list->record[index] = rc;
}

void HeapSort(OrderList *list){
    int i;
    int temp;
    //循環第一次找到最後一個非葉子節點,循環下一次找到倒數第二個非葉子節點......
    for(i=list->length/2 ; i>0;--i){
        HeapAdjust(list,i,list->length);
    }
    /**
     * 把堆底元素和堆頂元素進行交換之後,刪除最後一個節點,對剩下的節點進行堆調整
     */
    for(i=list->length;i>1;--i){
        temp = list->record[1];
        list->record[1] = list->record[i];
        list->record[i] = temp;
        HeapAdjust(list,1,i-1);
    }
}

int main( )
{
    int i,j;
    int n;
    printf("輸入序列長度");
    scanf("%d",&n);
    printf("輸入序列元素");

    OrderList orderList = CreatOrderList(n);
    HeapSort(&orderList);
    printOrderList(orderList);
    return 0;
}

 

#總結:

這篇blog其實主要是是捋了捋堆排序的思路和實現過程,沒有闡述堆排的優缺點和應用之類的話題,接下去的複習應該多注意一下

                                                                                                                                             2018年12月9日 14點07分

 

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