對於排序算法一般考慮:java
是否穩定(相同值的兩個數相對位置在修改先後是否會變) 和 時間複雜度(算法執行次數的規模量級)。至於說空間複雜度(算法在運行過程當中臨時佔用存儲空間大小的量度)對於每種算法具體實現代碼迥異。git
通常而言:
穩定的排序算法有:冒泡排序、插入排序、歸併排序和基數排序;
不穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。
我的感受在具體實現時,是否穩定取決於在處理相同值的兩個不一樣數據下標是否swap。好比說:算法
等等諸如此類在實現的處理細節決定是否能夠作成穩定, 可是一樣的帶來執行的次數會多。 而對於實際上只須要知道排序後的結果數組,固然執行規模越少,效率越快 越好嘍。數組
快排就沒辦法達到穩定了, 由於是隊首與隊尾雙邊查找,而後置換。 相同的值 先查到就先置換,後查到就更靠近中間位置。app
冒泡、選擇、插入: ide
平方階(O(N2))排序ui
通過實際代碼能夠發現,這些都是在從前日後的輪詢數組,只是邊界在慢慢變小,邊界每變化一次就輪詢一次。 (冒泡與選擇相同都在於輪詢數組比較值取大小;不一樣在於冒泡是每次比較後都swap,而選擇只是記錄須要的數據下標,最後纔將下標與隊尾swap)spa
快速、堆和歸併: 指針
線性對數階(O(n*log2n))排序日誌
通過實際代碼能夠發現, 都相似於將排序數組切分紅2部分遞歸處理。
基數: 線性階(O(n))排序
我的感受須要控制好輪詢邊界和等值時如何處理問題。能夠相應提升計算效率。
相鄰的兩個值比較,將最大/小的值逐步換至隊尾/首。
須要注意的是:
public class BubbleSort extends AbstractSort { @Override public int[] sort(int[] param) { int count = 0; //已經排序過的次數 int max_count = param.length - 1;//最大的排序次數 while (count <= max_count) { boolean is_swap = false; // 斷定本輪是否swap了,沒有則說明數組已經有序, break for (int i = 1; i <= max_count - count; i++) { // 控制好邊界 if (param[i - 1] > param[i]) { swap(param, i, i - 1); is_swap = true; } } count++; if (!is_swap) { break; } } System.out.println(String.format("總量:%s, 輪詢次數: %s", param.length, count)); return param; } public static void main(String[] args) { int[] param = new BubbleSort().sort(new int[] { 1, 2, 3, 5, 8, 6, 4, 9, 7 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
選中最大/小值置於已經排序隊列的隊尾,在未排序隊列中遞推執行。
與冒泡的異同在於: 都是輪詢比較值的大小,只是冒泡是每次比對後均可能作swap,而選擇是記錄下下標,與本次輪詢數組的邊界的隊首/隊尾一次swap,固然若是下標就是隊首/隊尾就不用swap。
public class SelectionSort extends AbstractSort { @Override public int[] sort(int[] param) { for (int i = 0; i < param.length; i++) { // 此處因是升序排序,因此i的值就是邊界 int min_index = getMinIndex(param, i); swap(param, i, min_index); } return param; } /** * 在指定起始位置以後隊列中查找最小值的下標 * * @param param * @param start * @return */ private int getMinIndex(int[] param, int start) { int min_index = start; for (int j = start + 1; j < param.length; j++) { if (param[j] < param[min_index]) { min_index = j; } } return min_index; } }
將待排序序列第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列。從頭至尾依次掃描未排序序列,將掃描到的當前元素依次與已排序序列從後往前遞推比較,若是當前元素小,則swap, 直到邊界。
/** * 穩定 插入排序 1)將待排序序列第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列。 * <p/> * 2)從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置。(若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面 * 。) Created by bear on 2016/2/29. */ public class InsertionSort extends AbstractSort { /** * 假定前面都是已從小至大排序的隊列:若是當前下標的值小於前一下標的值,交換位置,並在已排序隊列中從後向前遞推再判斷; * * @param param * @return */ public int[] sort(int[] param) { for (int i = 1; i < param.length; i++) { for (int j = i - 1; j >= 0; j--) { if (param[j] > param[j + 1]) { swap(param, j, j + 1); } else { break; // 若是後面的要不小於,則break; 邊界 } } } return param; } public static void main(String[] args) { int[] param = new InsertionSort().sort(new int[] { 4, 5, 3, 5, 8, 6, 4, 9, 7 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
插入排序在對幾乎已經排好序的數據操做時,便可以達到線性排序的高效率
希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。
按步長分組並插入排序,遞減步長至0。步長的起始值能夠按需定義
package com.noob.sort; /** * 不穩定(由於每次的排序隊列都是在元隊列基礎上按步長從新分組後的,致使相同值的數位置發生變化。) * 按步長分組並插入排序,遞減步長至0 * 希爾排序是基於插入排序的如下兩點性質而提出改進方法的: * <p/> * 插入排序在對幾乎已經排好序的數據操做時, 效率高, 便可以達到線性排序的效率 * 但插入排序通常來講是低效的, 由於插入排序每次只能將數據移動一位 * 希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。 * <p/> * 算法步驟: * <p/> * 1)選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1; * <p/> * 2)按增量序列個數k,對序列進行k 趟排序; * <p/> * 3)每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。 * Created by bear on 2016/2/29. */ public class ShellSort extends AbstractSort { public int[] sort(int[] param) { int length = param.length; for (int gap = length / 2; gap > 0; gap /= 2) {//步長遞減至1,核心思想與插入排序一致 for (int i = 0; i <= gap; i++) {//按步長分組[0, gap, 2gap, 3gap],[1, 1 + gap, 1 + 2gap, 3 + 3gap]... for (int m = i; m < length - gap; m += gap) {//在每個分組中進行排序,從第二個值m開始向前遞推比較。比較完後m++ for (int j = m + gap; j >= i + gap; j -= gap) { if (param[j - gap] > param[j]) {//最小座標爲分組起始座標,最大座標不大於原數組最大座標 swap(param, j, j - gap); } } } } } return param; } public static void main(String[] args) { int[] param = new ShellSort().sort(new int[] { 12, 3, 2, 3, 5, 8, 19, 6, 4, 9, 7, 0 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
選擇一個基準元素,一般選擇第一個元素或者最後一個元素, 經過一趟掃描,將待排序列分紅兩部分, 前比基準元素小,後大於等於基準元素, 此時基準元素在其排好序後的正確位置, 而後再用一樣的方法遞歸地排序劃分的兩部分。
實現的細節:
以第一個元素做爲基準數。(細節在於:前部分大於等於基準數的都置換到後半部分,最後留基準數與重合指針位置比對肯定好邊界)
package com.noob.sort; /** * 不穩定(判斷時,若相等,則出現不穩定情況) 快速排序:關鍵值排序 * (1)基本思想:選擇一個基準元素,一般選擇第一個元素或者最後一個元素,經過一趟掃描,將待排序列分紅兩部分,一部分比基準元素小,一部分大於等於基準元素, * 此時基準元素在其排好序後的正確位置,而後再用一樣的方法遞歸地排序劃分的兩部分。 * <p> * Created by bear on 2016/3/2. */ public class QuickSort extends AbstractSort { @Override public int[] sort(int[] param) { return sort_core(param, 0, param.length - 1); } /** * 左邊 < 關鍵值 <= 右邊 * * @param param * @param start 開始下標 * @param end 結束下標 * @return */ private int[] sort_core(int[] param, int start, int end) { int radix = param[start]; //選開始位置爲基準數 int low = start + 1; // 隊首指針開始位置爲除基準數後下標 int high = end; // 隊尾指針開始位置 while (low < high) {// 確保兩個指針不交叉!! 快排思想:後面部分是大於等於基準數的,因此 隊尾指針的數值相等也左移,隊首則不移動 /** * 跳出循環時的狀態: param[high] < radix || low == high */ while (param[high] >= radix && low < high) { //隊尾指針的值 >= 基準值 & 隊首指針下標 < 隊尾首指針下標 high--; //隊尾指針前移 } /** * 跳出循環時的狀態: param[low] >= radix || low == high */ while (param[low] < radix && low < high) { //隊首指針的值 < 基準值 & 隊首指針下標 < 於隊尾首指針下標 low++; //隊首指針後移 } if (low >= high) { // low 必定 <= high if (low > high) { System.out.println(String.format("error test: low %s > high %s : %s", low, high, low > high)); } break; } else { if (param[low] == param[high]) { //必定是param[low] > param[high] System.out.println("error1"); break; } swap(param, low, high); // 換好以後 param[high] 必定 > param[low] } } if (low == high) { // 基準數大, 則邊界就是這個low; 若是基準數小, 則邊界是low-1. 並swap. Integer limit = radix > param[low] ? low : low - 1; swap(param, start, limit); //分割成左右2部分 if (limit - 1 > start) { sort_core(param, start, limit - 1); //左部分 } if (limit + 1 < end) { sort_core(param, limit + 1, end); // 右部分 } } else { System.out.println(String.format("error test: low %s != high %s ", low, high)); } //System.out.println(toString(param)); return param; } public static void main(String[] args) { int[] param = new int[] { 5, 4, 3, 2, 1, 0, 2, 5, 6, 4, 9, 5 }; System.out.println(toString(new QuickSort().sort(param))); } }
運行結果:
沒有打印出任何 low > high 的日誌;證實了: low和high嚴格控制在 low<=high的狀況!!
歸併排序: 採用分治法,遞歸將數組分割成兩部分紅兩個已排序隊列,再合併至新的數組中。
兩個數組都有一個從隊首開始掃描的指針,依次對領一個數組指針值比較,小的先寫入新的store數組中,指針後移:++,再循環比較。
package com.noob.sort; /** * 不穩定(由於每次的排序隊列都是在元隊列基礎上按步長從新分組後的,致使相同值的數位置發生變化。) 按步長分組並插入排序,遞減步長至0 * 希爾排序是基於插入排序的如下兩點性質而提出改進方法的: * <p/> * 插入排序在對幾乎已經排好序的數據操做時, 效率高, 便可以達到線性排序的效率 但插入排序通常來講是低效的, 由於插入排序每次只能將數據移動一位 * 希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。 * <p/> * 算法步驟: * <p/> * 1)選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1; * <p/> * 2)按增量序列個數k,對序列進行k 趟排序; * <p/> * 3)每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 * 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。 Created by bear on 2016/2/29. */ public class ShellSort extends AbstractSort { public int[] sort(int[] param) { int length = param.length; if (length > 1) { int split = length / 2; int[] low = splitArray(param, 0, split - 1); int high[] = splitArray(param, split, param.length - 1); //切割數組後排序 param = sort_core(sort(low), sort(high)); } return param; } /** * 兩個已從小至大排序的數組合並 * * @param low * @param hight * @return */ private int[] sort_core(int[] low, int[] hight) { int low_length = low.length; int high_length = hight.length; int[] result = new int[low_length + high_length]; int i = 0, j = 0, m = 0; while (i < low_length || j < high_length) {//只有當兩個數組都取完才退出循環 if (i < low_length && (j >= high_length || low[i] < hight[j])) { //當low還沒被取完時,若是high已經被取完或者low_value < hight_value時,將low_value置於新數組中,下標+1 result[m++] = low[i]; i++; } else { result[m++] = hight[j]; j++; } } return result; } /** * 切分數組 * * @param param * @param start 開始下標 * @param end 結束下標 * @return */ private int[] splitArray(int[] param, int start, int end) { int[] result = new int[end - start + 1]; for (int i = start, m = 0; i <= end; i++, m++) { result[m] = param[i]; } return result; } public static void main(String[] args) { int[] param = new ShellSort().sort(new int[] { 12, 3, 2, 3, 5, 8, 19, 6, 4, 9, 7, 0 }); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
先了解三個概念:
堆排序利用的是完滿二叉樹的特性。每一次的排序完成,都將剩下的數組值看成新的一顆數。
2*root + 1 與 2*root + 2 是root節點的2個子節點
package com.noob.sort; /** * 不穩定(由於相同的值最終有可能下標小的先置換到末尾的已排序隊列中,可在比較父子節點大小的判斷中增長若相等也置換,那應該能達到穩定排序的要求, * 但多了不少不必的置換) 二叉堆 徹底二叉樹只是在最後一層要麼滿子節點,要麼都是倒數第二層左邊的節點開始滿起來。 * <p/> * Created by bear on 2016/3/3. */ public class HeapSort extends AbstractSort { /** * 不管是構建大根堆仍是小根堆,循環的結束是指針指向了元數組的0下標位置。由於param[0]纔是整個樹的root。 2*root + 1 與 * 2*root + 2 都沒辦法在沒有新數組的狀況下成爲整個樹的root。 * 若在構建小根堆時,start--,雖然本父子節點組的順序是對的。可是可能會致使父節點同級的兄弟節點間的值不按正序排列 * 因此,有必要將最大值或者最小值置換到最後一個end_index 上。 沒有葉子節點是徹底能夠的,根節點不變。 * * @param param * @return */ public int[] sort(int[] param) { sort_max_heap(param); return param; } /** * 從小到大排序 構建大根堆。獲取最大數組下標在二叉樹中的父節點。 * 從這個父節點開始直至樹根節點,比較是否符合父節點要大於等於任意子節點的值,不然將最大值替換置父節點上 最終能使得最大值必定是在樹根節點。 * * @param param */ private void sort_max_heap(int[] param) { for (int i = 0; i < param.length; i++) { int end_index = param.length - 1 - i; //未排序隊列最大下標 if (end_index > 0) { createMaxHeap(param, end_index); // 每次循環後都是將當前未排序隊列中最大值替換到最後。max_index 每次都少1 swap(param, 0, end_index); } } } /** * @param param * @param lastIndex 最後一個值的下標 */ private void createMaxHeap(int[] param, int lastIndex) { for (int i = (lastIndex - 1) / 2; i >= 0; i--) {//獲取根節點的下標 int root = i; int bigger_index = 2 * root + 1;//左右子節點中值最大的下標 if (bigger_index < lastIndex) { // 若左節點的下標小於最大的座標,說明有右節點 if (param[bigger_index] < param[bigger_index + 1]) { bigger_index++; } } if (param[root] < param[bigger_index]) { swap(param, root, bigger_index); } } } /** * 從大到小排序。 構建最小堆,從最後一組父子節點開始向樹root節點遞推,將最小值置於每組父子節點的父節點上,這樣最小值必定會在樹root節點上 * * @param param */ private void sort_min_heap(int[] param) { for (int i = 0; i < param.length; i++) { int end_index = param.length - 1 - i; //未排序隊列最大下標 if (end_index > 0) { createMinHeap(param, 0, end_index); // 每次循環後都是將當前未排序隊列中最小值替換到最後。end_index 每次都少1 swap(param, 0, end_index); } } } /** * 不必定有子節點,單必定有根節點。 一、判斷是否有右節點; 二、獲取最小子節點下標;三、最小子節點值是否比根父節點值小,若小則交換 * * @param param * @param start * @param end 每次的end */ private void createMinHeap(int[] param, int start, int end) { for (int root = (end - 1) / 2; root >= start; root--) { int min_index = 2 * root + 1; // 左節點 if (min_index < end) { // 判斷是否有右節點 if (param[min_index] > param[min_index + 1]) { min_index++; } } if (param[min_index] < param[root]) { swap(param, min_index, root); } } } public static void main(String[] args) { int[] param = new int[] { 5, 7, 3, 9, 2, 6, 1 }; new HeapSort().sort(param); StringBuilder str = new StringBuilder(); for (int i = 0; i < param.length; i++) { str.append(param[i]); if (i != param.length - 1) { str.append(" "); } } System.out.println(str.toString()); } }
定義的radix與max_position都與數組的最大值有關係; 桶的數量是10, 由於阿拉伯數是0-9。
從個位開始, 扔進桶內肯定好位置,再交由下一位的桶排序。
package com.noob.sort; /** * 按個位、十位、百位...排序 -----> 當前版本對負值無效 * <p/> * 穩定排序 基數排序 Created by bear on 2016/3/6. */ public class RadixSort extends AbstractSort { private int[] radix = new int[] { 1, 1, 10, 100, 1000 }; // 與數組的最大值的量級有關 private int max_index = 10; //桶的數量 由於阿拉伯數是0-9 private int max_position = 3; //數組最大值的位數 public int[] sort(int[] param) { for (int position = 0; position < max_position; position++) { sort_core(param, position); } return param; } /** * 按指定位上的數值排序 * * @param param * @param end_index * @param position */ private void sort_core(int[] param, int position) { int cap = param.length - 1; int[] count = new int[max_index];//記錄每一個桶統計個數 int[] store = new int[cap + 1]; // 桶的容量 // 置空各個桶的數據統計 for (int i = 0; i < max_index; i++) { count[i] = 0; } // 指定位的值與桶的編號一一對應。統計對應桶有多少數量 for (int i = 0; i <= cap; i++) { count[getDigit(param[i], position)]++; } for (int i = 1; i < max_index; i++) { count[i] = count[i] + count[i - 1]; // 經過累加能夠肯定每一個指定位的值在store桶上的邊界。 } // 這裏要從右向左掃描,保證排序穩定性 for (int i = cap; i >= 0; i--) { int digit = getDigit(param[i], position); store[count[digit] - 1] = param[i]; //放入對應的桶中,count[j]-1是第j個桶的右邊界索引 count[digit]--; // 對應桶的裝入數據索引減一 } // 將已分配好的桶中數據再倒出來,此時已經是對應當前位數有序的表 for (int i = 0, j = 0; i <= cap; i++, j++) { param[i] = store[j]; } } /** * 獲取指定位上的數值 */ private int getDigit(int param, int digit) { return (param / radix[digit]) % 10; } }