排序是一個經典的問題,它以必定的順序對一個數組或列表中的元素進行從新排序。而排序算法也是各有千秋,每一個都有自身的優勢和侷限性。雖然這些算法日常根本就不用本身去編寫,但做爲一個有追求的程序員,仍是要了解它們從不一樣角度解決排序問題的思想。前端
學習算法是枯燥的,那怎麼高效的理解它的原理呢?顯然,若是以動圖的方式,生動形象的把算法排序的過程展現出來,很是有助於學習。visualgo.net 就是一個可視化算法的網站,第一次訪問的時候,真的是眼前一亮。本文就對經常使用的排序進行下總結。java
冒泡排序的基本思想就是:把小的元素往前調或者把大的元素日後調。假設數組有 N 個元素,冒泡排序過程以下:程序員
冒泡排序的核心代碼:算法
public static void bubbleSort(int[] a, int n) { // 排序趟數,最後一個元素不用比較因此是 (n-1) 趟 for (int i = 0; i < n - 1; i++) { // 每趟比較的次數,第 i 趟比較 (n-1-i) 次 for (int j = 0; j < n - 1 - i; j++) { // 比較相鄰元素,若逆序則交換 if (a[j] > a[j+1]) { int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } }
難點在於邊界的肯定,算法分析:shell
選擇排序的基本思想就是:每次從未排序的列表中找到最小(大)的元素,放到已排序序列的末尾,直到全部元素排序完畢。假設數組有 N 個元素且 L=0,選擇排序過程以下:數組
選擇排序的核心代碼:數據結構
public static void selectionSort(int[] a, int n) { // 排序趟數,最後一個元素是最大的不用比較因此是 (n-1) 趟 for (int i = 0; i < n-1; i++) { int minIndex = i; // 無序列表中最小元素的下標 for (int j = i+1; j < n; j++) { // 在無序列表中查找最小元素的小標並記錄 if (a[j] < a[minIndex]) { minIndex = j; } }// 將最小元素交換到本次循環的前端 int tmp = a[minIndex]; a[minIndex] = a[i]; a[i] = tmp; } }
算法分析:源碼分析
插入排序的基本思想是:每次將待插入的元素,按照大小插入到前面已排序序列的適當位置上。插入排序過程以下:學習
插入排序的核心代碼:優化
public static void insertionSort(int[] a, int n) { // a[0] 看作已排序 for (int i = 1; i < n; i++) { int x = a[i]; // 待插入元素 int j=i-1; // 插入的位置 while (j >= 0 && a[j] > x) { a[j+1] = a[j]; // 爲待插入元素騰地 j--; } a[j+1] = x; // 插入到下一個位置 j+1 } }
算法分析:
希爾排序也稱爲增量遞減排序,是對直接插入算法的改進,基於如下兩點性質:
希爾排序的改進是,使用一個增量將數組切分不一樣的分組,而後在組內進行插入排序,遞減增量,直到增量爲 1,好處就是數據能跨多個元素移動,一次比較就可能消除多個元素的交換。基本過程以下:
x/2
或者x/3+1
核心代碼:
public static void shellSort(int[] a, int n) { // 計算遞增序列,3x+1 : 1, 4, 13, 40, 121, 364, 1093, ... int h = 1; while (h < n/3) h = 3*h + 1; while (h >= 1) {// 直到間隔爲 1 // 按間隔 h 切分數組 for (int i = h; i < n; i++) { // 對 a[i], a[i-h], a[i-2*h], a[i-3*h]...使用插入排序 int x = a[i]; // 待插入元素 int j=i; while (j >=h && x < a[j-h]) { a[j] = a[j-h];// 爲待插入元素騰地 j -= h; } a[j] = x; // 插入 x } // 遞減增量 h /= 3; } }
希爾排序數組拆分插入圖解,上面的動圖能夠輔助理解,與下圖數據不一致:
算法分析:
歸併排序是分而治之的排序算法,基本思想是:將待排序序列拆分多個子序列,先使每一個子序列有序,再使子序列間有序,最終獲得完整的有序序列。歸併排序本質就是不斷合併兩個有序數組的過程,實現時主要分爲兩個過程:
二路歸併遞歸實現,核心代碼:
public static void mergeSort(int[] a, int low, int high) { // 要排序的數組 a[low..high] if (low < high) {// 是否還能再二分 low >= high (0 或 1 個元素) int mid = low + (high - low) / 2; // 取中間值,避免 int 溢出 mergeSort(a, low, mid); // 將左半邊排序 mergeSort(a, mid + 1, high); // 將右半邊排序 merge(a, low, mid, high); // 歸併左右兩邊 } } public static void merge(int[] a, int low, int mid, int high) { int n = high - low + 1; // 合併後元素總數 int[] b = new int[n]; // 臨時合併數組 int left = low, // 左邊有序序列起始下標 right = mid + 1, // 右邊有序序列起始下標 bIdx = 0; // 按升序歸併到新數組 b 中 while (left <= mid && right <= high) { b[bIdx++] = (a[left] <= a[right]) ? a[left++] : a[right++]; } // 右邊序列已拷貝完畢,左邊還有剩餘,將其依次拷貝到合併數組中 while (left <= mid) { b[bIdx++] = a[left++]; } // 左邊序列已拷貝完畢,右邊還有剩餘,將其依次拷貝到合併數組中 while (right <= high) { b[bIdx++] = a[right++]; } // 將歸併後的數組元素拷貝到原數組適當位置 for (int k = 0; k < n; k++) { a[low + k] = b[k]; } }
數組拆分和方法調用的動態狀況以下圖(右鍵查看大圖):
遞歸的本質就是壓棧,對於 Java 來講,調用層次太深有可能形成棧溢出。通常的,遞歸都能轉爲迭代實現,有時迭代也是對算法的優化。
歸併排序中的遞歸主要是拆分數組,因此,非遞歸的重點就是把這部分改爲迭代,它們的終止條件不一樣:
核心代碼:
public static void unRecursiveMergeSort(int[] a, int n) { int low = 0, high = 0, mid = 0; // 待歸併數組長度,1 2 4 8 ... int len = 1; // 從最小分割單位 1 開始 while(len <= n) { // 按分割單位遍歷數組併合並 for (int i = 0; i + len <= n; i += len * 2) { low = i; // mid 變量主要是在合併時找到右半邊數組的起始下標 mid = i + len - 1; high = i + 2 * len - 1; // 防止超過數組長度 if (high > n - 1) { high = n - 1; } // 歸併兩個有序的子數組 merge(a, low, mid, high); } len *= 2; // 增長切分單位 } }
算法分析:
快排能夠說是應用最普遍的算法了,它的特色是使用很小的輔助棧原地排序。它也是一個分而治之的排序算法,基本思想是:選取一個關鍵值,將數組分紅兩部分,一部分小於關鍵值,一部分大於關鍵值,而後遞歸的對左右兩部分排序。過程以下:
遞歸實現快排核心代碼:
public static void quickSort(int[] a, int low, int high) { if (low < high) { int m = partition(a, low, high); // 切分 quickSort(a, low, m-1); // 將左半部分排序 quickSort(a, m+1, high); // 將右半部分排序 } } public static int partition(int[] a, int low, int high) { // 將數組切分爲 a[low..i-1], a[i], a[i+1..high] int p = a[low]; // 切分元素 int i = low; // 下一個小於切分元素可插入的位置 // 從切分元素下一個位置開始,遍歷整個數組,進行分區 for (int j = low + 1; j <= high; j++) { // 往前移動比切分元素小的元素 if (a[j] < p && (i++ != j)) { int tmp = a[j]; a[j] = a[i]; a[i] = tmp; } } // 交換中樞(切分)元素 int tmp = a[low]; a[low] = a[i]; a[i] = tmp; return i; }
算法分析:
堆排序是利用堆這種數據結構設計的算法。堆可看做一個徹底二叉樹,它按層級在數組中存儲,數組下標爲 k 的節點的父子節點位置分別以下:
堆的表示以下:
堆有序的定義是每一個節點都大於等於它的兩個子節點,因此根節點是有序二叉堆中值最大的節點。堆排序就是一個不斷移除根節點,使用數組剩餘元素從新構建堆的過程,和選擇排序有點相似(只不過按降序取元素),構建堆有序數組基本步驟以下:
核心代碼:
public static void heapSort(int[] a) { int n = a.length - 1; // 構建堆,一開始可將數組看做無序的堆 // 將從下標爲 n/2 開始到 0 的元素下沉到合適的位置 // 由於 n/2 後面的元素都是葉子結點,無需下沉 for (int k = n/2; k >= 0; k--) sink(a, k, n); // 下沉排序 // 堆的根結點永遠是最大值,因此只需將最大值和最後一位的元素交換便可 // 而後再維護一個除原最大結點之外的 n-1 的堆,再將新堆的根節點放在倒數第二的位置,如此反覆 while (n > 0) { // 將 a[1] 與最大的元素 a[n] 交換,並修復堆 int tmp = a[0]; a[0] = a[n]; a[n] = tmp; // 堆大小減1 n--; // 下沉排序,從新構建 sink(a, 0, n); } } /** 遞歸的構造大根堆 */ private static void sink(int[] a, int k, int n) { // 是否存在左孩子節點 while ((2*k+1) <= n) { // 左孩子下標 int left = 2*k+1; // left < n 說明存在右孩子,判斷將根節點下沉到左仍是右 // 若是左孩子小於右孩子,那麼下沉爲右子樹的根,而且下次從右子樹開始判斷是否還要下沉 if (left < n && a[left] < a[left + 1]) left = left + 1; // 若是根節點不小於它的子節點,表示這個子樹根節點最大 if (a[k] >= a[left]) break; // 不用下沉,跳出 // 不然將根節點下沉爲它的左子樹或右子樹的根,也就是將較大的值上調 int tmp = a[k]; a[k] = a[left]; a[left] = tmp; // 繼續從左子樹或右子樹開始,判斷根節點是否還要下沉 k = left; } }
算法分析:
每次看了,過段時間就忘,此次總結起來,方便查看,也分享給你們,有問題歡迎交流。首發於公衆號「頓悟源碼」,搜索獲取更多源碼分析和造的輪子。