本節介紹三種選擇排序算法,分別為:簡單選擇排序、樹形選擇排序和堆排序。
簡單選擇排序
該算法的實現思想為:對於具有 n 個記錄的無序表遍歷 n-1 次,第 i 次從無序表中第 i 個記錄開始,找出后序關鍵字中最小的記錄,然后放置在第 i 的位置上。
例如對無序表{56,12,80,91,20}
采用簡單選擇排序算法進行排序,具體過程為:
- 第一次遍歷時,從下標為 1 的位置即 56 開始,找出關鍵字值最小的記錄 12,同下標為 0 的關鍵字 56 交換位置:

- 第二次遍歷時,從下標為 2 的位置即 56 開始,找出最小值 20,同下標為 2 的關鍵字 56 互換位置:

- 第三次遍歷時,從下標為 3 的位置即 80 開始,找出最小值 56,同下標為 3 的關鍵字 80 互換位置:

- 第四次遍歷時,從下標為 4 的位置即 91 開始,找出最小是 80,同下標為 4 的關鍵字 91 互換位置:

- 到此簡單選擇排序算法完成,無序表變為有序表。
簡單選擇排序的實現代碼為:
#include <stdio.h> #include <stdlib.h> #define MAX 9
// 單個記錄的結構體 typedef struct
{ int key; }SqNote; // 記錄表的結構體 typedef struct
{ SqNote r[MAX]; int length; }SqList;
// 交換兩個記錄的位置 void swap(SqNote *a, SqNote *b)
{ int key = a->key; a->key = b->key; b->key = key; }
// 查找表中關鍵字的最小值 int SelectMinKey(SqList *L, int i)
{ int min = i; // 從下標為 i+1 開始,一直遍歷至最后一個關鍵字,找到最小值所在的位置 while (i+1<L->length)
{ if (L->r[min].key>L->r[i+1].key)
{ min=i+1; } i++; } return min; }
//簡單選擇排序算法實現函數 void SelectSort(SqList * L)
{ for (int i=0; i<L->length; i++)
{ // 查找第 i 的位置所要放置的最小值的位置 int j = SelectMinKey(L, i); // 如果 j 和 i 不相等,說明最小值不在下標為 i 的位置,需要交換 if (i != j)
{ swap(&(L->r[i]), &(L->r[j])); } } }
int main()
{ SqList *L = (SqList*)malloc(sizeof(SqList)); L->length = 8; L->r[0].key = 49; L->r[1].key = 38; L->r[2].key = 65; L->r[3].key = 97; L->r[4].key = 76; L->r[5].key = 13; L->r[6].key = 27; L->r[7].key = 49; SelectSort(L); for (int i=0; i<L->length; i++)
{ printf("%d ",L->r[i].key); }
return 0; }
運行結果: 13 27 38 49 49 65 76 97
樹形選擇排序
樹形選擇排序(又稱“錦標賽排序”),是一種按照錦標賽的思想進行選擇排序的方法,即所有記錄采取兩兩分組,篩選出較小(較大)的值;然后從篩選出的較小(較大)值中再兩兩分組選出更小(更大)值,依次類推,直到最后選出一個最小(最大)值。同樣可以采用此方式篩選出次小(次大)值等。
整個排序的過程,可以用一棵具有 n 個葉子結點的完全二叉樹表示。例如對無序表{49,38,65,97,76,13,27,49}
采用樹形選擇的方式排序,過程如下:
- 首先將無序表中的記錄采用兩兩分組,篩選出各組中的較小值(如圖 1 中的(a)過程);然后將篩選出的較小值兩兩分組,篩選出更小的值,以此類推(如圖 1 中的(b)(c)過程),最終整棵樹的根結點中的關鍵字即為最小關鍵字:

圖 1 樹形選擇排序(一)
- 篩選出關鍵字 13 之后,繼續重復此方式找到剩余記錄中的最小值,此時由於關鍵字 13 已經篩選完成,需要將關鍵字 13 改為“最大值”,繼續重復此過程,如圖 2 所示:

圖 2 樹形選擇排序(二)
通過不斷地重復此過程,可依次篩選出從小到大的所有關鍵字。該算法的時間復雜度為O(nlogn)
,同簡單選擇排序相比,該算法減少了不同記錄之間的比較次數,但是程序運行所需要的空間較多。
堆排序
在學習堆排序之前,首先需要了解堆的含義:在含有 n 個元素的序列中,如果序列中的元素滿足下面其中一種關系時,此序列可以稱之為堆。
- ki ≤ k2i 且 ki ≤ k2i+1(在 n 個記錄的范圍內,第 i 個關鍵字的值小於第 2*i 個關鍵字,同時也小於第 2*i+1 個關鍵字)
- ki ≥ k2i 且 ki ≥ k2i+1(在 n 個記錄的范圍內,第 i 個關鍵字的值大於第 2*i 個關鍵字,同時也大於第 2*i+1 個關鍵字)
對於堆的定義也可以使用完全二叉樹來解釋,因為在完全二叉樹中第 i 個結點的左孩子恰好是第 2i 個結點,右孩子恰好是 2i+1 個結點。如果該序列可以被稱為堆,則使用該序列構建的完全二叉樹中,每個根結點的值都必須不小於(或者不大於)左右孩子結點的值。
以無序表{49,38,65,97,76,13,27,49}
來講,其對應的堆用完全二叉樹來表示為:

圖 3 無序表對應的堆
提示:堆用完全二叉樹表示時,其表示方法不唯一,但是可以確定的是樹的根結點要么是無序表中的最小值,要么是最大值。
通過將無序表轉化為堆,可以直接找到表中最大值或者最小值,然后將其提取出來,令剩余的記錄再重建一個堆,取出次大值或者次小值,如此反復執行就可以得到一個有序序列,此過程為堆排序。
堆排序過程的代碼實現需要解決兩個問題:
- 如何將得到的無序序列轉化為一個堆?
- 在輸出堆頂元素之后(完全二叉樹的樹根結點),如何調整剩余元素構建一個新的堆?
首先先解決第 2 個問題。圖 3 所示為一個完全二叉樹,若去除堆頂元素,即刪除二叉樹的樹根結點,此時用二叉樹中最后一個結點 97 代替,如下圖所示:

此時由於結點 97 比左右孩子結點的值都大,破壞了堆的結構,所以需要進行調整:首先以 堆頂元素 97 同左右子樹比較,同值最小的結點交換位置,即 27 和 97 交換位置:

由於替代之后破壞了根結點右子樹的堆結構,所以需要進行和上述一樣的調整,即令 97 同 49 進行交換位置:

通過上述的調整,之前被破壞的堆結構又重新建立。從根結點到葉子結點的整個調整的過程,被稱為“篩選”。
解決第一個問題使用的就是不斷篩選的過程,如下圖所示,無序表{49,38,65,97,76,13,27,49}
初步建立的完全二叉樹,如下圖所示:

在對上圖做篩選工作時,規律是從底層結點開始,一直篩選到根結點。對於具有 n 個結點的完全二叉樹,篩選工作開始的結點為第 ⌊n/2⌋個結點(此結點后序都是葉子結點,無需篩選)。
所以,對於有 9 個結點的完全二叉樹,篩選工作從第 4 個結點 97 開始,由於 97 > 49 ,所以需要相互交換,交換后如下圖所示:

然后再篩選第 3 個結點 65 ,由於 65 比左右孩子結點都大,則選擇一個最小的同 65 進行交換,交換后的結果為:

然后篩選第 2 個結點,由於其符合要求,所以不用篩選;最后篩選根結點 49 ,同 13 進行交換,交換后的結果為:

交換后,發現破壞了其右子樹堆的結構,所以還需要調整,最終調整后的結果為:

所以實現堆排序的完整代碼為:
#include <stdio.h> #include <stdlib.h> #define MAX 9
// 單個記錄的結構體 typedef struct
{ int key; }SqNote;
// 記錄表的結構體 typedef struct
{ SqNote r[MAX]; int length; }SqList;
//將以 r[s]為根結點的子樹構成堆,堆中每個根結點的值都比其孩子結點的值大 void HeapAdjust(SqList *H, int s, int m)
{ SqNote rc = H->r[s]; // 先對操作位置上的結點數據進行保存,放置后序移動元素丟失。 // 對於第 s 個結點,篩選一直到葉子結點結束 for (int j=2*s; j<=m; j*=2)
{ // 找到值最大的孩子結點 if (j+1<m && (H->r[j].key<H->r[j+1].key))
{ j++; } // 如果當前結點比最大的孩子結點的值還大,則不需要對此結點進行篩選,直接略過 if (!(rc.key<H->r[j].key))
{ break; } // 如果當前結點的值比孩子結點中最大的值小,則將最大的值移至該結點,由於 rc 記錄着該結點的值,所以該結點的值不會丟失 H->r[s] = H->r[j]; s = j; // s相當於指針的作用,指向其孩子結點,繼續進行篩選 } H->r[s] = rc; // 最終需將rc的值添加到正確的位置 }
// 交換兩個記錄的位置 void swap(SqNote *a, SqNote *b)
{ int key = a->key; a->key = b->key; b->key = key; }
void HeapSort(SqList *H)
{ // 構建堆的過程 for (int i=H->length/2; i>0; i--)
{ // 對於有孩子結點的根結點進行篩選 HeapAdjust(H, i, H->length); } // 通過不斷地篩選出最大值,同時不斷地進行篩選剩余元素 for (int i=H->length; i>1; i--)
{ // 交換過程,即為將選出的最大值進行保存大表的最后,同時用最后位置上的元素進行替換,為下一次篩選做准備 swap(&(H->r[1]), &(H->r[i])); // 進行篩選次最大值的工作 HeapAdjust(H, 1, i-1); } }
int main()
{ SqList *L = (SqList*)malloc(sizeof(SqList)); L->length = 8; L->r[1].key = 49; L->r[2].key = 38; L->r[3].key = 65; L->r[4].key = 97; L->r[5].key = 76; L->r[6].key = 13; L->r[7].key = 27; L->r[8].key = 49; HeapSort(L); for (int i=1; i<=L->length; i++)
{ printf("%d ", L->r[i].key); }
return 0; }
運行結果為: 13 27 38 49 49 65 76 97
提示:代碼中為了體現構建堆和輸出堆頂元素后重建堆的過程,堆在構建過程中,采用的是堆的第二種關系,即父親結點的值比孩子結點的值大;重建堆的過程也是如此。
堆排序在最壞的情況下,其時間復雜度仍為O(nlogn)
。這是相對於快速排序的優點所在。同時堆排序相對於樹形選擇排序,其只需要一個用於記錄交換(rc)的輔助存儲空間,比樹形選擇排序的運行空間更小。
總結
本節介紹了三種選擇排序:簡單選擇排序、樹形選擇排序和堆排序。樹形選擇排序的產生是考慮到為了減少簡單選擇排序過程中做比較操作的次數;堆排序的產生是考慮到樹形選擇排序在運行時申請的輔助存儲空間多。要根據特定地情況,選擇合適的選擇排序算法。