這裏抽象出了一個排序基類:
public abstract class BaseSort {
public abstract int[] sort(int[] array);
public void swap(int[] arr, int a, int b) {
int temp;
temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
下面我會用7個實現類分別實現7種排序算法。
1.插入排序
數據結構 數組
最差時間複雜度 O(n^2)
最優時間複雜度 O(n)
平均時間複雜度 O(n^2)
最差空間複雜度 總共O(n) ,需要輔助空間O(1)
1.1.動圖展示
1.2.算法描述
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
1. 從第一個元素開始,該元素可以認爲已經被排序
2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
5. 將新元素插入到該位置後
6. 重複步驟2~5如果比較操作的代價比交換操作大的話,可以採用二分查找法來減少比較操作的數目。該算法可以認爲是插入排序的一個變種,稱爲二分查找插入排序。
1.3.示例代碼
public static class InsertSort extends BaseSort {
@Override
public int[] sort(int[] array) {
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j;
for (j = i - 1; j >= 0 && temp < array[j]; j--) {
array[j + 1] = array[j];
}
array[j + 1] = temp;
}
return array;
}
}
2.選擇排序
數據結構 數組
最差時間複雜度 О(n²)
最優時間複雜度 О(n²)
平均時間複雜度 О(n²)
最差空間複雜度 О(n) total, O(1) auxiliary
2.1.動圖展示
2.2.算法描述
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的主要優點與數據移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。
2.3.示例代碼
public class SelectSort extends BaseSort {
@Override
public int[] sort(int[] array) {
for (int i = 0; i < array.length; i++) {
int miniPost = i;
for (int m = i + 1; m < array.length; m++) {
if (array[m] < array[miniPost]) {
miniPost = m;
}
}
if (array[i] > array[miniPost]) {
int temp;
temp = array[i];
array[i] = array[miniPost];
array[miniPost] = temp;
}
}
return array;
}
}
3.冒泡排序
數據結構 數組
最差時間複雜度 O(n^2)
最優時間複雜度 O(n)
平均時間複雜度 O(n^2)
最差空間複雜度 總共O(n),需要輔助空間O(1)
3.1.動圖展示
3.2.算法描述
冒泡排序算法的運作如下:
1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
3. 針對所有的元素重複以上的步驟,除了最後一個。
4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
3.3.示例代碼
public class BubbleSort extends BaseSort {
@Override
public int[] sort(int[] array) {
int temp;
// 第一層循環:表明比較的次數, 比如 length 個元素,比較次數爲 length-1 次(肯定不需和自己比)
for (int i = 0; i < array.length - 1; i++) {
for (int j = array.length - 1; j > i; j--) {
if (array[j] < array[j - 1]) {
swap(array, j, j - 1);
}
}
}
return array;
}
}
4.快速排序
數據結構 不定
最差時間複雜度 О(n^2)
最優時間複雜度 О(n log n)
平均時間複雜度 О(n log n)
最差空間複雜度 根據實現的方式不同而不同
4.1動圖展示:
4.2.算法描述:
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分爲兩個子序列(sub-lists)。
步驟爲:
1. 從數列中挑出一個元素,稱爲”基準”(pivot),
2. 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
3. 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會結束,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
4.3.示例代碼:
public class QuickSort extends BaseSort {
@Override
public int[] sort(int[] array) {
return sortSub(array, 0, array.length - 1);
}
//排序子數組,採用分治思想,不斷遞歸迭代,當每個子數組都排好了,源數組也就排好了
private int[] sortSub(int[] arr, int low, int heigh) {
if (low < heigh) {
int division = partition(arr, low, heigh);
sortSub(arr, low, division - 1);
sortSub(arr, division + 1, heigh);
}
return arr;
}
// 分水嶺,基位,左邊的都比這個位置小,右邊的都大
private int partition(int[] arr, int low, int heigh) {
int base = arr[low]; //用子表的第一個記錄做樞軸(分水嶺)記錄
while (low < heigh) { //從表的兩端交替向中間掃描
while (low < heigh && arr[heigh] >= base) {
heigh--;
}
// base 賦值給 當前 heigh 位,base 挪到(互換)到了這裏,heigh位右邊的都比base大
swap(arr, heigh, low);
while (low < heigh && arr[low] <= base) {
low++;
}
// 遇到左邊比base值大的了,換位置
swap(arr, heigh, low);
}
// now low = heigh;
return low;
}
}
5.合併排序
數據結構 數組
最差時間複雜度 O(n log n)
最優時間複雜度 O(n)
平均時間複雜度 O(n log n)
最差空間複雜度 O(n)
5.1.動圖展示
5.2.算法描述
歸併操作(merge),也叫歸併算法,指的是將兩個已經排序的序列合併成一個序列的操作。歸併排序算法依賴歸併操作。
迭代法:
1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
4. 重複步驟3直到某一指針到達序列尾
5. 將另一序列剩下的所有元素直接複製到合併序列尾遞歸法:
原理如下(假設序列共有n個元素):
1. 將序列每相鄰兩個數字進行歸併操作,形成floor(n/2)個序列,排序後每個序列包含兩個元素
2. 將上述序列再次歸併,形成floor(n/4)個序列,每個序列包含四個元素
3. 重複步驟2,直到所有元素排序完畢
5.3.示例代碼
public class MergeSort extends BaseSort {
@Override
public int[] sort(int[] array) {
return sortSub(array, 0, array.length - 1);
}
private int[] sortSub(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左邊
sortSub(nums, low, mid);
// 右邊
sortSub(nums, mid + 1, high);
// 左右歸併
merge(nums, low, mid, high);
}
return nums;
}
private void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指針
int j = mid + 1;// 右指針
int k = 0;
// 把較小的數先移到新數組中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左邊剩餘的數移入數組
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右邊邊剩餘的數移入數組
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新數組中的數覆蓋nums數組
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
}
6.希爾排序
數據結構 數組
最差時間複雜度 根據步長序列的不同而不同。已知最好的:O(n log^2 n)
最優時間複雜度 O(n)
平均時間複雜度 根據步長序列的不同而不同。
最差空間複雜度 O(n)
6.1.動圖展示
以23, 10, 4, 1的步長序列進行希爾排序
6.2.算法描述
希爾排序通過將比較的全部元素分爲幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後算法再取越來越小的步長進行排序,算法的最後一步就是普通的插入排序,但是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。
假設有一個很小的數據在一個已按升序排好序的數組的末端。如果用複雜度爲O(n2)的排序(冒泡排序或插入排序),可能會進行n次的比較和交換才能將該數據移至正確位置。而希爾排序會用較大的步長移動數據,所以小數據只需進行少數比較和交換即可到正確位置。
一個更好理解的希爾排序實現:將數組列在一個表中並對列排序(用插入排序)。重複這過程,不過每次用更長的列來進行。最後整個表就只有一列了。將數組轉換至表是爲了更好地理解這算法,算法本身僅僅對原數組進行排序(通過增加索引的步長,例如是用i += step_size而不是i++)。
6.3.示例代碼
public class ShellSort extends BaseSort {
@Override
public int[] sort(int[] array) {
// 取增量
int step = array.length / 2;
while (step >= 1) {
for (int i = step; i < array.length; i++) {
int temp = array[i];
int j = 0;
// 跟插入排序的區別就在這裏
for (j = i - step; j >= 0 && temp < array[j]; j -= step) {
array[j + step] = array[j];
}
array[j + step] = temp;
}
step /= 2;
}
return array;
}
}
7.堆排序
7.1.動圖展示
堆排序算法的演示。首先,將元素進行重排,以匹配堆的條件。圖中排序過程之前簡單的繪出了堆樹的結構。
7.2.算法描述
堆節點的訪問:
通常堆是通過一維數組來實現的。在數組起始位置爲0的情形中:
1. 父節點i的左子節點在位置(2*i+1);
2. 父節點i的右子節點在位置(2*i+2);
3. 子節點i的父節點在位置floor((i-1)/2);堆的操作:
在堆的數據結構中,堆中的最大值總是位於根節點。堆中定義以下幾種操作:
1. 最大堆調整(Max_Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
2. 創建最大堆(Build_Max_Heap):將堆所有數據重新排序
3. 堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算
7.3.示例代碼
public class HeapSort extends BaseSort {
@Override
public int[] sort(int[] array) {
buildHeap(array);// 構建堆
int n = array.length;
int i = 0;
for (i = n - 1; i >= 1; i--) {
swap(array, 0, i);
heapify(array, 0, i);
}
return array;
}
private void buildHeap(int[] array) {
int n = array.length;// 數組中元素的個數
for (int i = n / 2 - 1; i >= 0; i--)
heapify(array, i, n);
}
private void heapify(int[] A, int idx, int max) {
int left = 2 * idx + 1;// 左孩子的下標(如果存在的話)
int right = 2 * idx + 2;// 左孩子的下標(如果存在的話)
int largest = 0;// 尋找3個節點中最大值節點的下標
if (left < max && A[left] > A[idx])
largest = left;
else
largest = idx;
if (right < max && A[right] > A[largest])
largest = right;
if (largest != idx) {
swap(A, largest, idx);
heapify(A, largest, max);
}
}
// 建堆函數,認爲【s,m】中只有 s
// 對應的關鍵字未滿足大頂堆定義,通過調整使【s,m】成爲大頂堆
public void heapAdjust(int[] array, int s, int m) {
// 用0下標元素作爲暫存單元
array[0] = array[s];
// 沿孩子較大的結點向下篩選
for (int j = 2 * s; j <= m; j *= 2) {
// 保證j爲較大孩子結點的下標,j < m 保證 j+1 <= m ,不越界
if (j < m && array[j] < array[j + 1]) {
j++;
}
if (!(array[0] < array[j])) {
break;
}
// 若S位較小,應將較大孩子上移
array[s] = array[j];
// 較大孩子的值變成S位的較小值,可能引起頂堆的不平衡,故對其所在的堆進行篩選
s = j;
}
// 若S位較大,則值不變;否則,S位向下移動至2*s、4*s、。。。
array[s] = array[0];
}
}
參考目錄:
1. 維基百科
2. java常用的7大排序算法彙總