本文總結了常用的幾種內排序算法的原理、實現以及各個算法性能的比較。雖然網絡上有不少的大神對排序算法進行了比較詳細的介紹,但是,本人一方面是爲了總結一下自己的知識系統,另一方面想以更加通俗易懂的方式讓廣大的初學者對排序算法的思想能快速的理解和掌握。由於作者水平有限錯誤之處在所難免,望大家批評指正。
本文介紹了常用的內排序算法包括比較排序算法(插入排序、冒泡排序、選擇排序、快速排序、歸併排序、堆排序)和基於運算的排序算法(基數排序、桶排序)。分別對這些算法從算法思想、僞代碼、複雜度和穩定性、算法的Java實現幾個方面進行了簡單的介紹。
0. 排序算法的簡單比較
名稱 | 穩定性1 | 時間複雜度 | 空間複雜度 |
---|---|---|---|
插入排序 | 穩定 | 平均/最壞: |
|
冒泡排序 | 穩定 | 平均/最壞: |
|
選擇排序 | 不穩定 | 平均/最壞: |
|
快速排序 | 不穩定 | 平均: |
|
歸併排序 | 穩定 | 平均/最壞: |
|
堆排序 | 不穩定 | 平均/最壞: |
|
基數排序 | 穩定 | 平均/最壞: |
|
桶排序 | 穩定 | 平均: |
1. 插入排序
算法思想:插入排序的工作方式像人們排序一手撲克牌一樣。開始時,我們的左手爲空並且桌子上的牌面朝下。然後,我們每次從桌子上拿走一張牌並將它插入左手中正確的位置。爲了找到一張牌的正確位置,我們從右到左將它與已在手中的每張牌進行比較,如圖1.1所示。
圖1.1 使用插入排序來排序手中的撲克牌(圖片來源《算法導論》第三版)
圖1.2和圖1.3給出了插入排序的過程圖。
圖1.2 使用插入排序爲一列數字進行排序的過程(圖片來源維基百科)
圖1.3 使用插入排序爲一列數字進行排序的過程(圖片來源維基百科)僞代碼:
insertSort(ArrayType num)
for i = 1 to num.length
x = num[i]
for j = i - 1 to 0 && num[j] > x
num[j+1] = num[j]
num[j+1] = x
算法複雜度和穩定性:
平均/最差時間複雜度:O(n2)
空間複雜度:O(1)
穩定性:穩定Java實現:
public static void insertSort(int[] num) {
if (2 > num.length) return;
for (int i = 1; i < num.length; i++) {
int tmp = num[i];
int j = i - 1;
for (; j >= 0 && num[j] > tmp; j--) {//調整數組位置,爲tmp騰出空位
num[j + 1] = num[j];
}
num[j + 1] = tmp; //將tmp插入到合適的位置
}
}
2. 冒泡排序
- 算法思想:(升序排列)從起始元素開始,對數組中兩兩相鄰的元素進行比較,將值較小的元素放在前面,值較大的元素放在後面,一輪比較完畢,一個最大的數沉底成爲數組中的最後一個元素,一些較小的數如同氣泡一樣上浮一個位置(因此成爲冒泡或者起泡排序)。n個數,經過n-1輪比較後完成排序。
圖2.1 冒泡排序的過程
圖2.2 使用冒泡排序爲一列數字進行排序的過程(圖片來源維基百科)
圖2.3 使用冒泡排序爲一列數字進行排序的過程(圖片來源維基百科) - 僞代碼:
bubbleSort(ArrayType num)
for i = 0 to num.length
for j = 0 to num.length - i - 1
if num[j] > num[j + 1]
num[j] <-> num[j+1]
- 算法複雜度和穩定性:
平均/最壞的時間複雜度:O(n2)
空間複雜度:O(1)
穩定性:穩定 - Java實現:
public static void bubbleSort(int[] num) {
if (num.length < 2) return;
for (int i = 0; i < num.length; i++) {
for (int j = 0; j < num.length - i - 1; j++) {
if (num[j] > num[j + 1]) {
int tmp = num[j];
num[j] = num[j + 1];
num[j + 1] = tmp;
}
}
}
}
3. 選擇排序
- 算法思想:(升序排序)首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
圖3.1 使用選擇排序爲一列數字進行排序的過程(圖片來源維基百科)
圖3.2 選擇排序的示例動畫。(紅色表示當前最小值,黃色表示已排序序列,藍色表示當前位置。)(圖片來源維基百科) - 僞代碼:
selectSort(ArrayType num)
for i = 0 until num.length - 1
int min = i
for j = 0 until num.length
if num[min] > num[j]
min = j
if i != min
num[min] <-> num[i]
- 算法複雜度和穩定性:
平均/最壞的時間複雜度:O(n2)
空間複雜度:O(1)
穩定性:不穩定 - Java實現:
public static void selectSort(int[] num) {
if(num.length < 2) return;
for (int i = 0; i < num.length - 1; i++) {
int min = i;
for (int j = i + 1; j < num.length; j++) {
if (num[min] > num[j])
min = j;
}
if (i != min) {
int tmp = num[i];
num[i] = num[min];
num[min] = tmp;
}
}
}
4. 快速排序
- 算法思想: 快速排序是對冒泡排序的一種改進。通過一趟排序將待排序記錄分割成獨立的兩個部分,其中一部分記錄的關鍵字均小於另一部分記錄的關鍵字,然後再對這兩個部分繼續進行分割,直至整個序列有序。快速排序使用一個樞軸進行劃分,把序列劃分成兩個部分。
圖4.1 使用快速排序法對一列數字進行排序的過程(圖片來源維基百科)
圖4.2 快速排序採用“分而治之、各個擊破”的觀念,此爲原地(In-place)分區版本。(圖片來源維基百科) - 僞代碼:
//主程序的遞歸過程
quickSort(ArrayType num, int low, int high)
if low < high
loc = qSort(num, low, high)
quickSort(num, low, loc - 1)
quickSort(num, loc + 1, high)
//一趟快排
qSort(ArrayType num, int low, int high)
num[0] = num[low] //使用num[0]來存放樞軸記錄
pivot = num[low].key
while low < high
while low < high && num[high].key >= pivot
high--
num[low] = num[high]
while low < high && num[low].key <= pivot
low++
num[high] = num[low]
num[low] = num[0]
return low
- 算法複雜度和穩定性:
平均時間複雜度:O(nlogn)
最壞的時間複雜度:O(n2)
空間複雜度:O(logn)
穩定性:不穩定 - Java實現:
public static void qucikSort(int[] num, int low, int high){
if(num.length < 2) return;
if(low < high){
int loc = qSort(num, low, high);
quickSort(num, low, loc - 1);
quickSort(num, loc + 1, high);
}
}
private static int qSort(int[] num, int low, int high){
int pivot = num[low];//這裏只存放了記錄的key值,對於有衛星數據的記錄,需要記錄完整的數據元素
while(low < high){
while(low < high && num[high] >= pivot) high--;
num[low] = num[high];
while(low < high && num[low] <= pivot) low++;
num[high] = num[low];
}
num[low] = pivot;
return low;
}
5. 歸併排序
- 算法思想: 歸併的含義是將兩個或者兩個以上的有序表組合成一個新的有序表。有序表的合併這裏就不在累述。對於兩兩歸併的有序表被稱爲2-路歸併,2-路歸併的核心操作是將數組中前後相鄰的兩個有序序列歸併成爲一個有序序列。
圖5.1 使用歸併排序法對散點進行排序的過程(圖片來源維基百科)
圖5.2 使用歸併排序法對一列數字進行排序的過程(圖片來源維基百科) - 僞代碼:
mergeSort(ArrayType num, int low, int high)
if low < high
mid = floor((low + high)/2)
mergeSort(num, low, mid)
mergeSort(num, mid + 1, high)
merge(num, low, mid, high)
//一次歸併
merge(ArrayType num, int low, int mid, int high)
n1 = mid - low + 1
n2 = high - mid
create left[n1+1] and right[n2+1]
for i = 0 to n1 - 1
left[i] = num[low + i]
for j = 0 to n2 - 1
right[j] = num[mid + j + 1]
left[n1]= right[n2] = MAX_VALUE //使用兩個哨兵
i = j = 0
for k = low to high
num[k] = left[i] < right[j] ? left[i++]:right[j++]
- 算法複雜度和穩定性:
平均/最壞時間複雜度:O(nlogn)
空間複雜度:O(n)
穩定性:穩定 - Java實現:
public static void mergeSort(int[] num, int low, int high){
if(low < high) {
int mid = (low + high) >>> 1;
mergeSort(num, low, mid);
mergeSort(num, mid + 1, high);
merge(num, low, mid, high);
}
}
private static void merge(int[] num, int low, int mid, int high){
int n1 = mid - low + 1;
int n2 = high - mid;
int[] left = new int[n1 + 1];
int[] right = new int[n2 + 1];
for (int i = 0; i < n1; i++) {
left[i] = num[low + i];
}
for (int i = 0; i < n2; i++) {
right[i] = num[mid + i + 1];
}
left[n1] = Integer.MAX_VALUE;
right[n2] = Integer.MAX_VALUE;
int i = 0, j = 0;
for (int k = low; k <= high; k++) {
num[k] = left[i] < right[j] ? left[i++] : right[j++];
}
}
6. 堆排序
算法思想: 堆是一個近似完全二叉樹的結構,並同時滿足堆的性質,即子結點的鍵值或索引總是小於(或者大於)它的父節點。爲了討論方便本文使用最大堆結構對數據進行排序。堆排序可以分解成三個部分排序部分(headSort)、建堆部分(buildMaxHeap)和維護堆(maxHeapify)。
有關堆的創建和維護如有需要以後再補充
圖6.1 堆排序算法的演示。首先,將元素進行重排,以匹配堆的條件。圖中排序過程之前簡單的繪出了堆樹的結構。(圖片來源維基百科)僞代碼:
heapSort(ArrayType num)
buildMaxHeap(num) //創建最大堆
for i = num.length - 1 to 2
num[1] <-> num[i] //把數組的第一個元素取出
num[0]--
maxHeapify(num, 1) //維護最大堆結構
buildMaxHeap(ArrayType num)
num[0] = num.length - 1 //使用數組的0號單元存放堆的大小
for i = 1 to (num.length - 1) >> 1
maxHeapify(num, i)
maxHeapify(ArrayType num, int i)
int left = i << 1; //左孩子
int right = i << 1 + 1; //右孩子
int largest = i; //最大值標識
if left <= num[0] && num[left] > num[largest]
largest = left
if right <= num[0] && num[right] > num[largest]
largest = right
if i != largest
num[largest] <-> num[i]
maxHeapify(num, largest)
- 算法複雜度和穩定性:
平均/最壞時間複雜度:O(nlogn)
空間複雜度:O(1)
穩定性:不穩定 - Java實現:
/**
* 堆排序
*/
public static void heapSort(int[] num) {
if(num.length < 2)return;
buildMaxHeap(num);
for (int i = num.length - 1; i > 1; i--) {
int tmp = num[1];
num[1] = num[i];
num[i] = tmp;
num[0]--;
maxHeapify(num, 1);
}
}
/**
* 最大堆的建立
*/
public static void buildMaxHeap(int[] num) {
num[0] = num.length - 1; //使用第一個元素記錄堆的大小
for (int i = (num.length - 1) >>> 1; i > 0; i--) {
maxHeapify(num, i);
}
}
/**
* 最大堆的維護
* 這是堆排序最關鍵的一個步驟
*/
public static void maxHeapify(int[] num, int i) {
int left = i << 1;
int right = i << 1 + 1;
int largest = i;
if (left <= num[0] && num[left] > num[i]) largest = left;
if (right <= num[0] && num[right] > num[largest])largest = right;
if (largest != i) {
int tmp = num[i];
num[i] = num[largest];
num[largest] = tmp;
maxHeapify(num, largest);
}
}
7. 桶排序
- 算法思想: 假設有一組長度爲N的待排關鍵字序列K[1….n]。首先將這個序列劃分成M個的子區間(桶) 。然後基於某種映射函數,將待排序列的關鍵字k映射到第i個桶中(即桶數組B的下標 i) ,那麼該關鍵字k就作爲B[i]中的元素。接着對每個桶B[i]中的所有元素進行比較排序(可以使用快排)。然後依次枚舉輸出B[0]….B[M]中的全部內容即是一個有序序列。9
圖8.1 桶排序的過程圖(圖片來源維基百科) - 僞代碼:
bucketSort(ArrayType num, int n)
buckets ← new array of n empty lists
for i = 0 to num.length - 1
insert num[i] into buckets[msbits(num[i], k)]
for i = 0 to n - 1
next-sort(buckets[i])
return the concatenation of buckets[0], ..., buckets[n-1]
算法複雜度和穩定性:
平均時間複雜度:O(n)
最壞時間複雜度:O(n2)
空間複雜度:O(n∗k)
穩定性:穩定Java實現:
public static void bucketSort(double[] num) {
int n = num.length;
List bucketList[] = new ArrayList[n];
for (int i = 0; i < n; i++) {
int temp = (int) Math.floor(n * num[i]);
if (null == bucketList[temp])
bucketList[temp] = new ArrayList<Object>();
bucketList[temp].add(num[i]);
}
for (int i = 0; i < bucketList.length; i++) {
if (bucketList[i] != null)
insert(bucketList[i]);
}
int index = 0;
for (int i = 0; i < n; i++) {
if (null != bucketList[i]) {
Iterator<?> it = bucketList[i].iterator();
while (it.hasNext()) {
num[index++] = (Double) it.next();
}
}
}
}
/**
* 用插入排序對每個桶進行排序 從小到大排序
*/
private static void insert(List list) {
if (list.size() > 1) {
for (int i = 1; i < list.size(); i++) {
double temp = (Double) list.get(i);
int j = i - 1;
for (; j >= 0 && ((Double) list.get(j) > (Double) list.get(j + 1)); j--)
list.set(j + 1, list.get(j)); // 後移
list.set(j + 1, temp);
}
}
}
8. 基數排序
- 算法思想:基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。基本思想:將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
基數排序引用http://www.cnblogs.com/jingmoxukong/p/4311237.html - 僞代碼:
radixSort(ArrayType A, int d) //d爲A中元素最多的位數
for i=1 to d
do use a stable sort to sort array A on digit i
- 算法複雜度和穩定性:
平均/最壞時間複雜度:O(n∗k)
空間複雜度:O(n∗k)
穩定性:穩定 - Java實現:
// 獲取x這個數的d位數上的數字
// 比如獲取123的1位數,結果返回3
public int getDigit(int x, int d) {
int a[] = { 1, 1, 10, 100}; // 本實例中的最大數是百位數,所以只要到100就可以了
return ((x / a[d]) % 10);
}
public void radixSort(int[] list, int begin, int end, int digit) {
final int radix = 10; // 基數
int i = 0, j = 0;
int[] count = new int[radix]; // 存放各個桶的數據統計個數
int[] bucket = new int[end - begin + 1];
// 按照從低位到高位的順序執行排序過程
for (int d = 1; d <= digit; d++) {
// 置空各個桶的數據統計
for (i = 0; i < radix; i++) {
count[i] = 0;
}
// 統計各個桶將要裝入的數據個數
for (i = begin; i <= end; i++) {
j = getDigit(list[i], d);
count[j]++;
}
// count[i]表示第i個桶的右邊界索引
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
// 將數據依次裝入桶中
// 這裏要從右向左掃描,保證排序穩定性
for (i = end; i >= begin; i--) {
j = getDigit(list[i], d); // 求出關鍵碼的第k位的數字, 例如:576的第3位是5
bucket[count[j] - 1] = list[i]; // 放入對應的桶中,count[j]-1是第j個桶的右邊界索引
count[j]--; // 對應桶的裝入數據索引減一
}
// 將已分配好的桶中數據再倒出來,此時已是對應當前位數有序的表
for (i = begin, j = 0; i <= end; i++, j++) {
list[i] = bucket[j];
}
}
}
public int[] sort(int[] list) {
radixSort(list, 0, list.length - 1, 3);
return list;
}
參考文獻
[1]: 科曼. 算法導論[M]. 機械工業出版社, 2013.
- 穩定排序算法會讓原本有相等鍵值的紀錄維持相對次序。也就是如果一個排序算法是穩定的,當有兩個相等鍵值的紀錄R和S,且在原本的列表中R出現在S之前,在排序過的列表中R也將會是在S之前。 ↩