本文為系列專題【資料結構和演算法:簡單方法】的第 12 篇文章。
- 資料結構 | 順序表
- 資料結構 | 連結串列
- 資料結構 | 棧
- 資料結構 | 佇列
- 資料結構 | 雙連結串列和迴圈連結串列
- 資料結構 | 二叉樹的概念和原理
- 資料結構 | 二叉樹的建立及遍歷實現
- 資料結構 | 線索二叉樹
- 資料結構 | 二叉堆
- 演算法 | 順序查詢和二分查詢
- 資料結構 | 二叉查詢樹
本文先簡單介紹一下什麼是排序,然後再結合動畫介紹暴力排序和氣泡排序。
1. 什麼是排序?
排序在日常生活中無處不在。比如考試成績的排名、體育課的從低到高的隊形、網購時按價格升序排列或降序排列等等。
姓名 | 學號 | 班級 | 成績 |
---|---|---|---|
張三 | 1001 | 2班 | 100 |
李四 | 1003 | 1班 | 90 |
王五 | 1000 | 3班 | 80 |
趙六 | 1006 | 2班 | 70 |
在該表格中,我們稱一行資料為一條記錄,其中姓名、學號等資料項是關鍵字,關鍵字是我們排序的依據。關鍵字可以是主關鍵字或次關鍵字,甚至是若干資料項的組合。
比如,在該表格中,我們是按照成績(次關鍵字)從高到低降序排序的。也可以按照學號排序,或者按照學號和成績同時排序:先按成績降序排列,成績相同的,按照學號升序排列。
所以,對多條記錄排序的本質是對關鍵字的排序,如果排序依據是多個關鍵字,可以轉化為單個關鍵字的排序。
所謂排序,是指按指定的關鍵字,將某個序列的所有元素以有序的方式排列起來。
如何實現排序呢?
比如上體育課時按身高的排序,就是比較兩個人的身高,然後矮的站前面,高的站後面。如果高的在前,矮的在後,就交換位置;否則就用不動。
沒錯,排序是藉助比較和交換來實現的。比較的是關鍵字,這是我們排序的依據。交換的是兩個元素的位置,通常我們不真的交換位置,而是藉助中間變數的賦值來實現交換位置的效果。
/*一個交換函式,交換陣列中下標為i和j的元素位置*/
void swap(int *array, int i, int j)
{
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
在實現排序演算法時,我們應當追求更少次數的比較和交換,這樣才能提高演算法的效能。
為了後面說明問題方便起見,這裡約定幾個規則:
- 只對陣列排序,並約定陣列長度
length = 10
- 全部排序演算法均按升序排序
- 由於是針對陣列,所以關鍵字就是其元素值本身。比較關鍵字即是比較元素值,不再區分。
2. 暴力排序(Violent Sort)
所謂暴力,是指沒有任何技巧而言的方式,它直接粗暴地遍歷真個陣列,從而不停的進行比較和交換。這是最容易理解的排序演算法。
核心思想:從陣列的第一個元素開始,將每個元素作為基準元素,基準元素和其後面的所有元素比較,如果基準元素大於其後的某個元素,則與其交換位置;否則位置不變。
當基準元素和其後的所有元素比較交換完後,最小值一定被排好了,此時就說一輪排序完成了,每輪排序有若干次迴圈,用於比較和交換。像這樣進行若干輪的排序後,排序就完成了。
我們需要兩個變數,用於標記兩個進行比較的元素下標:
- 變數
i
:某個元素的下標; - 變數
j
:下標i
之後的下標。
我們需要兩個巢狀迴圈:
- 外層迴圈控制輪數;
- 內層迴圈控制每輪的比較和交換。
動態過程如下:
程式碼實現如下:
/* 暴力排序
* array : 陣列
* length : 陣列長度
*/
void violent_sort(int *array, int length)
{
int i, j;
// 外層迴圈,遍歷陣列,控制輪數
for (i = 0; i < length; i++) {
// 內層迴圈,迴圈比較 array[i] 和其之後的元素,控制迴圈次數
for (j = i + 1; j < length; j++) {
if (array[i] > array[j]) { // 如果大於則交換
swap(array, i, j);
}
}
}
}
通過以上的暴力排序,我們能夠初體會排序的思想,即不斷地比較和交換。
暴力排序有其明顯的缺點,即每個元素都要和其之後的所有的元素比較,這就很容易破壞整個陣列的原來順序,比如在上面的動態過程中,元素 40 在某次交換後被交換到了更靠後的位置。為了排好某個元素,而對其之後的所有元素大動干戈,甚至破壞了它們原來之間的順序,這就影響了效率。
3. 氣泡排序(Bubble Sort)
氣泡排序是一種很簡單的排序演算法,由暴力排序改進而來。
核心思想:比較相鄰的兩個元素,當前者大於後者時,交換二者位置;否則位置不變。
注意:氣泡排序是相鄰的兩個元素比較並交換,而暴力排序是某個元素和其之後的所有元素比較並交換。
當陣列中所有的相鄰元素進行兩兩比較和交換後,一定會找到一個最大值,並且最大值被交換到陣列尾處,此時說一輪排序完成了,一輪排序中有若干次迴圈,用於比較和交換。像這樣進行若干輪排序後,排序就完成了。
我們需要兩個變數:
-
變數
i
:用於控制輪數; -
變數
j
:元素下標,j+1
為其相鄰元素的下標。
我們需要兩個巢狀迴圈:
- 外層迴圈控制輪數
- 內層迴圈控制每輪所有相鄰元素的比較和交換
動態過程如下:
程式碼實現如下:
/* 氣泡排序
* array : 陣列
* length : 陣列長度
*/
void bubble_sort(int *array, int length)
{
int i, j;
// 外層迴圈,控制輪數。每一輪排好一個元素
for (i = 0; i < length; i++) {
// 內層迴圈,迴圈比較陣列中未排序的元素
for (j = 0; j < length - i - 1; j++) {
// 若前者大於後者則交換
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
}
}
}
}
由於是相鄰的兩個元素進行比較,所以一輪比較後,一定有一個最大的元素被交換到最右邊,像“大泡泡”浮到水面上一樣,所以我們稱其為“氣泡排序”。
氣泡排序雖然改善了暴力排序的缺點,但仍然具有可優化的地方。
在上述動態過程中,第 7 輪(i = 6
)時就已經完成排序了,但後面仍然進行了 3 輪的比較。這最後 3 輪是完全沒必要的,所以我們可以改進一下,避免沒必要的比較。
方法就是增加一個標誌變數 unsorted
,用於標誌陣列是否已經有序,unsorted = 0
時陣列有序,unsorted = 1
時陣列無序。
unsorted
通過記錄在某輪排序中是否發生了交換,如果沒發生,就意味著已經有序。
void bubble_sort_v3(int *array, int length)
{
int i, j;
int unsorted = 1; // 標識變數
for (i = 0; i < length && unsorted; i++) {
unsorted = 0;
for (j = 0; j < length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
unsorted = 1; // 發生交換,說明尚未有序
}
}
}
}
雖然氣泡排序可以避免一些不必要的比較和交換,但是氣泡排序本質仍和暴力排序相似,即不斷地大量交換,或是某個元素和其後所有元素的交換,或是兩個相鄰元素的交換。通過動圖也可以看出,要使一個元素排到其正確的位置上,我們往往需要和多個元素相交換,這是影響效率的“硬傷”。
如有錯誤,還請指正。
如果覺得寫的不錯,可以點個贊和關注。後續會有更多資料結構和演算法相關文章。