第10章 內排序
1、排序的基本概念
- 排序:假設一個文件由 \(n\) 個記錄 \(R_1,R_2,\cdots,R_n\) 組成,以記錄中某個(或幾個)字段值不減(或不增)的次序把這 \(n\) 個記錄從新排列,稱該字段爲排序碼
- 關鍵碼:能惟一標識一個記錄的字段,關鍵碼能夠做爲排序碼,但排序碼不必定要是關鍵碼
- 穩定排序算法:任意兩個排序碼相同的記錄 \(R_i\) 和 \(R_j\),若是排序前 \(R_i\) 在 \(R_j\) 的前面,排序後 \(R_i\) 還在在 \(R_j\) 的前面,則該算法是穩定的排序算法,不然是不穩定的排序算法
- 注:如下討論的排序,都是以記錄中某個字段值不減的次序把這 \(n\) 個記錄從新排列,且排序碼數據類型爲整型
1.1 排序算法的記錄存儲結構定義
#define MAXSIZE 100 // 文件中記錄個數的最大值
typedef int keytype; // 定義排序碼類型爲整數類型
typedef struct {
keytype key;
// int other; // 此處還能夠定義記錄中除排序碼外的其它域
} recordtype; // 記錄類型的定義
typedef struct {
recordtype r[MAXSIZE + 1];
int length; // 待排序文件中記錄的個數
} table; // 待排序文件類型
2、插入排序
2.1 直接插入排序
-
算法步驟:算法
- 初始認爲文件中的第 \(1\) 個記錄已排好序
- 而後把第 \(2\) 個到第 \(n\) 個記錄依次插入到已排序的記錄組成的文件中
- 對第 \(i\) 個記錄 \(R_i\) 進行插入時,\(R_1,R_2,\cdots,R_{i-1}\) 已排序
- 把記錄 \(R_i\) 的排序碼 \(key_i\) 和已經排好序的排序碼從右向左依次比較,找到 \(R_i\) 應插入的位置
- 把該位置之後直到 \(R_{i-1}\) 的記錄順序後移,空出位置讓 \(R_i\) 插入
- 注:當待插入排序小於全部已排序的排序碼時,可能會出現下標越界,所以能夠在排序文件的第一個排序碼前插入一個值做爲哨兵
-
圖10-2-直接插入排序算法的執行過程示意圖:shell
2.1.1 直接插入排序算法(算法)
void insertsort(table *tab) {
int i, j;
// 依次插入從第2個開始的全部元素
for (i = 2; i <= tab->length; i++) {
j = i - 1;
tab->r[0].key = tab->r[i].key; // 設置哨兵,準備找插入位置
// 找插入位置並後移
while (tab->r[0].key < tab->r[j].key) {
tab->r[j + 1].key = tab->r[j].key; // 後移
j = j - 1; // 繼續向前(左)查找
}
tab->r[j + 1].key = tab->r[0].key; // 插入第i個元素的副本,即前面設置的哨兵
}
}
時間複雜度:\(O(n^2)\)數組
2.2 二分法(折半)插入排序
-
算法步驟:spa
- 注:依據直接插入排序的思想,進行擴充
- 在找第 \(i\) 個記錄的插入位置時,因爲前 \(i-1\) 個記錄已排好序,所以在插入過程當中使用二分法肯定 \(key[i]\) 的插入位置
-
算法代碼:略設計
2.3 表插入排序(大綱未規定)
- 略
2.4 希爾(Shell)插入排序
- 算法步驟:
- 對 \(n\) 個記錄進行排序,首先取一個整數 \(d<n\),把這個 \(n\) 個記錄分紅 \(d\) 組
- 全部位置相差爲 \(d\) 的倍數的記錄分在同一組
- 在每組中使用直接插入排序進行組內排序
- 縮小 \(d\) 的值
- 重複進行分組和組內排序
- 知道 \(d=1\) 結束排序
- 算法代碼:略
- 圖10-5-Shell插入排序示意圖:
3、選擇排序
- 選擇排序基本思想:每次從待排序文件中選擇出排序碼最小的記錄,把該記錄放入已排序文件的最後一個位置
3.1 直接選擇排序
- 算法步驟:
- 首先從 \(n\) 個記錄中選擇排序碼最小的記錄,讓該記錄和第 \(1\) 個記錄交換
- 再從剩下的 \(n-1\) 個記錄選出排序碼最小的記錄,讓該記錄和第 \(2\) 個記錄交換
- 重複這樣的操做直到剩下最後兩個記錄
- 從最後兩個記錄中選出最小的記錄和第 \(n-1\) 個記錄交換
- 結束
- 圖10-6-直接選擇排序算法執行過程:
3.1.1 直接選擇排序算法(算法)
void simpleselectsort(table *tab) {
int i, j, k;
for (i = 1; i <= tab->length - 1; i++) {
k = i; // 記下當前最小元素的位置
// 向右查找更小的元素
for (j = i + 1; j <= tab->length; j++)
if (tab->r[j].key < tab->r[k].key) k = j; // 修改當前最小元素的位置
// 若是第i次選到的最小元素位置k不等於i,則將第k、i個元素交換
if (k != i) {
tab->r[0].key = tab->r[k].key; // 以第0個元素做爲中間單元進行交換
tab->r[k].key = tab->r[i].key;
tab->r[i].key = tab->r[0].key;
}
}
}
時間複雜度:\(O(n^2)\)3d
3.2 樹型選擇排序(大綱未規定)
3.3 堆排序
-
堆排序解決的問題:保存中間比較結果,減小後面的比較次數,又不佔用大量的附加存儲空間指針
-
堆:一個序列 \(\{k_1,k_2,\cdots,k_n\}\),其中 \(k_i\leq{k_{2i}}\) 且 \(k_i\leq{k_{2i+1}}\),當 \(i=1,2,\cdots,n/2\) 且 \(2i+1\leq{n}\)code
- 注:當採用順序存儲這個序列,就能夠把這個序列的每個元素 \(k_i\) 當作是一顆有 \(n\) 個結點的徹底二叉樹的第 \(i\) 個結點,其中 \(k_1\) 是該二叉樹的根結點
-
堆的特徵:把堆對應的一維數組看做一顆徹底二叉樹的順序存儲,則該徹底二叉樹中任意分支結點的值都小於或等於它的左右兒子結點的值,且根結點的值是全部元素中值最小的blog
-
最小堆和最大堆:當父節點的值老是大於等於任何一個子節點的值時爲最大堆; 當父節點的值老是小於等於任何一個子節點的值時爲最小堆排序
-
堆排序使用條件:當對記錄 \(n\) 較大的文件,堆排序很好,當 \(n\) 較小時,並不提倡使用,由於初始建堆和調整建新堆時須要進行反覆的篩選
-
圖10-8-一個序列 Arr = {5, 1, 13, 3, 16, 7, 10, 14, 6, 9}
和相應的徹底二叉樹:
-
構造最大堆步驟(經過已有徹底二叉樹構建堆):
- 主要思想:判斷有子女結點的子樹是否知足最大(小)堆的要求
- 首先咱們須要找到最後一個結點的父結點如圖 \((a)\),咱們找到的結點是 \(16\),而後找出該結點的最大子節點與本身比較,若該子節點比自身大,則將兩個結點交換。圖 \((a)\) 中,\(16\) 是最大的結點,不須要交換
- 咱們移動到第下一個父結點 \(3\),如圖 \((b)\) 所示。同理作第一步的操做,交換了 \(3\) 和 \(14\),結果如圖 \((c)\) 所示
- 移動結點到下一個父結點 \(13\),如圖 \((d)\) 所示,發現不須要作任何操做
- 移動到下個父結點 \(1\),如圖 \((e)\) 所示,而後交換 \(1\) 和 \(16\),如圖 \((f)\) 所示,此時咱們發現交換後,\(1\) 的子節點並非最大的,咱們接着在交換,如圖 \((g)\)所示
- 移動到父結點到 \(5\),一次重複上述步驟,交換 \(5\) 和 \(16\),在交換 \(14\) 和 \(5\),在交換 \(5\) 和 \(6\) 全部節點交換完畢,最大堆構建完成
-
圖10-9-建堆過程示意圖:
-
堆排序步驟:
- 構建一個篩選算法,該算法能夠把一個任意的排序碼序列建成一個堆,堆的第一個元素就是排序碼中最小的
- 把選出的最小排序碼從堆中刪除,對剩餘的部分從新建堆,繼續選出其中的最小者
- 直到剩餘 \(1\) 個元素時,排序結束
4、交換排序
- 交換排序:對待排序記錄兩兩進行排序碼比較,若不知足排序順序,則交換這對排序碼
4.1 冒泡排序
- 冒泡排序算法步驟:
- 對全部記錄從左到右每相鄰兩個記錄的排序碼進行比較,若是這兩個記錄的排序碼不符合排序要求,則進行交換,這樣一趟作完,將排序碼最大者放在最後一個位置
- 對剩下的 \(n-1\) 個待排序記錄重複上述過程,又將一個排序碼放於最終位置,反覆進行 \(n-1\) 次,可將 \(n-1\) 個排序碼對應的記錄放至最終位置,剩下的即爲排序碼最小的記錄,它在第 \(1\) 的位置處
- 注:若是在某一趟中,沒有發生交換,則說明此時全部記錄已經按排序要求排列完畢,排序結束
- 圖10-10-冒泡排序算法示意圖:
4.2 快速排序
- 快速排序算法步驟(主要經過首尾指針的移動來劃分大於 \(x\) 和小於 \(x\) 的值):
- 從 \(n\) 個待排序的記錄中任取一個記錄(不妨取第 \(1\) 個記錄)
- 設法將該記錄放置於排序後它最終應該放的位置,使它前面的記錄排序碼都不大於它的排序碼,然後面的記錄排序碼都大於它的排序碼
- 而後對前、後兩部分待排序記錄重複上述過程,能夠將全部記錄放於排序成功後的相應位置,排序即告完成
- 圖10-11-一次劃分的過程:
5、歸併排序
-
歸併排序算法基本思路:一個待排序記錄構成的文件,能夠看做是有多個有序子文件組成的,對有序子文件經過若干次使用歸併的方法,獲得一個有序文件
-
歸併:把兩個(或多個)有序子表合併成一個有序表的過程
-
一次歸併:把一個數組中兩個相鄰的有序數組歸併爲一個有序數組段,其結果存儲於另外一個數組
-
一趟歸併:\(n\) 個元素的數組中,從第 \(1\) 個元素開始,每連續 \(len\) 個元素組成的數組段是有序的,從第一個數組段開始,每相鄰的兩個長度相等的有序數組段進行一次歸併,一直到剩餘元素個數小於 \(2*len\)時結束。若是剩餘元素個數大於 \(len\),能夠對一個長度爲 \(len\),另外一個長度小於 \(len\) 的兩個有序數組實行一次歸併;若是其個數小於 \(len\),則把這些元素直接拷貝到目標數組中
-
圖10-13-一趟歸併的圖示:
-
二路歸併排序算法步驟:
- 對任意一個待排序的文件,初始時它的有序段的長度爲 \(1\)
- 經過不斷調用一趟歸併算法,使有序段的長度不斷增長
- 直到有序段的長度不小於待排序文件的長度,排序結束
-
圖10-14-二路歸併排序過程示意圖:
6、基數排序
- 基數排序(又稱分配排序):不對排序碼比較,而是藉助於多排序碼排序的思想進行單排序嗎排序的方法
6.1 多排序碼的排序
- 注: 每一張牌有兩個 「排序碼」:花色(梅花<方塊<紅心<黑桃)和麪值(\(2<3<\ldots<A\)),且花色的地位高於面值,即面值相等的兩張牌,以花色大的爲大,即在比較兩張牌的牌面大小時,先比較花色,若花色相同,則再比較面值
- 分配:把 \(52\) 張撲克牌按面值分紅 \(13\) 堆,再按照不一樣的花色分紅 \(4\) 堆,分紅若干堆的過程稱爲分配
- 收集:第一次分配時,把面值不一樣的 \(13\) 堆牌自小至大疊在一塊兒,把不一樣花色的 \(4\) 堆牌自小至大的次序合在一塊兒,從若干堆自小到大疊在一塊兒的過程稱爲收集
6.2 靜態鏈式基數排序
- 靜態鏈式基數排序步驟:
- 先用靜態鏈表存儲待排序文件中的 \(n\) 個記錄,表中每個結點對應一個記錄
- 第一趟對低位排序碼(個位數)進行分配,將 \(n\) 個結點分配到序號分別爲 \(0-9\) 的 \(10\) 個鏈式隊列中,用 \(f[i]\) 和 \(e[i]\) 分別做爲第 \(i\) 個隊列的隊首和隊尾指針
- 第一趟收集過程當中把這個 \(10\) 個隊列中非空的隊列依次合併在一塊兒產生一個新的靜態單鏈表
- 對這個新的靜態單鏈表按十位數進行分配和採集,而後再依次對百位數、千位數直到最高位數反覆進行這樣的分配和收集操做,排序結束
- 圖10-15-靜態鏈式技術排序示意圖:
7、各類內部排序算法的比較(書中未給出)
排序方法 |
比較次數(最好) |
比較次數(最差) |
移動次數(最好) |
移動次數(最差) |
穩定性 |
直接插入排序 |
\(n\) |
\(n^2\) |
\(0\) |
$$n^2$$ |
\(√\) |
折半插入排序 |
\(nlog_2n\) |
\(nlog_2n\) |
\(0\) |
\(n^2\) |
\(√\) |
冒泡排序 |
\(n\) |
\(n^2\) |
\(0\) |
\(n^2\) |
\(√\) |
快速排序 |
\(nlog_2n\) |
\(n^2\) |
\(nlog_2n\) |
\(n^2\) |
\(×\) |
簡單選擇排序 |
\(n^2\) |
\(n^2\) |
\(0\) |
\(n\) |
\(×\) |
堆排序 |
\(nlog_2n\) |
\(nlog_2n\) |
$$nlog_2n$$ |
\(nlog_2n\) |
\(×\) |
歸併排序 |
\(nlog_2n\) |
\(nlog_2n\) |
\(nlog_2n\) |
\(nlog_2n\) |
\(√\) |
基數排序 |
\(O(b(n+rd))\) |
\(O(b(n+rd))\) |
|
|
\(√\) |
上表基數排序中:\(rd\) 是收集過程和各位排序碼的取值範圍中值的個數(十進制爲 \(10\));靜態鏈式基數排序要進行 \(b\) 唐收集和分配
8、內部排序算法的應用(書中未給出)
- 略
9、算法設計題
- 略
10、錯題集
- 下列說法錯誤的是:基數排序適合實型數據的排序
- 插入排序算法在最後一趟開始以前,全部的元素可能都不在其最終的位置上(好比最後一個須要插入的元素是最小的元素)
- 一個序列中有 \(10000\) 個元素,若只想獲得其中前 \(10\) 個最小元素,最好採用堆排序算法
- 排序的趟數和待排序元素的原始狀態有關的排序算法是冒泡排序
- 快速排序算法和冒泡排序算法都會出現起泡的現象