@author StormMa
@date 2017-10/19
生命不息,奮鬥不止!
常用的排序算法分析與實現,全部代碼庫地址sort,以及algorithms4
前言
排序算法是算法的入門知識,其經典思想可以用於很多算法當中。因爲其實現代碼較短,應用較常見。所以在面試中經常會問到排序算法及其相關的問題。但萬變不離其宗,只要熟悉了思想,靈活運用也不是難事。一般在面試中最常考的是快速排序和歸併排序,並且經常有面試官要求現場寫出這兩種排序的代碼。對這兩種排序的代碼一定要信手拈來纔行。還有插入排序、冒泡排序、堆排序、基數排序、桶排序等。面試官對於這些排序可能會要求比較各自的優劣、各種算法的思想及其使用場景。還有要會分析算法的時間和空間複雜度。通常查找和排序算法的考察是面試的開始,如果這些問題回答不好,估計面試官都沒有繼續面試下去的興趣都沒了。所以想開個好頭就要把常見的排序算法思想及其特點要熟練掌握,有必要時要熟練寫出代碼。
冒泡排序
冒泡排序是最簡單的排序之一了,其大體思想就是通過與相鄰元素的比較和交換來把小的數交換到最前面。這個過程類似於水泡向上升一樣,因此而得名。舉個例子,對5, 3, 8, 6, 4這個無序序列進行冒泡排序。首先從前向後冒泡,5和3比較,把5交換到後面,序列變成3, 5, 8, 4, 6。同理5和8比較,依然是3, 5, 8, 4, 6。8和4交換,變成3, 5, 4, 8, 6。8和6交換,變成3, 5, 4, 6, 8這樣一次冒泡就完了,把最大的數8排到最後面了。對剩下的序列依次冒泡就會得到一個有序序列。冒泡排序的時間複雜度爲O(n^2)。當然也可以從後到前冒泡,依次冒泡得到最小的數排到最前面。
BubbleSort.java
/**
* This {@code BubbleSort} class is implementation of bubble sort.
*
* @author stormma
* @date 2017/10/19
*/
public class BubbleSort implements Sort {
@Override
public void sort(Comparable[] a) {
int length = a.length;
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (!SortUtils.less(a[j], a[j+1])) {
SortUtils.exch(a, j, j+1);
}
}
}
}
}
BubbleSort.c
void bubble_sort(int* array) {
for (int i = 0; i < SIZE - 1; i++) {
for (int j = 0; j < SIZE - 1 - i; j++) {
if (array[j] > array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
選擇排序
選擇排序的思想其實和冒泡排序有點類似,但是過程不同,冒泡排序是通過相鄰的比較和交換。而選擇排序是通過對整體的選擇。舉個栗子,對5, 3, 8, 6, 4這個無序序列進行簡單選擇排序,首先要選擇5以外的最小數來和5交換,也就是選擇3和5交換,一次排序後就變成了3, 5, 8, 6, 4.對剩下的序列一次進行選擇和交換,最終就會得到一個有序序列。其實選擇排序可以看成冒泡排序的優化,因爲其目的相同,只是選擇排序只有在確定了最小數的前提下才進行交換,大大減少了交換的次數。選擇排序的時間複雜度爲O(n^2)
SelectSort.java
/**
* This {@code SelectSort} class is implementation of select sort.
*
* @author stormma
* @date 2017/10/19
*/
public class SelectSort implements Sort {
@Override
public void sort(Comparable[] a) {
int length = a.length;
for (int i = 0; i < length; i++) {
int min = i;
for (int j = i + 1; j < length; j++) {
if (SortUtils.less(a[j], a[min])) {
min = j;
}
}
if (min != i) {
SortUtils.exch(a, i, min);
}
}
}
}
SelectSort.c
void select_sort(int* array) {
for (int i = 0; i < SIZE - 1; i++) {
int min = i;
for (int j = i + 1; j < SIZE; j++) {
if (array[j] < array[min]) {
min = j;
}
}
if (min != i) {
int temp = array[min];
array[min] = array[i];
array[i] = temp;
}
}
}
插入排序
插入排序不是通過交換位置而是通過比較找到合適的位置插入元素來達到排序的目的的。相信大家都有過打撲克牌的經歷,特別是牌數較大的。在分牌時可能要整理自己的牌,牌多的時候怎麼整理呢?就是拿到一張牌,找到一個合適的位置插入。這個原理其實和插入排序是一樣的。舉個例子,對5, 3, 8, 6, 4這個無序序列進行簡單插入排序,首先假設第一個數的位置是正確的,想一下在拿到第一張牌的時候,沒必要整理。然後3要插到5前面,把5後移一位,變成3, 5, 8, 6, 4.想一下整理牌的時候應該也是這樣吧。然後8不用動,6插在8前面,8後移一位,4插在5前面,從5開始都向後移一位。注意在插入一個數的時候要保證這個數前面的數已經有序。簡單插入排序的時間複雜度也是O(n^2)。
InsertSort.java
/**
* This {@code InsertSort} class is implementation of insert sort.
*
* @author stormma
* @date 2017/10/19
*/
public class InsertSort implements Sort {
@Override
public void sort(Comparable[] a) {
int length = a.length;
for (int i = 1; i < length; i++) {
for (int j = i; j > 0 && SortUtils.less(a[j], a[j-1]); j--) {
SortUtils.exch(a, j, j-1);
}
}
}
}
InsertSort.c
void insert_sort(int* array) {
for (int i = 1; i < SIZE; i++) {
for (int j = i; j > 0; j--) {
if (array[j] < array[j-1]) {
int temp = array[j];
array[j] = array[j-1];
array[j-1] = temp;
}
}
}
}
希爾排序
希爾排序是插入排序的優化,試想一下,還是我們打牌的整牌,插入的時候是一步一步的向前找到正確的位置?,不,是個正常的人都不會這麼做,都是一次找到正確的位置插入的。但是對應的算法中,我們沒法一次找到合適的位置,但是我們可以將數組的以不同的間隔gap分組,先在組內進行插入排序,然後縮小間隔gap直至到1,最後就變成了近乎有序的插入排序了。希爾排序基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時, 效率高, 即可以達到線性排序的效率
- 但插入排序一般來說是低效的, 因爲插入排序每次只能將數據移動一位
算法思路:
- 先取一個正整數 d1(d1 < n),把全部記錄分成 d1 個組,所有距離爲 d1 的倍數的記錄看成一組,然後在各組內進行插入排序
- 然後取 d2(d2 < d1)
- 重複上述分組和排序操作;直到取 di = 1(i >= 1) 位置,即所有記錄成爲一個組,最後對這個組進行插入排序。間隔的選擇方式有很多種,而且每一種都是對應的算法複雜度也不一樣。這樣我們以1, 4, 13, 40, 121…這種間隔做示範。希爾排序的平均時間複雜度是O(nlogn),怎麼算法
舉個例子:
ShellSort.java
/**
* This {@code ShellSort} class is implementation of shell sort.
*
* @author stormma
* @date 2017/10/19
*/
public class ShellSort implements Sort {
@Override
public void sort(Comparable[] a) {
//before shell sort
System.out.print("before shell sort==>");
SortUtils.show(a);
int length = a.length;
// 1, 4, 13, 40, 121...
int gap = 1;
while (gap < length / 3) {
gap = gap * 3 + 1;
}
for (; gap > 0; gap /= 3) {
for (int i = gap; i < length; i++) {
for (int j = i; j >= gap && SortUtils.less(a[j], a[j-gap]); j -= gap) {
SortUtils.exch(a, j, j-gap);
}
}
}
}
}
ShellSort.c
void shell_sort(int* array, int len) {
int gap = 1;
while (gap < len / 3) {
gap = gap * 3 + 1;
}
for (; gap > 0; gap /= 3) {
for (int i = gap; i < len; i++) {
for (int j = i; j >= gap; j -= gap) {
if (array[j] < array[j-gap]) {
int temp = array[j];
array[j] = array[j-gap];
array[j-gap] = temp;
}
}
}
}
}
歸併排序(Top-Down)
歸併排序的歸併操作(merge),也叫歸併算法,指的是將兩個已經排序的序列合併成一個序列的操作。歸併排序算法依賴歸併操作。歸併排序其基本思想是,先遞歸劃分子問題,然後合併結果。把待排序列看成由兩個有序的子序列,然後合併兩個子序列,然後把子序列看成由兩個有序序列。倒着來看,其實就是先兩兩合併,然後四四合並。。。最終形成有序序列。空間複雜度爲O(n),時間複雜度爲O(nlogn)。歸併排序過程:
圖一:
[圖片來自維基百科]
圖二:
對應Top-Down MargeSort來說,最重要的環節就是歸併操作,將兩個有序的子數組歸併成一個有序的數組。原地歸併操作,需要引進一個輔助數組aux
。具體代碼如下:
MergeSortTD.java
/**
* This {@code MergeSortTD} is implementation of merge sort top-down
*
* @author stormma
* @date 2017/10/19
*/
public class MergeSortTD implements Sort {
private static Comparable[] aux;
@Override
public void sort(Comparable[] a) {
aux = new Comparable[a.length];
sort(a, 0, a.length - 1);
}
private void sort(Comparable[] a, int low, int high) {
if (high <= low) {
return;
}
int mid = low + ((high - low) >> 1);
sort(a, low, mid);
sort(a, mid + 1, high);
merge(a, low, mid, high);
}
private static void merge(Comparable[] a, int low, int mid, int high) {
int i = low, j = mid + 1;
System.arraycopy(a, low, aux, low, high + 1 - low);
for (int k = low; k <= high; k++) {
if (i <= mid && j <= high) {
a[k] = SortUtils.less(aux[i], aux[j]) ? aux[i++] : aux[j++];
} else if (i > mid) {
a[k] = aux[j++];
} else if (j > high) {
a[k] = aux[i++];
}
}
}
}
MergeSort.c
static int aux[N] = {0};
void merge(int* array, int low, int mid, int high) {
int start1 = low, start2 = mid + 1;
for (int i = start1; i <= high; i++) {
aux[i] = array[i];
}
for (int k = low; k <= high; k++) {
if (start1 <= mid && start2 <= high) {
array[k] = aux[start1] < aux[start2] ? aux[start1++] : aux[start2++];
} else if (start1 > mid) {
array[k] = aux[start2++];
} else if (start2 > high) {
array[k] = aux[start1++];
}
}
}
void _merge_sort(int* array, int low, int high) {
if (high <= low) {
return;
}
int mid = (low + high) >> 1;
_merge_sort(array, low, mid);
_merge_sort(array, mid + 1, high);
merge(array, low, mid, high);
}
void merge_sort(int* array, int length) {
_merge_sort(array, 0, length - 1);
}
歸併排序(Bottom-Up)
同理自頂而下mergesort
MergeSortBU.java
/**
* This {@code MergeSortBU} class is implementation of merge sort bottom-up
*
* @author stormma
* @date 2017/10/19
*/
public class MergeSortBU implements Sort {
private static Comparable[] aux;
@Override
public void sort(Comparable[] a) {
aux = new Comparable[a.length];
int length = a.length;
int step = 2;
for (int sz = 1; sz < length; sz += sz) {
for (int lo = 0; lo < length - sz; lo += step * sz) {
merge(a, lo, lo + sz - 1, Math.min(lo + step * sz - 1, length - 1) );
}
}
}
private static void merge(Comparable[] a, int low, int mid, int high) {
System.out.println("Starting => merge(a, " + low + ", " + mid + ", " + high + ")");
int start1 = low, start2 = mid + 1;
System.arraycopy(a, low, aux, low, high + 1 - low);
for (int k = low; k <= high; k++) {
if (start1 <= mid && start2 <= high) {
a[k] = SortUtils.less(aux[start1], aux[start2]) ? aux[start1++] : aux[start2++];
} else if (start1 > mid) {
a[k] = aux[start2++];
} else if (start2 > high) {
a[k] = aux[start1++];
}
}
System.out.println("Merge result: ");
SortUtils.show(a);
}
}
快速排序
通過一趟掃描將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。和歸併排序有點類似,原理也很簡單。
步驟如下
- 對數組切分,用數組的第一個元素切分,比第一個元素大的放在它後邊,小的放在前面。此時這個切分元素已經在正確位置上了。
- 對前部分[0, pos]和[pos+1, length-1]重複步驟1。直至有序。
QuickSort.java
/**
* This {@code QuickSort} class is implementation of quick sort.
*
* @author stormma
* @date 2017/10/19
*/
public class QuickSort implements Sort {
@Override
public void sort(Comparable[] a) {
int length = a.length;
sort(a, 0, length - 1);
}
private void sort(Comparable[] a, int low, int high) {
if (high <= low) {
return;
}
int j = partition(a, low, high);
sort(a, low, j - 1);
sort(a, j + 1, high);
}
private int partition(Comparable[] a, int low, int high) {
int i = low, j = high + 1;
Comparable value = a[low];
while (true) {
while (SortUtils.less(a[++i], value)) {
if (i == high) {
break;
}
}
while (SortUtils.less(value, a[--j])) {
if (j == low) {
break;
}
}
if (i >= j) {
break;
}
SortUtils.exch(a, i, j);
}
SortUtils.exch(a, low, j);
return j;
}
}
QuickSort.c
int partition(int* array, int low, int high) {
int i = low, j = high + 1;
int value = array[low];
while (1) {
while (array[++i] < value) {
if (i == high)
break;
}
while (value < array[--j]) {
if (j == low)
break;
}
if (i >= j)
break;
exch(array, i, j);
}
exch(array, low, j);
return j;
}
void _quick_sort(int* array, int low, int high) {
if (high <= low) {
return;
}
int j = partition(array, low, high);
_quick_sort(array, low, j - 1);
_quick_sort(array, j + 1, high);
}
void quick_sort(int* array, int length) {
_quick_sort(array, 0, length - 1);
}
三向切分快速排序
對比快速排序,有了下面幾點改變: 對於每次切分:從數組的左邊到右邊遍歷一次,維護三個指針lt,gthe i,其中
- lt指針使得元素(arr[0]-arr[lt-1])的值均小於切分元素;
- gt指針使得元素(arr[gt+1]-arr[N-1])的值均大於切分元素;
- i指針使得元素(arr[lt]-arr[i-1])的值均等於切分元素,(arr[i]-arr[gt])的元素還沒被掃描,切分算法執行到i>gt爲止。
QuickSort3Way.java
/**
* This {@code QuickSort3Way} is implementation of quick sort three-way.
*
* @author stormma
* @date 2017/10/19
*/
public class QuickSort3Way implements Sort {
@Override
public void sort(Comparable[] a) {
sort(a, 0, a.length - 1);
}
private void sort(Comparable[] a, int low, int high) {
if (high <= low) {
return;
}
int lt = low, i = low + 1, gt = high;
Comparable v = a[low];
while (i <= gt) {
int cmp = a[i].compareTo(v);
if (cmp < 0) {
SortUtils.exch(a, lt++, i++);
} else if (cmp > 0) {
SortUtils.exch(a, i, gt--);
} else {
i++;
}
}
sort(a, low, lt - 1);
sort(a, gt + 1, high);
}
}
QuickSort3Way.c
void _quick_sort_3way(int* array, int low, int high) {
if (high <= low) {
return;
}
int lt = low, i = low + 1, gt = high;
int value = array[low];
while (i <= gt) {
int cmp = array[i] - value;
if (cmp < 0)
exch(array, lt++, i++);
else if (cmp > 0)
exch(array, i, gt--);
else
i++;
}
_quick_sort_3way(array, low, lt - 1);
_quick_sort_3way(array, gt + 1, high);
}
void quick_sort_3way(int * array, int length) {
_quick_sort(array, 0, length - 1);
}
堆排序
其原理就是構造最大堆,然後進行下沉排序,原理也很簡單,一點都不復雜。
HeapSort.java
/**
* This {@code HeapSort} class is implementation of heap sort.
*
* @author stormma
* @date 2017/10/19
*/
public class HeapSort implements Sort {
@Override
public void sort(Comparable[] a) {
int N = a.length - 1;
//構造最大堆
for (int k = (N - 1) / 2; k >= 0; k--) {
sink(a, k, N);
}
SortUtils.show(a);
while (N > 0) {
SortUtils.exch(a, 0, N--);
sink(a, 0, N);
}
}
private void sink(Comparable[] a, int index, int N) {
//left sub tree lt N
while ((2 * index + 1) <= N) {
int leftI = 2 * index + 1;
int maxI = leftI, rightI = leftI + 1;
if (leftI < N && SortUtils.less(a[leftI], a[rightI])) {
maxI = rightI;
}
if (!SortUtils.less(a[index], a[maxI])) {
break;
}
SortUtils.exch(a, index, maxI);
index = maxI;
}
}
}
算法複雜度比較
博文來自我的個人博客stormma’s blog,轉載註明出處。