控制檯運行編譯java程序帶中文亂碼問題解決辦法: javac -encoding utf-8 Test.java
穩定性:排序算法需要保留數組中重複元素的相對位置。(具體詳見算法第四版P217)
冒泡排序
思想: 兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄爲止。
時間複雜度: O(N2)
空間複雜度: O(1)
穩定性: 穩定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
for (int i = 0; i < N-1; i++) {
for (int j = 0; j < N-i-1; j++) {
if (nums[j] > nums[j + 1]) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
}
選擇排序
思想: 每一趟在n-i+1(i=1,2,…,n-1)個記錄中選取關鍵字最小的記錄作爲有序序列的第i個記錄。
時間複雜度: O(N2)
空間複雜度: O(1)
穩定性: 不穩定(說明:舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序算法)
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
for (int i = 0; i < N; i++ ) {
int min = i;
for (int j = i+1; j < N; j++) {
if (nums[j] < nums[min]) {
min = j;
}
}
if (min != i) {
temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
}
}
插入排序
思想: 將一個記錄插入到已經排序好的有序表中,從而得到一個新的、記錄數增1的有序表
時間複雜度: O(N2)
空間複雜度: O(1)
穩定性: 穩定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
for (int i = 1; i < N; i++ ) {
for (int j = i; j > 0 && nums[j] < nums[j-1]; j--) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
改進版本
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int i = 0;
int j = 0;
for (i = 1; i < N; i++) {
if (nums[i] < nums[i-1]) {
int temp = nums[i];
for (j = i-1; (j >= 0) && (nums[j] > temp); j--) {
nums[j+1] = nums[j];
}
nums[j+1] = temp;
}
}
}
}
希爾排序(插入排序升級)
思想: 將數據分爲若干組記錄,然後分別對每一組做插入排序。
時間複雜度: O(NlgN)
空間複雜度: O(1)
穩定性: 不穩定
public class Sort {
public static void sort(int[] nums) {
int N = nums.length;
int temp = 0;
int h = 1;
int i = 0;
int j = 0;
//固定步長:1,4,13,40...
while (h < N/3) {
h = 3*h + 1;
}
while (h >= 1) {
//下面操作和插入排序算法基本相同
for (i = h; i < N; i++) {
if (nums[i] < nums[i - h]) {
int temp = nums[i];
for (j = i-h; (j >= 0) && (nums[j] > temp); j = j-h) {
nums[j+h] = nums[j];
}
nums[j+h] = temp;
}
}
h = h / 3;
}
}
}
快速排序(冒泡排序增強)
思想: 選取一個軸值(比較的基準),將待排序記錄分爲獨立的兩個部分,左側記錄都是小於或等於軸值,右側記錄都是大於或等於軸值,然後分別對左側部分和右側部分重複前面的過程,也就是左側部分又選擇一個軸值,又分爲兩個獨立的部分,這就使用了遞歸了。到最後,整個序列就變得有序了。
時間複雜度: O(NlgN)
空間複雜度: O(lgN)~O(N)
穩定性: 不穩定
public class Sort {
public static void sort(int[] nums) {
//消除對輸入的依賴
//StdRandom.shuffle(a);
sort(nums, 0, nums.length - 1);
}
public static void sort (int[] nums, int lo, int hi) {
if (hi <= lo) return;
//切分
int j = partition(nums, lo, hi);
sort(nums, lo, j-1);
sort(nums, j+1, hi);
}
public static int partition(int[] nums, int lo, int hi) {
//定義左右掃描的指針
int i = lo, j = hi + 1;
//定義切分的元素d
int point = nums[lo];
int temp = 0;
while (true) {
while (nums[++i] < point) if (i == hi) break;
while (point < nums[--j]) if (j == lo) break;
if (i >= j) break;
//交換兩個元素
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
//將切分元素放到數組中合適位置
temp = nums[lo];
nums[lo] = nums[j];
nums[j] = temp;
return j;
}
}
堆排序(選擇排序增強)
思想: 利用優先隊列的刪除最大元素的特點,依次將將刪除的最大元素保存起來,就有序了。當然,這裏的優先隊列是用堆實現的。
第一步:需要保證堆有序,也就是每一個父節點要比它的任意子節點要大;
第二步:使用堆的下沉操作來實現排序;
時間複雜度: O(NlgN)
空間複雜度: O(1)
穩定性: 不穩定
public class Sort {
public static void heapSort(int[] a) {
int N = a.length;
//第一步:通過下沉操作來實現堆有序,這裏只需要操作數組中前面的一半元素即可
for (int k = N / 2; k >= 1; k--) {
sink(a, k, N);
}
//第二步:將堆有序的數組使用下沉操作來得到有序數組
while (N > 1) {
exch(a, 1, N--);
sink(a, 1, N);
}
}
//堆下沉操作
public static void sink(int[] a, int k, int N) {
while (2*k <= N) {
int j = 2*k;
//取兩個子節點中較大的一個
if (j < N && less(a, j, j+1)) j++;
//比較如果父節點比子節點中較大的一個小,則交換
if (!less(a, k, j)) break;
exch(a, k, j);
//繼續往下面遍歷
k = j;
}
}
//之所以取i-1,是因爲堆中下標是從1開始的,需要還原到數組中從0開始的。
public static boolean less(int[] a, int i, int j) {
return a[i-1] < a[j-1];
}
public static void exch(int[] a, int i, int j) {
int temp = a[i-1];
a[i-1] = a[j-1];
a[j-1] = temp;
}
}
歸併排序
思想: 要將一個數組排序,可以先(遞歸地)將它們分成兩半分別排序,然後將它們的結果歸併起來。
時間複雜度: O(NlgN)
空間複雜度: O(N)
穩定性: 穩定
public class Sort {
//1.將兩個有序數組合併爲一個有序數組
public static void merge(int[] a, int lo, int mid, int hi) {
//將a[lo..mid] 和 a[mid+1..hi]歸併
//i代表左半邊索引,j代表右半邊索引
int i = lo, j = mid + 1;
//定義一個輔助數組
int[] aux;
//將a[lo..hi]複製到aux[lo..hi]
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
//左半邊元素用盡,取右半邊的元素
if (i > mid) {
a[k] = aux[j++];
//右半邊元素用盡,取左半邊元素
} else if (j > hi) {
a[k] = aux[i++];
//右半邊的當前元素小於左半邊的當前元素,則取右半邊的元素
} else if (aux[j] < aux[i]) {
a[k] = aux[j++];
//左半邊的當前元素小於等於右半邊的當前元素,則取左半邊的元素
} else {
a[k] = aux[i++];
}
}
}
//自頂向下
pubic static void sort (int[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int mid = lo + (hi - lo) / 2;
//將左半邊排序
sort(a, lo, mid);
//將右半部分排序
sort(a, mid+1, hi);
//歸併結果
merger(a, lo, mid, hi);
}
//自底向上
public static void sort (int[] a) {
int N = a.length;
int[] aux = new int[N];
//定義子數組的大小
for (int sz = 1; sz < N; sz = sz + sz) {
//子數組的索引
for (int lo = 0; lo < N-sz; lo += sz+sz) {
//lo+sz+sz-1可能越界,因此需要一個min函數來取邊界
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
}
}
}
總結
時間複雜度:
O(N)
計數排序、基數排序
O(N2)
冒泡排序、選擇排序、插入排序
O(NlogN)
希爾排序、堆排序、快速排序、歸併排序
空間複雜度:
O(1)
插入排序、選擇排序、冒泡排序、堆排序、希爾排序
O(logN) ~ O(N)
快速排序
O(N)
歸併排序
O(M) M爲桶的數量
計數排序、基數排序
穩定性:
穩定的排序算法:
冒泡排序、插入排序、歸併排序、計數排序、基數排序、桶排序
不穩定的排序算法:
選擇排序、快速排序、希爾排序、堆排序