寫在前面:紙上得來終覺淺。基本排序算法的思想,可能不少人都說的頭頭是到,但能說和能寫出來,真的仍是有很大區別的。面試
今天整理了一下各類經常使用排序算法,固然還不全,後面會繼續補充。代碼中可能有累贅或錯誤的地方,歡迎指正。算法
冒泡排序是最簡單的排序算法之一,其具體思想就是將相鄰兩個元素進行比較,大的元素交換到最後面(升序),最大的元素移動的過程就像水冒泡同樣。冒泡排序中,須要對n個元素進行冒泡,每次冒泡又須要進行n的數量級次比較,因此冒泡排序的時間複雜度爲O(n^2)shell
/** * 冒泡排序 */ public void bubbleSort(int[] array) { if(array == null || array.length <= 0) { return ; } for(int i = 0; i < array.length - 1; i++) { for(int j = 0; j < array.length - i - 1; j++) { if(array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } }
固然,比較理想的狀態下,咱們想,若是在待排序數組已是有序的狀況下,咱們冒泡一次,發現交換次數爲零,那麼後面的比較與交換就沒必要再進行,因此,這種狀況下,冒泡排序的最好狀況時間複雜度是O(n),咱們所須要作的就是在每次冒泡過程當中添加一個記錄交換次數的計數器。 以下:數組
/** * 冒泡排序2 */ public void bubbleSort2(int[] array) { if(array == null || array.length <= 0) { return ; } int swapCount = 0; //天加計數器記錄每趟交換次數 for(int i = 0; i < array.length - 1; i++) { swapCount = 0; //清零 for(int j = 0; j < array.length - i - 1; j++) { if(array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; swapCount++; } } if(swapCount == 0) { //這一趟冒泡過程沒有發生交換,待排數組已經有序,直接退出循環 break; } } }
選擇排序集本思想和冒牌排序類似,都是經過一次遍歷比較後獲得最值。與冒泡排序不一樣的是,冒泡排序須要相鄰元素每次比較而後交換。而選擇排序是通過一次遍歷比較記錄下最值,也就是通過總體選擇,而後將選出的元素放在合適的位置。選擇排序對冒泡排序進行了必定的優化,比較的次數沒有發生變化,但省去了相鄰元素頻繁的交換。在時間複雜度上,依然是O(n^2);函數
public void selectSort(int[] array) { if(array == null || array.length <= 0) { return; } for(int i = 0; i < array.length - 1; i++) { int minIndex = i; int minNum = array[i]; for(int j = i; j < array.length; j++) { if(array[j] < minNum) { minNum = array[j]; minIndex = j; //記錄最小值的下標 } } if(minIndex != i) { //若最小值不是在i處,將最小值交換到前面 int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } } }
插入排序與冒泡,選擇排序不一樣。冒泡與選擇都是經過一次遍歷比較肯定出一個最值,而後放在合適的位置。而插入排序是經過比較來找到合適的位置進行插入,例如待排數組:3,2,6,4,8;第一個數3,已經有序,而後是2,與3比較,發現比3小,插到3前面:2,3,6,4,8,而後是6,6比3大,插到3後面,2,3,6,4,8,而後是4,4比6小而且比3大,則插到3後面6前面:2,3,4,6,8,最後是8,比6大,插到6的後面,完成插入排序。冒泡排序和選擇排序在進行一次排序後,就能惟一肯定一個元素的位置,而插入排序卻不行。最好狀況,若待排序列已經有序,則插入排序的時間複雜度爲O(n)。插入排序的平均時間複雜度依然是O(n^2)。學習
/** *插入排序 */ public void insertSort(int[] array) { if(array == null || array.length <= 0) { return; } for(int i = 0; i < array.length; i++) { int temp = array[i]; int j = i; while(j > 0 && temp < array[j - 1]) { //向後移動 array[j] = array[j - 1]; j--; } array[j] = temp; } }
我這裏講希爾排序放在直接插入排序算法後面,是由於希爾排序是插入排序的一種高效實現方法。希爾排序將整個待排序列經過劃分紅若干子序列來分別進行插入,分割子序列的方法是經過一個增量來達到。在插入排序中,若是待排序數組是有序的,那麼,插入排序的只須要遍歷一次數組,不用移動任何元素,就能完成排序,且時間複雜度爲O(n)。因此利用插入排序,若數組是基本有序的,那麼直接插入排序效率將會提升。希爾排序就是利用這個特色。希爾排序因爲前面的插入排序中記錄的關鍵字是和同一子序列中的前一個記錄的關鍵字進行比較,所以關鍵字較小的記錄就不是一步一步地向前挪動,而是跳躍式地往前移,從而使得進行最後一趟排序時,增量爲1,至關於直接插入排序,可是這個時候,數組已經基本有序,只要做記錄的少許比較和移動便可。所以希爾排序的效率要比直接插入排序高。希爾排序的時間複雜度取決於初始增量的選取,致使其時間複雜度很難計算,合理的選取n,時間複雜度能夠達到O(n^1.3)大數據
/** * 一次增量爲n的插入排序,能夠對比直接插入排序 * @param array 待排序數組 * @param n 增量 */ public void oneShellSort(int[] array,int n) { if(array == null || array.length <= 0 || n <1) { return; } for(int i = n; i < array.length; i++) { int temp = array[i]; int j = i; while(j >= n && temp < array[j - n]) { array[j] = array[j - n]; j = j - n; } array[j] = temp; } } /** * 希爾排序,n遞減 * @param array待排數組 * @param n 初始增量 */ public void shellSort(int[] array,int n) { if(array == null || array.length <= 0 || n <1) { return; } for(int i = n; i >=1; i--) { oneShellSort(array,i); //i等於1的時候至關於直接插入排序,不過這時已經基本有序了,因此快 } }
快速排序多是排序中經典排序了,咱們平時常常會看到快速排序的字眼,無論是你剛學習的考試中, 仍是面試題中。那麼,快速排序究竟是什麼樣?一種思想實際上是這樣,快速排序是取一個基數做爲對比基準,在把大的數向後移的同時將比較小的數向前移。快速排序通過一次排序後就能惟一肯定一個元素的位置,這個元素將待排序序列分爲兩個部分,一部分全部元素小於等於該數,另外一部分全部元素大於等於該數。這兩部分的元素又能夠遞歸進行快速排序。快速排序不是一種穩定的排序,時間複雜度和待排序列的初始狀況有很大關係。最壞狀況下(待排序數組已經有序),時間複雜度爲O(n^2);平均時間複雜度爲O(nlog2n)。優化
快速排序分爲兩個步驟,一是要實現將待排序數組按照基數分爲兩部分,二是要遞歸實現排序。ui
/** * 快速排序每次獲得一個肯定的位置,再根據這個位置將數組分爲兩部分 * @param array 待排序數組 * @param left 左邊界 * @param right 右邊界 * @return 肯定位置的元素的下標 */ public int getMiddleIndex(int[] array,int left,int right) { if(array == null || array.length <= 0) { return 0; } if(left >= right) { return 0; } int pivotNum = array[left]; //選取最左邊的值爲基準值 while(left < right) { while(left < right && array[right] >= pivotNum) { //從後開始找到小於基準值的數移到前面 right--; } array[left] = array[right]; while(left < right && array[left] <= pivotNum) { //從前開始找到大於基準值的數移到後面 left++; } array[right] = array[left]; } array[left] = pivotNum;//基準值的肯定位置 return left; } /** * 快速排序 */ public void quickSort(int[] array, int left, int right) { if(array == null || array.length <= 0 || left >= right) { return; } int minIndex = getMiddleIndex(array,left,right); //將待排序數組分爲兩部分 quickSort(array,left, minIndex - 1);//左邊遞歸 quickSort(array,minIndex + 1,right);//右邊遞歸 }
堆排序實質上是藉助堆這一結構來實現排序,堆實際上是一種徹底二叉樹結構,父親節值點大於子節點的堆稱爲大頂堆,父節點值小於子節點的值稱爲小頂堆,很形象的名字。那麼,對於待排序數組,如何經過堆來進行排序呢? 這裏用大頂堆來舉例。知足大頂堆的結構,那麼堆的根確定就是最大值,假如咱們要按照升序排列,當咱們將最大值和最後一個值交換後,剩餘元素如何從新構建一個新的大頂堆?只要又能構建出一個大頂堆,就能夠將堆頂元素和倒數第二個元素進行交換,接下來就是迭代的過程了。因此,咱們須要
解決的問題就有兩個:
一、如何將給的待排序數組構建成一個知足條件的堆?
二、將堆頂元素和最後一個元素交換後,如何調整成一個新的知足條件的堆。
spa
堆排序實例過程,參考:http://blog.csdn.net/xiaoxiaoxuewen/article/details/7570621/
咱們能夠直接使用線性數組來表示一個堆,由初始的無序序列建成一個堆就須要自底向上從第一個非葉元素開始挨個調整成一個堆。比較當前堆頂元素的左右孩子節點,由於除了當前的堆頂元素,左右孩子堆均知足條件,這時須要選擇當前堆頂元素與左右孩子節點的較大者(大頂堆)交換,直至葉子節點。咱們稱這個自堆頂自葉子的調整成爲篩選。每次調整堆的時間爲logn,一共n個數,須要n次調整,因此堆排序的時間複雜度爲:O(nlogn)
/** * 調整一個節點,使自該節點之後知足堆的要求 * 通過對構建後,若一個節點有左右子樹,則其左右子樹已經知足堆的要求 * @param array * @param begin * @param end */ public void headAdjust(int[] array, int begin, int end) { if(array == null || array.length <= 0 || begin >= end || begin >= array.length) { return; } int temp = array[begin]; for(int i = 2 * begin + 1; i <= end; i = i * 2 + 1) { if(i < end && array[i] < array[i + 1]) { i++; } if(temp > array[i]) { break; } array[begin] = array[i]; begin = i; } array[begin] = temp; } /** * 利用大頂堆,實現數組升序排序 * @param array */ public void heapSort(int[] array) { if(array == null || array.length <= 0) { return; } //第一次構建堆,從第一個非葉子節點開始調整,一直到根節點(數組中第一個數) for(int i = array.length / 2; i >= 0; i--) { headAdjust(array,i,array.length - 1); } //每次交換堆頂元素到數組後面,前面剩餘數組以堆頂元素進行一次調整堆 for(int i = 1; i <= array.length - 1; i++) { int temp = array[0]; array[0] = array[array.length - i]; array[array.length - i] = temp; headAdjust(array,0,array.length - 1 - i); } }
歸併排序使用了遞歸分治的思想,其基本思想是,先遞歸劃分子問題,而後合併結果。把待排序列當作由兩個有序的子序列,而後合併兩個子序列,而後把子序列當作由兩個有序序列。倒着來看,其實就是先兩兩合併,而後四四合並。最終造成有序序列。歸併排序須要額外的空間來存儲合併時的中間數組,空間複雜度爲O(n),時間複雜度爲O(nlogn)
歸併排序過程:
/** * 歸併排序 * @param array * @param left * @param right */ public void mergeSort(int[] array, int left, int right) { if(array == null || array.length == 0 || right >= array.length ) { return ; } if(left >= right) { return; } int mid = (left + right) / 2; //分紅兩部分 mergeSort(array, left, mid); //左右遞歸 mergeSort(array, mid + 1, right); merge(array,mid,left,right);//在原數組上進行合併 } /** * 歸併排序的合併操做 * @param array * @param mid * @param left * @param right */ public void merge(int[] array, int mid, int left, int right) { if(array == null || array.length == 0) { return; } int[] temp = new int[right - left + 1]; int i = left; int j = mid + 1; int k = 0; while(i <= mid && j <= right) { if(array[i] < array[j]) { temp[k] = array[i]; i++; } else { temp[k] = array[j]; j++; } k++; } while(i <= mid) {//若左邊有剩餘 temp[k++] = array[i++]; } while(j <= right) {//若右邊有剩餘 temp[k++] = array[j++]; } k = 0; while(left <= right) {//有序數組複製到原數組相應位置 array[left++] = temp[k++]; } }
計數排序是利用空間換時間的一種排序方式,通常來講,基於比較的排序方式(前面的冒泡排序,選擇排序,直接插入排序,快速排序,歸併排序等)時間複雜度最低是O(n^2)。可是計數排序在利用較多空間後的時間複雜度能夠達到O(n);
計數排序基本思想:
將待排序列的數字做爲排序數組的下標,遍歷一次待排序列,排序數組統計每一個位置出現的次數。而後一次輸出便可。固然,由於是待排數據做爲數組下標,若待排序列存在負數,則須要找到最小的負數,全部數加上一個值轉換成正數,最後的輸出再轉換回去。
/** * 計數排序 * @param array */ public void countSort(int[] array) { if(array == null || array.length == 0) { return; } int maxNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大值 if(array[i] > maxNum) { maxNum = array[i]; } } int[] sortArray = new int[maxNum + 1]; //新建一個排序數組,加1 爲了讓最大值下標能放排序數組 for(int i = 0; i < array.length; i++) { //按照下標放入排序數組中 sortArray[array[i]] += 1; } int k = 0; for(int i = 0; i < sortArray.length; i++) { if(sortArray[i] == 0) { continue; } while(sortArray[i] > 0) { array[k++] = i; //sortArray[i]>0的狀況是存在相同的值 sortArray[i]--; } } }
這裏其實計數排序能夠有一個小的改進,就是,咱們其實不須要建立maxNum長的排序數組,而只須要建立(maxNum - minNum + 1)長的數組就足夠。
例如給定無序數組 { 2, 6, 3, 4, 5, 10, 9 },處理過程以下:
實現代碼以下:
/** * 計數排序小改進 * @param array */ public void countSort2(int[] array) { if(array == null || array.length == 0) { return; } int maxNum = array[0]; int minNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大和最小值 if(array[i] > maxNum) { maxNum = array[i]; } if(array[i] < minNum) { minNum = array[i]; } } //新建一個排序數組,最大值減去最小值加1,節省了必定的空間 int[] sortArray = new int[maxNum - minNum + 1]; for(int i = 0; i < array.length; i++) { //按照下標放入排序數組中,注意減去minNum sortArray[array[i] - minNum] += 1; } int k = 0; for(int i = 0; i < sortArray.length; i++) { if(sortArray[i] == 0) { continue; } while(sortArray[i] > 0) { array[k++] = i + minNum; //sortArray[i]>0的狀況是存在相同的值,注意加上minNum sortArray[i]--; } } }
上面說到計數排序的小改進,真的只是小改進,由於若是出現:3,4,100,10000。這樣的待排序序列,依然會消耗大量的空間。那麼有沒有更進一步的改進呢? 那就是桶排序啦。。
桶排序比較複雜,但核心思路來自計數排序,將待排序數組的maxNum - minNum按區間分紅n個桶,如下分析來自:http並按照必定的映射函數將待排序序列每一個值映射到相應的桶中,而後對每一個桶排序,最後一次輸出桶中的數據。如下分析來自:http://hxraid.iteye.com/blog/647759。
桶排序基本思想: 假設有一組長度爲N的待排關鍵字序列K[1....n]。首先將這個序列劃分紅M個的子區間(桶) 。而後基於某種映射函數 ,將待排序列的關鍵字k映射到第i個桶中(即桶數組B的下標 i) ,那麼該關鍵字k就做爲B[i]中的元素(每一個桶B[i]都是一組大小爲N/M的序列)。接着對每一個桶B[i]中的全部元素進行比較排序(可使用快排)。而後依次枚舉輸出B[0]....B[M]中的所有內容便是一個有序序列。
桶排序的映射:
bindex=f(key) 其中,bindex 爲桶數組B的下標(即第bindex個桶), k爲待排序列的關鍵字。桶排序之因此可以高效,其關鍵在於這個映射函數,它必須作到:若是關鍵字k1<k2,那麼f(k1)<=f(k2)。
也就是說B(i)中的最小數據都要大於B(i-1)中最大數據。很顯然,映射函數的肯定與數據自己的特色有很大的關係,咱們下面舉個例子:假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。這些數據所有在1—100之間。所以咱們定製10個桶,而後肯定映射函數f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將全部關鍵字所有堆入桶中,並在每一個非空的桶中進行快速排序後獲得以下圖所示:
/** * 桶排序 * @param array */ public void bucketSort(int[] array) { if(array == null || array.length <= 0) { return; } int maxNum = array[0]; int minNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大和最小值 if(array[i] > maxNum) { maxNum = array[i]; } if(array[i] < minNum) { minNum = array[i]; } } int bucketNum = (maxNum - minNum) / array.length; //bucketNum是桶的個數 ArrayList<List<Integer>> buckets = new ArrayList<>(); //桶的鏈表,每一個節點使一個桶 for(int i = 0; i <= bucketNum; i++) { buckets.add(new ArrayList<Integer>()); //初始化桶 } for(int i = 0; i < array.length; i++) { int num = (array[i] - minNum) / array.length; buckets.get(num).add(array[i]); } for(int i = 0; i <= bucketNum; i++) { Collections.sort(buckets.get(i)); //偷個懶,jdk1.8該方法採用二分插入法排序 } int k = 0; for(int i = 0; i <= bucketNum; i++) {//全部桶的元素順序輸出 for(int num: buckets.get(i)) { array[k++] = num; } } }
又到了分析各類算法時間複雜度,空間複雜度的時候了,萬能的表格出來!-------……&……&……>>>>>>>:
排序方法 | 最好時間複雜度 | 最壞時間複雜度 | 平均時間複雜度 | 須要的輔助存儲 | 算法的穩定性 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 穩定 |
選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 穩定 |
希爾排序 | O(n^1.3) | -- | -- | O(1) | 不穩定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |
計數排序 | O(n) | O(n) | O(n) | O(n) | |
桶排序 | 0(n) |
只有一個桶,取決 於桶內排序算法 |
O(N+C),其中C=N*(logN-logM) M是桶的個數 |
O(N+M) m爲桶的個數 |
|
另外,關於算法的穩定性:
假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj以前,而在排序後的序列中,ri仍在rj以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。
其中直接插入排序是比較簡單的,在序列基本有序或者n較小時,直接插入排序是好的方法,所以常將它和其餘的排序方法,如快速排序、歸併排序等結合在一塊兒使用。
參考連接:
http://www.cnblogs.com/wxisme/ 桶排序分析:http://hxraid.iteye.com/blog/647759
2017-03-14 17:25:07 Gonjan