內部排序
基於比較的排序。
1. 插入排序
直接插入排序
基本思想:
將元素插入到已經排好序的序列中。第一個元素已經是有序序列,然後比較外圍的元素和序列的最後一個元素,判斷是否需要插入,如果小,則插入。
時間複雜度:
最優 O(n)
最差 O(n^2)
是否穩定:
是
public void insertSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++)
for (int j = i - 1; j >= 0; j--)
if (arr[j + 1] < arr[j])
swap(arr, j + 1, j);
}
改進後的插入排序
比如,用二分查找優化插入排序,因爲是要插入到已經排好序的序列當中,所以在查找插入位置這個地方可以用二分查找來優化。
public int binarySearch(int[] arr, int low, int high, int key) {
while (low <= high) {
int mid = low + (high - low) / 2;
if (key < arr[mid])
high = mid - 1;
else
low = mid + 1;
}
return low;
}
public void insertSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++)
// determine whether to insert
if (arr[i] < arr[i - 1]) {
// record the element to insert
int key = arr[i];
// find insert index
int indexInsert = binarySearch(arr, 0, i - 1, arr[i]);
// move elements
for (int j = i - 1; j >= indexInsert; j--)
arr[j + 1] = arr[j];
// insert the key
arr[indexInsert] = key;
}
}
2. 選擇排序
簡單選擇排序
基本思想:
選出後面最小的元素和前面的交換
時間複雜度:
最優 O(n^2)
最差 O(n^2)
是否穩定:
否
public void selectSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
int minIndex = i;
// find the min index
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex])
minIndex = j;
}
if (minIndex != i)
swap(arr, i, minIndex);
}
}
改進後的選擇排序
之前的選擇排序是一趟只找到最小的,如果一趟可以把最大最小都找出來,就可以將循環的次數減半。
不過在交換時需要注意一種情況,就是第一個元素就已經是最大元素的情況,因爲前面已經交換過 i 和 min,所以再交換時就是交換的 n - i - 1 和 min,而不是 max。
public void selectSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n / 2; i++) {
int min = i;
int max = i;
for (int j = i + 1; j < n - i; j++) {
// find the min element
if (arr[j] < arr[min]) {
min = j;
continue;
}
// find the max element
if (arr[j] > arr[max])
max = j;
}
swap(arr, i, min);
if (max != i)
swap(arr, n - i - 1, max);
else // the first element is the max element
swap(arr, n - i - 1, min);
}
}
堆排序
基本思想:
堆排序也是一種直接選擇排序,因爲它也是將排好序的大根堆 or 小根堆的頂部元素逐個和尾部元素交換,也是一種選擇。
時間複雜度:
最優 O(nlgn)
最差 O(nlgn)
是一個很優秀的排序算法
是否穩定:
否
public void adjustHeap(int[] arr, int i, int len) {
int parent = arr[i];
// start from left child
for (int k = 2 * i + 1; k < len; k = 2 * k + 1) {
// find max child (left or right)
if (k + 1 < len && arr[k + 1] > arr[k])
k++;
// compare the parent and its child, but don't swap
if (arr[k] > arr[i]) {
arr[i] = arr[k];
i = k;
} else
break;
}
// insert the original parent on index i
arr[i] = parent;
}
public void heapSort(int[] arr) {
int len = arr.length;
// adjust heap from the last none-leaf node
// from bottom to up; from right to left
for (int i = len / 2 - 1; i >= 0; i--)
adjustHeap(arr, i, len);
// swap the top element and the last element
for (int i = len - 1; i >= 0; i--) {
swap(arr, i, 0);
adjustHeap(arr, 0, i);
}
}
3. 歸併排序
基本思想:
分治。將兩個有序序列合併成一個有序序列,遞歸進行。需要輔助數組。
時間複雜度:
最優 O(nlgn)
最差 O(nlgn)
空間複雜度
O(n)
是否穩定:
是
/* create an array in advance to avoid creating arrays frequently */
private void sort(int[] arr) {
int n = arr.length;
int[] temp_arr = new int[n];
mergeSort(arr, 0, n - 1, temp_arr);
}
public void mergeSort(int[] arr, int left, int right, int[] temp_arr) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid, temp_arr);
mergeSort(arr, mid + 1, right, temp_arr);
merge(arr, left, mid, right, temp_arr);
}
}
public void merge(int[] arr, int left, int mid, int right, int[] temp_arr) {
int i = left;
int j = mid + 1;
int t = 0;
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp_arr[t++] = arr[i];
i++;
} else {
temp_arr[t++] = arr[j];
j++;
}
}
// insert the remaining elements
while (i <= mid)
temp_arr[t++] = arr[i++];
while (j <= right)
temp_arr[t++] = arr[j++];
t = 0;
// copy elements into array
while (left <= right)
arr[left++] = temp_arr[t++];
}
4. 交換排序
冒泡排序
基本思想:
總共有 n 個元素,就比較 n - 1 趟,每一趟比較都會將相鄰元素進行比較,然後將較大的元素向後放,就像大數沉底,小數像上冒一樣。冒泡排序的交換次數等於原始序列的逆序數。
時間複雜度:
最優 O(n)
最差也是平均 O(n^2)
是否穩定:
是
public void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1])
swap(arr, j, j + 1);
}
}
}
改進後的冒泡排序一
交換過後,會有部分元素是已經排好序了,這一部分再進行比較是沒有意義的,可以從這個地方入手改進冒泡排序。
用一個標誌位記錄最後一次進行交換的位置,這個位置後面的元素是沒有進行交換的,說明是已經排好序的,所以不需要再比較。
public void bubbleSort(int[] arr) {
int n = arr.length;
int i = n - 1;
int pos = 0;
while (i > 0) {
for (int j = 0; j < i; j++) {
pos = 0;
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
pos = j;
}
}
i = pos;
}
}
改進後的冒泡排序二
雙向冒泡(正反兩個方向同時排序),使排序次數幾乎減少一半
public void bubbleSort(int[] arr) {
int n = arr.length;
int left = 0;
int right = n - 1;
while (left < right) {
for (int i = left; i < right; i++) {
if (arr[i] > arr[i + 1])
swap(arr, i, i + 1);
}
right--;
for (int i = right; i > left; i--) {
if (arr[i] < arr[i - 1])
swap(arr, i, i - 1);
}
left++;
}
}
快速排序
基本思想:
根據選擇的基準元素進行劃分,然後兩邊都進行排序,再遞歸進行。需要注意的是,遍歷需要從選擇的基準元素的反方向開始。
時間複雜度:
最優也是平均 O(nlgn)
最差 O(n^2) 即每次選的基準元素都是最大(小)值
是否穩定:
否
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotLoc = partition(arr, low, high);
quickSort(arr, low, pivotLoc - 1);
quickSort(arr, pivotLoc + 1, high);
}
}
public int partition(int[] arr, int low, int high) {
int pivot = arr[low];
while (low < high) {
while (low < high && arr[high] >= pivot)
high--;
swap(arr, low, high);
while (low < high && arr[low] <= pivot)
low++;
swap(arr, low, high);
}
return low;
}
外部排序
非基於比較的排序,時間複雜度可以達到 O(n),其實是用空間換時間。
下面列舉的都是線性時間排序法
1. 計數排序(Counting Sort)
基本思想:
利用數組下標來排序。但只適合有限數值的數字,序列的數字最大值 k 如果太大,那麼開的輔助數組 C[] 就會很大,佔用太多空間。
這種思路經常被用到。
時間複雜度:
最優也是平均 O(n + k)
最差 O(n^2) 即每次選的基準元素都是最大(小)值
是否穩定:
是
public int[] countingSort(int[] A, int k) {
int n = A.length;
int[] C = new int[k + 1];
// counting the number of times each element appears in A
for (int i = 0; i < n; i++) {
C[A[i]]++;
}
// counting elements less than or equal to C[i]
for (int i = 1; i <= k; i++) {
C[i] = C[i] + C[i - 1];
}
// insert into result array
int[] B = new int[n];
for (int i = n - 1; i >= 0; i--) {
B[C[A[i]] - 1] = A[i];
C[A[i]]--;
}
return B;
}
2. 桶排序(Bucket Sort)
基本思想:
把數組中數據放到有限個桶中,在每個桶中分別進行排序(可以採用任意排序方法)。
模擬畫圖 ≡ω≡
input:[12,22,2,13,23,3]
| | | | | |
| 2,3 | |12,13| |22,23|
|_____| |_____| |_____|
bucket1 bucket2 bucket3
(1-10) (11-20) (21-30)
output:[2,3,12,13,22,23]
時間複雜度:
最優近似 O(n)
最差 O(n^2) 即每次選的基準元素都是最大(小)值
是否穩定:
是
public void bucketSort(int[] arr){
int n = arr.length;
int max = arr[0];
int min = arr[0];
for (int num : arr) {
if (num < min)
min = num;
if (num > max)
max = num;
}
// create bucket
int bucketNum = max / 10 - min / 10 + 1;
List bucketList = new ArrayList<List<Integer>>();
for (int i = 1; i <= bucketNum; i++) {
bucketList.add(new ArrayList<Integer>());
}
// insert into bucket
for (int i = 0; i < n; i++) {
int index = (arr[i] - min) / 10;
((ArrayList<Integer>)bucketList.get(index)).add(arr[i]);
}
ArrayList<Integer> bucket = null;
int index = 0;
for (int i = 0; i < bucketNum; i++) {
bucket = (ArrayList<Integer>)bucketList.get(i);
bucketInsertSort(bucket);
for (int num : bucket) {
arr[index++] = num;
}
}
}
public void bucketInsertSort(List<Integer> bucket) {
for (int i = 0; i < bucket.size(); i++) {
int temp = bucket.get(i);
int j = i - 1;
for (; j >= 0 && bucket.get(j) > temp; j--) {
bucket.set(j + 1, bucket.get(j));
}
bucket.set(j + 1, temp);
}
}
3. 基數排序(Radix Sort)
基本思想:
是一種線性時間排序法。
前面的計數和桶排序都是隻能排一個關鍵字,而基數排序可以排多個關鍵字。
基數排序分爲兩種:假設有二元組 (a, b),以 a 爲首要關鍵字,b 爲次要關鍵字排序
1. MSD(Most Siginificant Digit) 先排 a,後排 b
2. LSD(Least Siginificant Digit) 先排 b,後排 a
基數排序需要使用穩定的排序算法,一般用計數或者桶排序。
e.g. 採用 LSD
input: [170, 45, 75, 90, 802, 24, 2, 66]
1. 從最後一個關鍵字開始排:
170, 45, 75, 90, 802, 24, 2, 66
0 5 5 0 2 4 2 6
排序後(注意保持原來的相對順序,802 仍然在 2 前面):
170, 90, 802, 2, 24, 45, 75, 66
2. 從次要關鍵字開始排
170, 90, 802, 2, 24, 45, 75, 66
7 9 0 2 4 7 6
排序後(170 仍然在 75 前面):
802, 2, 24, 45, 66, 170, 75, 90
3. 從首要關鍵字開始排:
802, 2, 24, 45, 66, 170, 75, 90
8 1
排序後:
2, 24, 45, 66, 75, 90, 170, 802
output: [2, 24, 45, 66, 75, 90, 170, 802]
時間複雜度:
平均 O(d * (r + n))
d:digit 數字位數 r:radix 基數 n:number 關鍵字個數
空間複雜度:
O(r + n)
是否穩定:
是
public void radixSort(int[] arr, int n) {
// find max element
int max = 0;
for (int i = 1; i < n; i++) {
if (arr[i] > max)
max = arr[i];
}
for (int k = 1; max / k > 0; k *= 10) {
countingSort(arr, n, k);
}
}
public void countingSort(int[] arr, int n, int k) {
// max number is 9
int C[] = new int[10];
// counting occurrences
for (int i = 0; i < n; i++) {
C[(arr[i] / k) % 10]++;
}
// counting elements less than or equal to C[i]
for (int i = 1; i < 10; i++) {
C[i] = C[i] + C[i - 1];
}
// insert into result array
int[] B = new int[n];
for (int i = n - 1; i >= 0; i--) {
B[C[(arr[i] / k) % 10] - 1] = arr[i];
C[(arr[i] / k) % 10]--;
}
for (int i = 0; i < n; i++) {
arr[i] = B[i];
}
}
總結
- 關於穩定性:
穩定的排序算法:冒泡、插入、歸併、計數、桶和基數排序
不穩定的排序算法: 選擇、快速、希爾和堆排序
- 排序算法的選擇:
如果數據有序或基本有序,冒泡和插入的時間複雜度可以降到 O(n),而快排則相反。
如果數據很大,需要考慮使用 O(nlgn) 的排序方法,如快排、歸併排序、堆排。
如果對空間限制不大,可以使用基數排序等方法降低時間複雜度,這些線性時間排序法是利用了數據的特性達到最佳的效果。
參考資料: