本文就是介紹一些常見的排序算法。排序是一個非常常見的應用場景,很多時候,我們需要根據自己需要排序的數據類型,來自定義排序算法,但是,在這裏,我們只介紹這些基礎排序算法,包括:插入排序、選擇排序、冒泡排序、快速排序(重點)、堆排序、歸併排序等等。看下圖:

給定數組:int data[] = {9,2,7,19,100,97,63,208,55,78}
一、直接插入排序(內部排序、O(n2)、穩定)
原理:從待排序的數中選出一個來,插入到前面的合適位置。
- package com.xtfggef.algo.sort;
-
- public class InsertSort {
-
- static int data[] = { 9, 2, 7, 19, 100, 97, 63, 208, 55, 78 };
-
- public static void insertSort() {
- int tmp, j = 0;
- for (int k = 0; k < data.length; k++) {//-----1-----
- tmp = data[k];
- j = k - 1;
- while (j >= 0 && tmp < data[j]) {//-----2-----
- data[j + 1] = data[j];
- j--;
- }
- data[j + 1] = tmp;//------3-------
- }
- }
-
- public static void main(String[] args) {
- print();
- System.out.println();
- insertSort();
- System.out.println();
- print();
- }
-
- static void print() {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- }
我簡單的講解一下過程:思路上從待排序的數據中選出一個,插入到前面合適的位置,耗時點在插入方面,合適的位置意味着我們需要進行比較找出哪是合適的位置,舉個例子:對於
9,2,7,19,100,97,63,208,55,78這組數,第一個數9前面沒有,不做操作,當第一個數完後,剩下的數就是待排序的數,我們將要從除去9開始的書中選出一個插入到前面合適的位置,拿到2後,放在tmp上,進行註釋中的2處的代碼,2處的代碼就是通過循環找出這個合適的位置,發現比tmp大的數,立即將該數向後移動一位(這樣做的目的是:前面需要空出一位來進行插入),最後通過註釋3處的代碼將數插入。
本排序適合:基本有序的數據
二、選擇排序(O(n2)、不穩定)
與直接插入排序正好相反,選擇排序是從待排序的數中選出最小的放在已經排好的後面,這個算法選數耗時。
- package com.xtfggef.algo.sort;
-
- public class SelectSort {
-
- static int data[] = { 9, 2, 7, 19, 100, 97, 63, 208, 55, 78 };
-
- public static void selectSort() {
- int i, j, k, tmp = 0;
- for (i = 0; i < data.length - 1; i++) {
- k = i;
- for (j = i + 1; j < data.length; j++)
- if (data[j] < data[k])
- k = j;
- if (k != i) {
- tmp = data[i];
- data[i] = data[k];
- data[k] = tmp;
- }
- }
- }
- public static void main(String[] args) {
- print();
- System.out.println();
- selectSort();
- System.out.println();
- print();
- }
-
- static void print() {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- }
通過循環,找出最小的數的下標,賦值於k,即k永遠保持待排序數據中最小的數的下標,最後和當前位置i互換數據即可。
三、快速排序(O(nlogn)、不穩定)
快速排序簡稱快排,是一種比較快的排序,適合基本無序的數據,爲什麼這麼說呢?下面我說下快排的思路:
設置兩個指針:i和j,分別指向第一個和最後一個,i像後移動,j向前移動,選第一個數爲標準(一般這樣做,當然快排的關鍵就是這個「標準」的選取),從後面開始,找到第一個比標準小的數,互換位置,然後再從前面,找到第一個比標準大的數,互換位置,第一趟的結果就是標準左邊的都小於標準,右邊的都大於標準(但不一定有序),分成兩撥後,繼續遞歸的使用上述方法,最終有序!代碼如下:
- package com.xtfggef.algo.sort;
-
- public class QuickSortTest {
-
- static class QuickSort {
-
- public int data[];
-
- private int partition(int array[], int low, int high) {
- int key = array[low];
- while (low < high) {
- while (low < high && array[high] >= key)
- high--;
- array[low] = array[high];
- while (low < high && array[low] <= key)
- low++;
- array[high] = array[low];
- }
- array[low] = key;
- return low;
- }
-
- public int[] sort(int low, int high) {
- if (low < high) {
- int result = partition(data, low, high);
- sort(low, result - 1);
- sort(result + 1, high);
- }
- return data;
- }
- }
-
- static void print(int data[]) {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- public static void main(String[] args) {
- int data[] = { 20, 3, 10, 9, 186, 99, 200, 96, 3000 };
- print(data);
- System.out.println();
- QuickSort qs = new QuickSort();
- qs.data = data;
- qs.sort(0, data.length - 1);
- print(data);
- }
- }
看看上面的圖,基本就明白了。
四、冒泡排序(穩定、基本有序可達O(n),最壞情況爲O(n2))
冒泡排序是一種很簡單,不論是理解還是時間起來都比較容易的一種排序算法,思路簡單:小的數一點一點向前起泡,最終有序。
- package com.xtfggef.algo.sort;
-
- public class BubbleSort {
-
- static int data[] = { 9, 2, 7, 19, 100, 97, 63, 208, 55, 78 };
-
- public static void bubbleSort() {
- int i, j, tmp = 0;
- for (i = 0; i < data.length - 1; i++) {
- for (j = data.length - 1; j > i; j--) {
- if (data[j] < data[j - 1]) {
- tmp = data[j];
- data[j] = data[j - 1];
- data[j - 1] = tmp;
- }
- }
- }
- }
-
- public static void main(String[] args) {
- print();
- System.out.println();
- bubbleSort();
- System.out.println();
- print();
- }
-
- static void print() {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- }
五、堆排序
我們這裏不詳細介紹概念,堆的話,大家只要記得堆是一個完全二叉樹(什麼是完全二叉樹,請不懂的讀者去查資料),堆排序分爲兩種堆,大頂堆和小頂堆,大頂堆的意思就是堆頂元素是整個堆中最大的,小頂堆的意思就是堆頂元素是整個堆中最小的,滿足:任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。堆排序是一個相對難理解的過程,下面我會較爲清楚、詳細的講解一下堆排序。堆排序分爲三個過程:
建堆:從一個數組順序讀取元素,建立一個堆(完全二叉樹)
初始化:將堆進行調整,使得堆頂爲最大(最大堆)或者最小(最小堆)的元素
維護:將堆頂元素出堆後,需要將堆的最後一個節點補充到堆頂,因爲這樣破壞了堆的秩序,所以需要進行維護。下面我們圖示一下:
一般情況,建堆和初始化同步進行,


最後爲如下所示,即爲建堆、初始化成功。

我們可以觀察下這個最大堆,看出堆頂是整個堆中最大的元素,而且除葉子節點外每個節點都大於其子節點。下面的過程就是當我們輸出堆頂元素後,對堆進行維護。

過程是這樣:將堆頂元素出堆後,用最後一個元素補充堆頂元素,這樣破壞了之前的秩序,需要重新維護堆,在堆頂元素的左右節點中選出較小的和堆頂互換,然後一直遞歸下去,所以每次出一個元素,需要一次維護,堆排序適合解決topK問題,能將複雜度降到nlogK。下面是代碼:
- package com.xtfggef.algo.sort;
-
- public class HeapSort {
- private static int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12,
- 17, 34, 11 };
-
- public static void main(String[] args) {
- buildMaxHeapify(sort);
- heapSort(sort);
- print(sort);
- }
-
- private static void buildMaxHeapify(int[] data) {
- // 沒有子節點的才需要創建最大堆,從最後一個的父節點開始
- int startIndex = getParentIndex(data.length - 1);
- // 從尾端開始創建最大堆,每次都是正確的堆
- for (int i = startIndex; i >= 0; i--) {
- maxHeapify(data, data.length, i);
- }
- }
-
- /**
- * 創建最大堆
- *
- * @param data
- * @param heapSize
- * 需要創建最大堆的大小,一般在sort的時候用到,因爲最多值放在末尾,末尾就不再歸入最大堆了
- * @param index
- * 當前需要創建最大堆的位置
- */
- private static void maxHeapify(int[] data, int heapSize, int index) {
- // 當前點與左右子節點比較
- int left = getChildLeftIndex(index);
- int right = getChildRightIndex(index);
-
- int largest = index;
- if (left < heapSize && data[index] < data[left]) {
- largest = left;
- }
- if (right < heapSize && data[largest] < data[right]) {
- largest = right;
- }
- // 得到最大值後可能需要交換,如果交換了,其子節點可能就不是最大堆了,需要重新調整
- if (largest != index) {
- int temp = data[index];
- data[index] = data[largest];
- data[largest] = temp;
- maxHeapify(data, heapSize, largest);
- }
- }
-
- /**
- * 排序,最大值放在末尾,data雖然是最大堆,在排序後就成了遞增的
- *
- * @param data
- */
- private static void heapSort(int[] data) {
- // 末尾與頭交換,交換後調整最大堆
- for (int i = data.length - 1; i > 0; i--) {
- int temp = data[0];
- data[0] = data[i];
- data[i] = temp;
- maxHeapify(data, i, 0);
- }
- }
-
- /**
- * 父節點位置
- *
- * @param current
- * @return
- */
- private static int getParentIndex(int current) {
- return (current - 1) >> 1;
- }
-
- /**
- * 左子節點position 注意括號,加法優先級更高
- *
- * @param current
- * @return
- */
- private static int getChildLeftIndex(int current) {
- return (current << 1) + 1;
- }
-
- /**
- * 右子節點position
- *
- * @param current
- * @return
- */
- private static int getChildRightIndex(int current) {
- return (current << 1) + 2;
- }
-
- private static void print(int[] data) {
- int pre = -2;
- for (int i = 0; i < data.length; i++) {
- if (pre < (int) getLog(i + 1)) {
- pre = (int) getLog(i + 1);
- System.out.println();
- }
- System.out.print(data[i] + " |");
- }
- }
-
- /**
- * 以2爲底的對數
- *
- * @param param
- * @return
- */
- private static double getLog(double param) {
- return Math.log(param) / Math.log(2);
- }
- }
慢慢理解一下,還是容易明白的!
六、歸併排序
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
首先考慮下如何將將二個有序數列合併。這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。然後再進行比較,如果有數列爲空,那直接將另一個數列的數據依次取出即可。
- package com.xtfggef.algo.sort;
-
- public class SortTest {
-
- // 將有序數組a[]和b[]合併到c[]中
- static void MemeryArray(int a[], int n, int b[], int m, int c[]) {
- int i, j, k;
-
- i = j = k = 0;
- while (i < n && j < m) {
- if (a[i] < b[j])
- c[k++] = a[i++];
- else
- c[k++] = b[j++];
- }
-
- while (i < n)
- c[k++] = a[i++];
-
- while (j < m)
- c[k++] = b[j++];
- }
-
- public static void main(String[] args) {
- int a[] = { 2, 7, 8, 10, 299 };
- int b[] = { 5, 9, 14, 20, 66, 88, 92 };
- int c[] = new int[a.length + b.length];
- MemeryArray(a, 5, b, 7, c);
- print(c);
- }
-
- private static void print(int[] c) {
- for (int i = 0; i < c.length; i++) {
- System.out.print(c[i] + " ");
- }
- }
- }
可以看出合併有序數列的效率是比較高的,可以達到O(n)。解決了上面的合併有序數列問題,再來看歸併排序,其的基本思路就是將數組分成二組A,B,如果這二組組內的數據都是有序的,那麼就可以很方便的將這二組數據進行排序。如何讓這二組組內數據有序了?
可以將A,B組各自再分成二組。依次類推,當分出來的小組只有一個數據時,可以認爲這個小組組內已經達到了有序,然後再合併相鄰的二個小組就可以了。這樣通過先遞歸的分解數列,再合併數列就完成了歸併排序。下面是歸併排序代碼:
- package com.xtfggef.algo.sort;
-
- public class MergeSort {
-
- private static void mergeSort(int[] data, int start, int end) {
- if (end > start) {
- int pos = (start + end) / 2;
- mergeSort(data, start, pos);
- mergeSort(data, pos + 1, end);
- merge(data, start, pos, end);
- }
- }
-
- private static void merge(int[] data, int start, int pos, int end) {
- int len1 = pos - start + 1;
- int len2 = end - pos;
- int A[] = new int[len1 + 1];
- int B[] = new int[len2 + 1];
- for (int i = 0; i < len1; i++) {
- A[i] = data[i + start - 1];
- }
- A[len1] = Integer.MAX_VALUE;
- for (int i = 0; i < len2; i++) {
- B[i] = data[i + pos];
- }
- B[len2] = Integer.MAX_VALUE;
- int m = 0, n = 0;
- for (int i = start - 1; i < end; i++) {
- if (A[m] > B[n]) {
- data[i] = B[n];
- n++;
- } else {
- data[i] = A[m];
- m++;
- }
- }
- }
-
- private static void print(int[] data) {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- public static void main(String args[]) {
- int data[] = { 8, 16, 99, 732, 10, 1, 29, 66 };
- print(data);
- System.out.println();
- mergeSort(data, 1, data.length);
- print(data);
- }
- }
七、希爾排序(不穩定、O(nlogn))
d1 = n/2,d2 = d1/2 ...
舉例一下:{9,8,7,6,5,4,3,2,1,0} 10個數,現分爲5組(9,4),(8,3),(7,2),(6,1),(5,0),然後分別對每組進行直接插入排序得到:
(4,9),(3,8),(2,7),(1,6),(0,5),再將這5組分爲2組(4,3,2,1,0),(9,8,7,6,5)分別對這兩組進行直插排序,得:(0,1,2,3,4),(5,6,7,8,9)最終有序。
- package com.xtfggef.algo.sort;
-
- public class ShellSort {
-
- static void shellsort(int[] a, int n) {
- int i, j, temp;
- int gap = 0;
- while (gap <= n) {
- gap = gap * 3 + 1;
- }
- while (gap > 0) {
- for (i = gap; i < n; i++) {
- j = i - gap;
- temp = a[i];
- while ((j >= 0) && (a[j] > temp)) {
- a[j + gap] = a[j];
- j = j - gap;
- }
- a[j + gap] = temp;
- }
- gap = (gap - 1) / 3;
- }
- }
-
- static void print(int data[]) {
- for (int i = 0; i < data.length; i++) {
- System.out.print(data[i] + " ");
- }
- }
-
- public static void main(String[] args) {
- int data[] = { 2, 68, 7, 19, 1, 28, 66, 200 };
- print(data);
- System.out.println();
- shellsort(data, data.length);
- print(data);
- }
- }
八、多路快排
JDK1.8中Arrays.sort()採用的排序算法,具有較快的時間複雜度和穩定性,基本思路爲:
1. 選取兩個中軸P1, P2。
2. 假設P1<P2,否則交換。
3. 過程中原數組會分爲四個部分:小於中軸1,大於中軸2,介於兩個中軸之間,未排序部分(剛開始除了兩個中軸,其它元素都屬於這部分)。
4. 開始後,從未排序部分選取一個數,和兩個中軸作比較,然後放到合適的位置,一直到未排序部分無數據,結束一趟排序。
5. 遞歸地處理子數組,穩定排序,時間複雜度穩定爲O(nlogn)。
詳情可以參見我的另一篇博文《
Java之美[從菜鳥到高手演練]之Arrays類及其方法分析
》
九、其他排序
下面的一段轉載自博友@清蒸水皮 --- 補充於2015年1月14日
==============================================
計數排序
當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0li style="line-height:18px;"> }
a[j + gap] = temp;
}
gap = (gap - 1) / 3;
}
}
static void print(int data[]) {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] + " ");
}
}
public static void main(String[] args) {
int data[] = { 2, 68, 7, 19, 1, 28, 66, 200 };
print(data);
System.out.println();
shellsort(data, data.length);
print(data);
}
}
1. 選取兩個中軸P1, P2。
2. 假設P1<P2,否則交換。
3. 過程中原數組會分爲四個部分:小於中軸1,大於中軸2,介於兩個中軸之間,未排序部分(剛開始除了兩個中軸,其它元素都屬於這部分)。
4. 開始後,從未排序部分選取一個數,和兩個中軸作比較,然後放到合適的位置,一直到未排序部分無數據,結束一趟排序。
5. 遞歸地處理子數組,穩定排序,時間複雜度穩定爲O(nlogn)。
由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。
1. 選取兩個中軸P1, P2。
2. 假設P1<P2,否則交換。
3. 過程中原數組會分爲四個部分:小於中軸1,大於中軸2,介於兩個中軸之間,未排序部分(剛開始除了兩個中軸,其它元素都屬於這部分)。
4. 開始後,從未排序部分選取一個數,和兩個中軸作比較,然後放到合適的位置,一直到未排序部分無數據,結束一趟排序。
5. 遞歸地處理子數組,穩定排序,時間複雜度穩定爲O(nlogn)。
由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。