目錄
前言
排序算法在筆試面試中幾乎是必考的,因爲它是很多複雜算法的基礎,也是我們學習數據結構與算法的入門知識。目前網上介紹各類排序算法的博客和帖子非常多,但其中有不少作者提供的代碼有錯誤或者代碼中沒有考慮特殊情況。在此,我們再次總結一下常見的八大排序算法,試圖講清楚各個算法的基本原理,並提供java代碼以及詳細的註釋。所有代碼都是對數組進行升序排列且已經測試通過,但沒有使用足夠的測試用例,如果代碼中存在問題,請大家留言指出,或者有更好的算法思路,也可以交流討論。
冒泡排序
/**
* 冒泡排序
*
* 原理:
* 每一輪循環中依次比較相鄰位置上元素的大小,使得較大的元素後移,
* 且確保第i輪循環完之後能把第i大的元素移動到排序後的位置上
*
* 改進:
* 每一輪循環開始前設置標誌位,如果本輪循環沒有交換任何元素,
* 則說明所有元素已經有序,可以提前結束排序
*
* @author Roamer
*/
void bubbleSort(int[] a) {
if(a == null || a.length == 0) return;
boolean flag;
for(int i = a.length - 1; i > 0; --i) {
flag = true;//每一輪冒泡之前重置標誌位
for(int j = 0; j < i; ++j) {
if(a[j] > a[j+1]) {
swap(a, j, j+1);
flag = false;
}
}
if(flag) break;
}
}
選擇排序
/**
* 選擇排序
*
* 原理:
* 從數組的首元素開始,將第i個元素之後的所有元素通過相互比較找到最小值的索引,
* 如果當前元素比這個最小元素大,則交換之,使得第i個位置的元素值爲第i小,
* 相當於每一輪循環是在所有未排序的元素之中選擇出最小的元素。
*
* @author Roamer
*/
void selectSort(int[] a) {
if(a == null || a.length == 0) return;
for(int i = 0; i < a.length - 1; ++i) {
//找到第i個元素之後的最小元素的下標minIdx
int minIdx = i + 1;
for(int j = i + 2; j < a.length; ++j) {
if(a[j] < a[minIdx])
minIdx = j;
}
//如果當前元素比其後的最小元素還小,則交換之
if(a[i] > a[minIdx])
swap(a, i, minIdx);
}
}
插入排序
/**
* 插入排序
*
* 原理:
* 確保數組前i-1個元素已經排好序,在第i輪循環時,將第i個元素從後往前依次和
* 其前面的元素比較和交換,最後插入到前i-1個有序的子數組中的合適位置
*
* @author Roamer
*/
void insertSort(int[] a) {
if(a == null || a.length == 0) return;
//從數組第二個元素開始進行前插
for(int i = 1; i < a.length; ++i) {
for(int j = i; j > 0 && a[j-1] > a[j]; --j)
swap(a, j-1, j);
}
}
/**
* 插入排序改進
*
* 如果當前元素已經位於正確的位置,則不必繼續往前插入,可以提前結束本輪循環
*
* @author liwendongyang
*/
void insertSort2(int[] a) {
if (a == null || a.length == 0)
return;
// 從數組第二個元素開始進行前插
for (int i = 1; i < a.length; ++i) {
for (int j = i; j > 0; --j) {
if (a[j - 1] > a[j])
swap(a, j - 1, j);
else
break;
}
}
}
快速排序
/**
* 快速排序
*
* 原理:
* 將數組的首元素作爲比較的基準,用兩個指針從數組的兩端往中間掃描,
* 當左指針對應的元素大於基準值且右指針對應的元素小於基準值,則交換兩者對應的元素值,
* 使得每一輪遍歷之後數組分成比基準值更大和更小的兩部分,
* 再把基準元素從數組首位交換到數組的中間,從而其左邊的元素都不大於它,
* 且其右邊的元素都不小於它,然後將左右兩個子數組遞歸調用快排函數,最終使數組有序
*
* @author Roamer
*/
void quickSort(int[] a, int low, int high) {
if(a == null || a.length == 0) return;
if(low < high){
int pivot = partition(a, low, high);
quickSort(a, low, pivot - 1);
quickSort(a, pivot + 1, high);
}
}
//版本一:兩個指針互相交換不合格元素(交換之後就都合格了),最後將基準元素移動到中間
private int partition(int[] a, int low, int high) {
//將數組首元素作爲每一輪比較的基準
int pivot = low;
while(low < high) {
//從右往左掃描,直到遇到比基準元素小的元素
while(low < high && a[high] >= a[pivot])
--high;
//從左往右掃描,直到遇到比基準元素大的元素
while(low < high && a[low] <= a[pivot])
++low;
//將左子數組中不合格的元素與右子數組中不合格的元素交換
swap(a, low, high);
}
//將數組首元素交換到中間位置
swap(a, pivot, low);
//返回數組的中軸位置
return low;
}
//版本一的改進版:
//由於基準元素已經保存了,所以其位置可以被覆蓋掉,且兩個指針是交替掃描的,
//所以右邊(左邊)的不合格元素可以直接覆蓋左邊(右邊)的不合格元素,
//由於high指針先掃描,然後兩個指針的元素交替覆蓋對方,所以循環結束後,
//low對應的位置還沒有被覆蓋,且它就是兩個子數組的分界,將基準元素放到此位置即可
private int partition1(int[] a, int low, int high) {
//將數組首元素作爲每一輪比較的基準
int pivotValue = a[low];
while(low < high) {
//從右往左掃描,直到遇到比基準元素小的元素
while(low < high && a[high] >= pivotValue)
--high;
//將右子數組中不合格的元素放到左邊不合格元素的位置(原元素已經移走)
a[low] = a[high];
//從左往右掃描,直到遇到比基準元素大的元素
while(low < high && a[low] <= pivotValue)
++low;
//將左子數組中不合格的元素放到左邊不合格元素的位置(原元素已經移走)
a[high] = a[low];
}
//將基準元素放到中間位置
a[low] = pivotValue;
//返回數組的中軸位置
return low;
}
//版本二:保持兩個指針中總有一個指向基準元素,所以每次交換都是不合格元素與基準元素做交換,
//當兩個指針在數組中間相遇時,low一定指向着基準元素
private int partition2(int[] a, int low, int high) {
//將數組首元素作爲每一輪比較的基準
int pivotValue = a[low];
while(low < high) {
//從右往左掃描,直到遇到比基準元素小的元素
while(low < high && a[high] >= pivotValue)
--high;
//將右子數組中不合格的元素與基準元素交換
swap(a, low, high);
//從左往右掃描,直到遇到比基準元素大的元素
while(low < high && a[low] <= pivotValue)
++low;
//將左子數組中不合格的元素與基準元素交換
swap(a, low, high);
}
//返回數組的中軸位置,low必定指向了基準元素pivotValue
return low;
}
歸併排序
/**
* 歸併排序(遞歸版)
*
* 原理:
* 將數組均分成兩個子數組,先將兩個子數組分別進行排序,然後合併得到全體元素都有序的數組,
* 爲使上述的兩個子數組分別有序,需要先對其各自的兩個子數組進行排序再合併,因此需要遞歸地
* 對每個子數組的兩個子數組進行歸併排序,直到子數組只有2個元素,此時只需要直接進行合併
*
* @author Roamer
*/
void MergeSort(int[] a) {
if(a == null || a.length == 0) return;
int[] b = new int[a.length];//輔助數組
Merge(a, b, 0, a.length-1, (a.length-1)/2);
}
//對數組a的兩個子數組進行歸併排序
private void Merge(int[] a, int[] b, int low, int high, int pivot) {
//先遞歸地劃分子數組(子數組最小長度爲2),並對子數組進行歸併排序
if(low < high) {
Merge(a, b, low, pivot, (low+pivot)/2);
Merge(a, b, pivot+1, high, (high+pivot+1)/2);
}
//將已經排好序的兩個子數組元素依次進行比較再合併
int i = low;
int j = pivot+1;
int k = low;
while(i <= pivot && j <= high){
if(a[i] < a[j])
b[k++] = a[i++];
else
b[k++] = a[j++];
}
//取出子數組中可能的剩餘元素(每次只可能有一個while執行)
while(i <= pivot) b[k++] = a[i++];
while(j <= high) b[k++] = a[j++];
//將本次排好序的部分元素拷貝回原數組a
System.arraycopy(b, low, a, low, high-low+1);
}
/**
* 歸併排序(迭代版)
*
* 原理:
* 先將整個數組依次劃分成若干個長度爲2的子數組,對每個子數組中的兩個元素進行合併,
* 再將整個數組依次劃分成若干個長度爲4的子數組,對每個子數組中的兩個子數組進行合併,
* 如此循環,直到只能將數組劃分成兩個子數組,這兩個子數組已經分別有序,直接合並即可
*
* @author Roamer
*/
//利用分治策略,對數組a的各級子數組進行迭代歸併
void MergeSort2(int[] a) {
if(a == null || a.length == 0) return;
int[] b = new int[a.length];//輔助數組
int len = 2;//每一輪合併中數組的長度
while(len <= a.length) {
//將前若干組中兩個等長的子數組合並
int i = 0;
while(i + len <= a.length) {
Merge2(a, b, i, i+len-1, i+(len-1)/2);
i += len;
}
//若原數組長度不是2的冪,則數組可能不能被均分
//從而最後一組的兩個子數組長度會不同,單獨合併之
if(i != a.length)
Merge2(a, b, i, a.length-1, (i+a.length)/2);
//下一輪合併中數組的長度翻倍
len <<= 1;
//將本輪分組有序的元素拷貝回原數組a
System.arraycopy(b, 0, a, 0, a.length);
}
//若原數組長度不是2的冪,需要最後合併一次!
if(len != a.length) {
Merge2(a, b, 0, a.length-1, (len-1)/2);
System.arraycopy(b, 0, a, 0, a.length);
}
}
private void Merge2(int[] a, int[] b, int low, int high, int pivot) {
//將已經排好序的兩個子數組元素依次進行比較再合併
int i = low;
int j = pivot+1;
int k = low;
while(i <= pivot && j <= high){
if(a[i] < a[j])
b[k++] = a[i++];
else
b[k++] = a[j++];
}
//取出子數組中可能的剩餘元素(每次只可能有一個while執行)
while(i <= pivot) b[k++] = a[i++];
while(j <= high) b[k++] = a[j++];
}
堆排序
/**
* 堆排序
*
* 原理:
* 先將無序數組構成一個二叉堆(完全二叉樹),使得每個節點都小於其子節點(兄弟節點之間可以無序),
* 每次取出根節點後,重新調整堆使其仍滿足上述特性,每次取出的根節點就構成了一個有序數組。
*
* 建堆
* 用數組來存放整個二叉堆,且數組的首元素中存儲着整個二叉堆的節點總個數,
* 建堆時總是在二叉堆的葉子節點處插入新節點,然後“上浮”該節點,最終在數組中得到一個二叉堆。
*
* 調整堆
* 每次在葉子節點處插入新節點即在數組末位插入新元素需要調整堆,比較新節點和其父節點的大小,
* 通過不斷交換使其“上浮”,並最終位於二叉樹的合適位置;
* 每次取出二叉堆的根節點即取出數組的第二個元素後需要調整堆,將二叉樹的最後一個葉子節點作爲新的根節點,
* 然後依次比較其和子節點的大小,通過不斷交換使其“下沉”,並最終位於二叉樹的合適位置;
*
* @author Roamer
*/
void heapSort(int[] a) {
int[] b = new int[a.length + 1];//二叉堆數組
//建堆
for(int i = 0; i < a.length; ++i)
insert(b, a[i]);
//得到有序數組
for(int i = 0; i < a.length; ++i)
a[i] = getRoot(b);
}
//往二叉堆插入新節點,並調整堆
private void insert(int[] heap, int ele) {
heap[0] += 1;//節點總數加1
heap[heap[0]] = ele;
goUp(heap);
}
//取出二叉堆的根節點,並調整堆
private int getRoot(int[] heap) {
if (heap[0] < 1)
throw new RuntimeException("二叉堆已經爲空,不能再取出元素!");
int root = heap[1];//取出根節點元素
heap[1] = heap[heap[0]];//將二叉堆的最後一個葉子節點作爲新的根節點
heap[0] -= 1;//節點總數減1
goDown(heap);
return root;
}
//根節點"下沉"
private void goDown(int[] heap) {
int idx = 1;//需要下沉的根節點的索引
int left, right, minIdx;//minIdx表示左右子節點中較小的那個
boolean flag = true;//是否有元素交換的標誌位
//如果上一輪循環有元素交換則繼續交換
while(flag) {
flag = false;
left = (idx << 1);//左子節點
right = left + 1;//右子節點
if (left > heap[0])//無子節點
break;
else if (right > heap[0])//只有左子節點
minIdx = left;
else
minIdx = (heap[left] < heap[right]) ? left : right;
if (heap[idx] > heap[minIdx]) {
swap(heap, idx, minIdx);
idx = minIdx;
flag = true;//本次循環有元素交換
}
}
}
//末位葉子節點“上浮”
private void goUp(int[] heap) {
int idx = heap[0];
int parent = (idx >> 1);
while(parent > 0 && heap[idx] < heap[parent]) {
swap(heap, idx, parent);
idx = parent;
parent = (idx >> 1);
}
}
希爾排序
/**
* 希爾排序(縮小增量排序)
*
* 原理:
* 先設置一個較大的步長(增量),將數組分爲若干個子序列,對每個子序列分別進行排序,
* 再減少步長,再次將數組分爲若干個更長的子序列,對每個子序列分別進行排序,
* 如此循環,直到步長爲1,即整個數組中只有一個子序列,此時整個數組已經有序
* 其中,子序列的排序可以採用任何其他排序算法,每一輪排序之後,數組將變得更加有序一些
*
* @author Roamer
*/
void shellSort(int[] a) {
//得到初始步長
int step = 1;
while(step < a.length)
step = 3*step + 1;
while(step > 1) {
//縮小步長
step = step / 3 + 1;
for (int i = 0; i < step; ++i) {
// 得到子序列數組
int nsub = (a.length - i - 1) / step + 1;
int[] sub = new int[nsub];
for (int j = 0; j < nsub; ++j)
sub[j] = a[i + j * step];
//對子序列數組進行冒泡排序
bubbleSort(sub);
//將排序後的元素保存到原數組的對應位置
for (int j = 0; j < nsub; j++)
a[i + j * step] = sub[j];
}
}
}
基數排序
/**
* 基數排序
*
* 原理:
* 基數排數基於桶排序的思想。如果需要排序的元素是正整數,則可以通過依次比較他們每個數位上
* 數字的大小進行排序,由於十進制只有10個數碼,所以只需要10個“桶”就夠了。
* 基數排序的方式可以採用LSD(Least sgnificant digital)或MSD(Most sgnificant digital),
* 如果是採用LSD算法,則從個位開始,將所有整數根據個位數字分別放到對應的10個桶中,
* 再按順序從各個桶中將所有整數取出依次填回原數組,然後將所有整數根據十位數字重新放到新的10個桶中,
* 以此類推,直到所有元素的所有數位都遍歷完,由於基數排序是穩定的,所以數組中的所有元素最終都有序了
*
* @author Roamer
*/
void radixSort(int[] a, int len) {
int k = 0;//用於遍歷數組的下標指針
int m = 1;//表示當前用於比較的數位,從個位開始
int n = 1;//表示數位m對應的權重,即1,10,100,1000...
//數組的第一維爲每個數位上可能出現的數字(0~9),即桶的個數,
//第二維是包含當前數位的元素可能的總個數,即每個桶中可以放入的整數個數,
//數組的元素值記錄了數位是lsd的整數
int[][] bucket = new int[10][a.length];
int[] order = new int[10];//用於記錄每個桶中整數的總個數
while(m <= len) {
//入桶,即將數組中的所有整數按照數位m放到對應的桶中
for(int i = 0; i < a.length; ++i) {
int lsd = (a[i] / n) % 10;//通過取整取餘得到數位m上的數字lsd
bucket[lsd][order[lsd]] = a[i];//記錄新出現的數位是lsd的整數
order[lsd]++;//數位是lsd的整數的個數加1
}
//出桶,即從10個桶中依次取出整數,填回數組中(即記錄當前已排好的相對順序)
for(int i = 0; i < 10; i++) {
//如果當前桶不爲空
if(order[i] != 0) {
//依次取出當前桶中的記錄的整數(bucket數組第二維中元素必定是連續的)
for(int j = 0; j < order[i]; j++)
a[k++] = bucket[i][j];
//清空對桶中整數個數的記錄
order[i] = 0;
}
}
//完成當前數位上的排序,準備下一輪排序
k = 0;//將數組a的下標重置爲0
m++;//數位往高位增加
n *= 10;//數位對應的權重增加
}
}
參考資料
2: 面試中的排序算法總結
3: 基數排序——百度百科