參考書籍:數據結構(C語言版)嚴蔚敏吳偉民編著清華大學出版社
本文中的代碼可從這裏下載:https://github.com/qingyujean/data-structure
1.簡單選擇排序
1.1基本思想
通過 n-i 次關鍵字間的比較,從無序序列[i..n]的 n-i+1 記錄中選出關鍵字最小的記錄加入有序序列(即作爲有序序列中的第i個記錄)。
1.2代碼實現
package sort.selectionSort;
public class SimpleSelectSort {
/**
* @param args
*/
public static int selectMin(int[] L, int i){
int min = L[i], min_k = i;
for(int k = i+1; k <= L.length-1; k++)
if(L[k] < min){
min = L[k];
min_k = k;
}
return min_k;
}
//對順序表L做簡單選擇排序
public static void selectSort(int[] L){
//從i後面的子序列中(即從i->n中)選出一個min插入到i的位置上
for(int i = 1; i < L.length-1; i++){//i的取值是1...n-1
int j = selectMin(L, i);//從L[i...n]裏選出一個min,並返回其下標
if(j!=i){//交換L[i]與L[j]
L[0] = L[i];
L[i] = L[j];
L[j] = L[0];
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] test = {0, 8, 3, 9, 1, 6}; //0號單元未使用
selectSort(test);
for(int i = 1; i <= test.length-1; i++)
System.out.print(test[i]+" ");
}
}
運行結果:
1.3性能分析
由於存在着不相鄰元素之間的互換,因此,簡單選擇排序是“不穩定的” 。
算法實現共需要進行 n-1 次選擇,每次選擇需要進行n-i次比較(1≤i≤n-1),而每次交換最多需3次移動,因此,總的比較次數 C = n(n-1)/2,總的移動次數 M = 3(n-1)。故其時間複雜度爲O(n^2)。
選擇排序的主要操作是進行關鍵字間的比較,因此改進選擇排序應從如何減少 “比較” 考慮。顯然,在n個關鍵字中選出最小值,至少進行n-1次比較,然而,繼續在剩餘的n-1個關鍵字中選擇次小值,是否一定要進行n-2次比較呢?如果能利用前n-1次比較所得信息,就可減少以後各趟選擇排序中所用的比較次數。如樹形選擇排序,又稱錦標賽排序,其過程: 首先對n個記錄的關鍵字兩兩比較,然後在 n/2 個較小者之間再進行兩兩比較,如此重複,直至選出最小關鍵字的記錄爲止。此對應的過程可以用一棵有n個葉子結點的完全二叉樹表示。實際上,體育比賽中的錦標賽便是一種選擇排序。
再例如後面提到的堆排序。
2.堆排序
堆的定義:若有n個元素的排序碼k1,k2,k3,…,kn,當滿足如下條件:
其中i=1,2,…,[n/2]向下取整,則稱此n個元素的排序碼k1,k2,k3,…,kn爲一個堆。若將此排序碼按順序組成一棵完全二叉樹,則(1)稱爲小根堆(二叉樹的所有根結點值小於或等於左右孩子的值),(2)稱爲大根堆(二叉樹的所有根結點值大於或等於左右孩子的值)。
例如:判定序列{96,83,27,38,11,09}、{12,36,24,85,47,30,53,91 }是否堆
將排序碼按順序組成一棵完全二叉樹,則易判別。
小根堆:二叉樹的所有根結點值小於或等於左右孩子的值;
大根堆:二叉樹的所有根結點值大於或等於左右孩子的值;
2.1基本思想
將堆中第一個結點(二叉樹根結點)和最後一個結點的數據進行交換(k1與kn),再將k1~kn-1重新建堆,然後k1和kn-1交換,再將 k1~kn-2重新建堆,然後k1和kn-2交換,如此重複下去,每次重新建堆的元素個數不斷減1,直到重新建堆的元素個數僅剩一個爲止。這時堆排序已經完成,則排序碼k1,k2,k3,…,kn已排成一個有序序列。
實現堆排序需要解決兩個問題:
(1)如何由一個無序序列建成一個堆?
(2)如何在輸出堆頂元素之後,調整剩餘元素成爲一個新的堆?
例:圖(a)是個堆,假設輸出堆頂元素之後,以堆中最後一個元素替代之,如圖(b)所示。此時根結點的左、右子樹均爲堆,則僅需自上至下進行調整即可。
這個自堆頂至葉子的調整過程稱爲“篩選”。
概念:“篩選”
假若完全二叉樹的某一個結點 i,它的左、右子樹已是堆。需要將R[2i].key與R[2i+1].key之中的最小者與R[i].key比較,若R[i].key較大則交換,這有可能破壞下一級堆。於是繼續採用上述方法構造下一級堆,直到完全二叉樹中結點i構成堆爲止。這個自堆頂到葉子的調整過程爲“篩選”。
建堆
將排序碼{ k1,k2,k3 … ,kn }表示成一棵完全二叉樹,然後從第 [n/2]向下取整 個排序碼(即最後一個非終端結點)開始篩選,使由該結點作爲根結點組成的子二叉樹符合堆的定義,然後從第 [n/2]向下取整- 1個排序碼重複剛纔操作,直到第一個排序碼(即根)止。這時候,該二叉樹符合堆的定義,初始堆已經建立。
2.2代碼實現
例:關鍵字序列T= (21,25,49,25,16,08),請建大根堆。然後對建好的大根堆進行排序
package sort.selectionSort;
public class HeapSort {
/**
* @param args
*/
//已知H[s...m]中記錄的關鍵字除H[s]之外其他關鍵字均滿足大頂堆的定義,本函數則
//實現調整H[s]以使得H[s...m]成爲一個大頂堆
public static void heapAdjust(int[] H, int s, int m){
//s指向暫時的堆頂位置,但H[s]使得不滿足大頂堆的定義,所以調整H[s],使得H[s...m]成爲一個大頂堆
//這個過程叫做一次“篩選”:從堆頂到葉子節點的調整
for(int j = 2*s; j <= m; j *= 2){//j = j * 2,即從當前層往下一層考察
if(j<m && H[j+1] >H[j])//j<m是爲了防止H[j+1]的j+1越界
j++;//j總指向s的2個孩子裏大的那個
if(H[j] > H[s]){
H[0] = H[s];
H[s] = H[j];
H[j] = H[0];
}else
break;//如果新換上來的堆頂本身就並不破壞堆得定義,則無需繼續調整,因爲他下面的元素是一定滿足堆的
s = j;//s調整好後,往下一層考察,直至到葉子節點
}
}
public static void heapSort(int[] H){
int n = H.length-1;
//建堆
for(int i = n/2; i > 0; i--){//將H[1...n]建成大頂堆,從第n/2個元素開始調整,即“篩選”過程
//即調整元素的順序是第n/2個元素,第n/2 - 1個元素,...,第1個元素
//每一次篩選都是一次從當前元素(局部的堆頂到葉子節點的調整)
heapAdjust(H, i, n);//每一次調整,都使得H[i...n]滿足大頂堆的定義,直至整個H[1...n]都滿足大頂堆定義
}
//堆排序
//總是將最後一個元素與堆頂互換,即越大的數慢慢往後排,互換以後要做調整使剩餘的元素仍然滿足大頂堆的定義
for(int i = n; i > 0; i--){//i指向當前還未排好序的子序列的“最後一個元素”,而i後面的是已經排好序的,
//即每次從堆頂輸出的那個最大值,排到i指向的這個最後位置
H[0] = H[i];
H[i] = H[1];//H[1]是當前的堆頂元素,是當前最大值,排到最後面去
H[1] = H[0];//由於H[i]跑到了堆頂位置可能破壞堆,需要調整
heapAdjust(H, 1, i-1);//從堆頂調整到i-1對應的節點
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] testH = {0, 21, 25, 49, 25, 16, 8}; //0號單元未使用
//堆採用順序表存儲
heapSort(testH);
for(int i = 1; i <= testH.length-1; i++)
System.out.print(testH[i]+" ");
}
}
運行結果:
2.3性能分析
在整個堆排序中,共需要進行 n+[n/2]向下取整 -1 次篩選運算,每次篩選運算進行雙親和孩子或兄弟結點的排序碼的比較和移動次數都不會超過完全二叉樹的深度。故每次篩選運算的時間複雜度爲O(log2n),則整個堆排序過程的時間複雜度爲O(nlog2n) 。
堆排序在最壞情況下,時間複雜度也爲O(nlog2n)。相對於快速排序,這是堆排序的最大優點。此外,堆排序僅需一個記錄大小輔助存儲空間供交換使用。
由於存在着不相鄰元素之間的互換,因此,堆排序是一種不穩定的排序方法。