現現在大學生學習排序算法,除了學習它的算法原理、代碼實現以外,做爲一個大學生更重要的每每是要學會如何評價、分析一個排序算法。排序對於任何一個程序員來講,可能都不會陌生。大部分編程語言中,也都提供了排序函數。在日常的項目中,咱們也常常會用到排序。排序很是重要!本章主要從如何分析一個算法開始入手,從而循進漸進的分析那些大學四年結束以前必須掌握的排序算法! @[toc]java
固然你能夠先思考一兩分鐘,帶着這個問題,咱們開始以下的內容!<font color=red>而且注意我標紅的字體,每每是起眼或者不起眼的重點。</font>程序員
對於排序算法執行效率的分析,咱們通常會從這三個方面來衡量:算法
咱們在分析排序算法的時間複雜度時,要分別給出最好狀況、最壞狀況、平均狀況下的時間複雜度。除此以外,你還要說出最好、最壞時間複雜度對應的要排序的原始數據是什麼樣的。shell
爲何要區分這三種時間複雜度呢?第一,有些排序算法會區分,爲了好對比,因此咱們最好都作一下區分。第二,對於要排序的數據,有的接近有序,有的徹底無序。有序度不一樣的數據,對於排序的執行時間確定是有影響的,咱們要知道排序算法在不一樣數據下的性能表現。編程
咱們知道,時間複雜度反應的是數據規模n很大的時候的一個增加趨勢,因此它表示的時候會忽略係數、常數、低階。可是實際的軟件開發中,咱們排序的多是10個、100個、1000個這樣規模很小的數據,因此,在對同一階時間複雜度的排序算法性能對比的時候,咱們就要把係數、常數、低階也考慮進來。api
這一節和下一節講的都是基於比較的排序算法。基於比較的排序算法的執行過程,會涉及兩種操做,一種是元素比較大小,另外一種是元素交換或移動。因此,若是咱們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。數組
咱們前面講過,算法的內存消耗能夠經過空間複雜度來衡量,排序算法也不例外。不過,針對排序算法的空間複雜度,咱們還引入了一個新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空間複雜度是O(1)的排序算法。性能優化
穩定性千萬不要忽略,僅僅用執行效率和內存消耗來衡量排序算法的好壞是不夠的。針對排序算法,咱們還有一個重要的度量指標,穩定性。這個概念是說,若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變。數據結構
我經過一個例子來解釋一下。好比咱們有一組數據2,9,3,4,8,3,按照大小排序以後就是2,3,3,4,8,9。 這組數據裏有兩個3。通過某種排序算法排序以後,若是兩個3的先後順序沒有改變,那咱們就把這種排序算法叫做穩定的排序算法;若是先後順序發生變化,那對應的排序算法就叫做不穩定的排序算法。數據結構和算法
你可能要問了,兩個3哪一個在前,哪一個在後有什麼關係啊,穩不穩定又有什麼關係呢?爲何要考察排序算法的穩定性呢?
不少數據結構和算法課程,在講排序的時候,都是用整數來舉例,但在真正軟件開發中,咱們要排序的每每不是單純的整數,而是一組對象,咱們須要按照對象的某個key來排序。
好比說,咱們如今要給電商交易系統中的「訂單」排序。訂單有兩個屬性,一個是下單時間,另外一個是訂單金額。若是咱們如今有10萬條訂單數據,咱們但願按照金額從小到大對訂單數據排序。對於金額相同的訂單,咱們但願按照下單時間從早到晚有序。對於這樣一個排序需求,咱們怎麼來作呢?
最早想到的方法是:咱們先按照金額對訂單數據進行排序,而後,再遍歷排序以後的訂單數據,對於每一個金額相同的小區間再按照下單時間排序。這種排序思路理解起來不難,可是實現起來會很複雜。
藉助穩定排序算法,這個問題能夠很是簡潔地解決。解決思路是這樣的:咱們先按照下單時間給訂單排序,注意是按照下單時間,不是金額。排序完成以後,咱們用穩定排序算法,按照訂單金額從新排序。兩遍排序以後,咱們獲得的訂單數據就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。爲何呢?
<font color=red>穩定排序算法能夠保持金額相同的兩個對象,在排序以後的先後順序不變</font>。第一次排序以後,全部的訂單按照下單時間從早到晚有序了。在第二次排序中,咱們用的是穩定的排序算法,因此通過第二次排序以後,相同金額的訂單仍然保持下單時間從早到晚有序。
到這裏,分析一個「排序算法」就結束了,你get到了嗎?接下來,咱們進入實戰算法分析。
冒泡排序描述:冒泡排序只會操做相鄰的兩個數據。每次冒泡操做都會對相鄰的兩個元素進行比較,看是否知足大小關係要求。若是不知足就讓它倆互換。一次冒泡會讓至少一個元素移動到它應該在的位置,重複n次,就完成了n個數據的排序工做。
若是仍是不能一眼看出其靈魂,沒事,我還有一招:
怎麼樣,夠不夠直觀,就是有點慢,哈哈~
package BubbleSort; import java.util.Arrays; public class generalBubble { public static void main(String[] args) { int[] arr=new int[] {5,7,2,9,4,1,0,5,8,7}; System.out.println(Arrays.toString(arr)); bubbleSort(arr); System.out.println(Arrays.toString(arr)); } //冒泡排序 public static void bubbleSort(int[] arr) { //控制共比較多少輪 for(int i=0;i<arr.length-1;i++) { //控制比較的次數 for(int j=0;j<arr.length-1-i;j++) { if(arr[j]>arr[j+1]) { int temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp; } } } } }
測試效果:
實際上,剛講的冒泡過程還能夠優化。當某次冒泡操做已經沒有數據交換時,說明已經達到徹底有序,不用再繼續執行後續的冒泡操做。我這裏還有另一個例子,這裏面給6個元素排序,只須要4次冒泡操做就能夠了。
// 冒泡排序,a表示數組,n表示數組大小 public void bubbleSort(int[] a, int n) { if (n <= 1) return; for (int i = 0; i < n; ++i) { // 提早退出冒泡循環的標誌位 boolean flag = false; for (int j = 0; j < n - i - 1; ++j) { if (a[j] > a[j+1]) { // 交換 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; // 表示有數據交換 } } if (!flag) break; // 沒有數據交換,提早退出 } }
如今,結合剛纔我分析排序算法的三個方面,開始分析冒泡排序算法。
首先,原地排序算法就是特指空間複雜度是O(1)的排序算法,我在上文說起過的,再提一遍(我猜大家確定沒仔細看文章。。。)
冒泡的過程只涉及相鄰數據的交換操做,只須要常量級的臨時空間,因此它的空間複雜度爲O(1),是一個原地排序算法。
在冒泡排序中,只有交換才能夠改變兩個元素的先後順序。爲了保證冒泡排序算法的穩定性,當有相鄰的兩個元素大小相等的時候,咱們不作交換,相同大小的數據在排序先後不會改變順序,因此冒泡排序是穩定的排序算法。
最好狀況下,要排序的數據已是有序的了,咱們只須要進行一次冒泡操做,就能夠結束了,因此最好狀況時間複雜度是O(n)。而最壞的狀況是,要排序的數據恰好是倒序排列的,咱們須要進行n次冒泡操做,因此最壞狀況時間複雜度爲O(n2),平均狀況下的時間複雜度就是O(n2)。
插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工做原理是經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,一般採用in-place排序(即只需用到O(1)的額外空間的排序),於是在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
一樣,我也準備了數字版的,是否是很貼心?
public class InsertSort { public static void main(String[] args) { int[] arr = new int[] {5,3,2,8,5,9,1,0}; insertSort(arr); System.out.println(Arrays.toString(arr)); } //插入排序 public static void insertSort(int[] arr) { //遍歷全部的數字 for(int i=1;i<arr.length;i++) { //若是當前數字比前一個數字小 if(arr[i]<arr[i-1]) { //把當前遍歷數字存起來 int temp=arr[i]; int j; //遍歷當前數字前面全部的數字 for(j=i-1;j>=0&&temp<arr[j];j--) { //把前一個數字賦給後一個數字 arr[j+1]=arr[j]; } //把臨時變量(外層for循環的當前元素)賦給不知足條件的後一個元素 arr[j+1]=temp; } } } }
如今,結合剛纔我分析排序算法的三個方面,開始分析插入排序算法。
從實現過程能夠很明顯地看出,插入排序算法的運行並不須要額外的存儲空間,因此空間複雜度是O(1),也就是說,這是一個原地排序算法。
在插入排序中,對於值相同的元素,咱們能夠選擇將後面出現的元素,插入到前面出現元素的後面,這樣就能夠保持原有的先後順序不變,因此插入排序是穩定的排序算法。
若是要排序的數據已是有序的,咱們並不須要搬移任何數據。若是咱們從尾到頭在有序數據組裏面查找插入位置,每次只須要比較一個數據就能肯定插入的位置。因此這種狀況下,最好是時間複雜度爲O(n)。注意,這裏是從尾到頭遍歷已經有序的數據。
若是數組是倒序的,每次插入都至關於在數組的第一個位置插入新的數據,因此須要移動大量的數據,因此最壞狀況時間複雜度爲O(n2)。
還記得咱們在數組中插入一個數據的平均時間複雜度是多少嗎?沒錯,是O(n)。因此,對於插入排序來講,每次插入操做都至關於在數組中插入一個數據,循環執行n次插入操做,因此平均時間複雜度爲O(n2)。
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,而後再從剩餘的未排序元素中尋找到最小(大)元素,而後放到已排序的序列的末尾。以此類推,直到所有待排序的數據元素的個數爲零。選擇排序是不穩定的排序方法。
public class SelectSort { public static void main(String[] args) { int[] arr = new int[] {3,4,5,7,1,2,0,3,6,8}; selectSort(arr); System.out.println(Arrays.toString(arr)); } //選擇排序 public static void selectSort(int[] arr) { //遍歷全部的數 for(int i=0;i<arr.length;i++) { int minIndex=i; //把當前遍歷的數和後面全部的數依次進行比較,並記錄下最小的數的下標 for(int j=i+1;j<arr.length;j++) { //若是後面比較的數比記錄的最小的數小。 if(arr[minIndex]>arr[j]) { //記錄下最小的那個數的下標 minIndex=j; } } //若是最小的數和當前遍歷數的下標不一致,說明下標爲minIndex的數比當前遍歷的數更小。 if(i!=minIndex) { int temp=arr[i]; arr[i]=arr[minIndex]; arr[minIndex]=temp; } } } }
選擇排序算法是一種原地、不穩定的排序算法,最好時間複雜度狀況:T(n) = O(n2) 最差時間複雜度狀況:T(n) = O(n2) 平均時間複雜度狀況:T(n) = O(n2)
希爾排序也是一種插入排序,它是簡單插入排序通過改進以後的一個更高效的版本,也稱爲縮小增量排序,同時該算法是衝破O(n2)的第一批算法之一。它與插入排序的不一樣之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。希爾排序是把記錄按下表的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。 希爾排序常規步驟: 一、選擇增量gap=length/2 二、縮小增量繼續以gap = gap/2的方式,n/2,(n/2)/2...1 ,有點暈了對吧,仍是看圖解吧哈哈~
一樣是二圖(捂臉)
public class ShellSort { public static void main(String[] args) { int[] arr = new int[] { 3, 5, 2, 7, 8, 1, 2, 0, 4, 7, 4, 3, 8 }; System.out.println(Arrays.toString(arr)); shellSort(arr); System.out.println(Arrays.toString(arr)); } public static void shellSort(int[] arr) { int k = 1; // 遍歷全部的步長 for (int d = arr.length / 2; d > 0; d /= 2) { // 遍歷全部有元素 for (int i = d; i < arr.length; i++) { // 遍歷本組中全部的元素 for (int j = i - d; j >= 0; j -= d) { // 若是當前元素大於加上步長後的那個元素 if (arr[j] > arr[j + d]) { int temp = arr[j]; arr[j] = arr[j + d]; arr[j + d] = temp; } } } System.out.println("第" + k + "次排序結果:" + Arrays.toString(arr)); k++; } } }
希爾排序算法是一種原地、不穩定的排序算法,最好時間複雜度狀況:T(n) = O(nlog2 n) 最差時間複雜度狀況:T(n) = O(nlog2 n) 平均時間複雜度狀況:T(n) =O(nlog2n)
咱們習慣性把它簡稱爲「快排」。快排利用的也是分治思想。乍看起來,它有點像歸併排序,可是思路其實徹底不同。經過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另外一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。 快速排序常規步驟: 一、從數列中挑出一個元素,稱爲 「<font color=red>基準</font>」(pivot),通常第一個基數取第一個數; 二、從新排序數列,全部元素比基準值小的擺放在基準前面,全部元素比基準值大的擺在基準的後面(相同的數能夠到任一邊)。在這個分區退出以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做; 三、<font color=red>遞歸</font>地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
貌似上圖太過於抽象,仍是看下圖吧,哈哈~
public class QuickSort { public static void main(String[] args) { int[] arr = new int[] {3,4,6,7,2,7,2,8,0,9,1}; quickSort(arr,0,arr.length-1); System.out.println(Arrays.toString(arr)); } public static void quickSort(int[] arr,int start,int end) { if(start<end) { //把數組中的第0個數字作爲標準數 int stard=arr[start]; //記錄須要排序的下標 int low=start; int high=end; //循環找比標準數大的數和比標準數小的數 while(low<high) { //右邊的數字比標準數大 while(low<high&&stard<=arr[high]) { high--; } //使用右邊的數字替換左邊的數 arr[low]=arr[high]; //若是左邊的數字比標準數小 while(low<high&&arr[low]<=stard) { low++; } arr[high]=arr[low]; } //把標準數賦給低所在的位置的元素 arr[low]=stard; //處理全部的小的數字 quickSort(arr, start, low); //處理全部的大的數字 quickSort(arr, low+1, end); } } }
快速排序算法是一種原地、不穩定的排序算法,最好時間複雜度狀況:T(n) = O(nlogn) 最差時間複雜度狀況:T(n) = O(n2) 平均時間複雜度狀況:T(n) = O(nlogn)
歸併排序(MERGE-SORT)是創建在歸併操做上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。
歸併操做的工做原理以下: 第一步:申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列 第二步:設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置 第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置 重複步驟3直到某一指針超出序列尾 將另外一序列剩下的全部元素直接複製到合併序列尾
public class MergeSort { public static void main(String[] args) { int[] arr = new int[] {1,3,5,2,4,6,8,10}; System.out.println(Arrays.toString(arr)); mergeSort(arr, 0, arr.length-1); System.out.println(Arrays.toString(arr)); } //歸併排序 public static void mergeSort(int[] arr,int low,int high) { int middle=(high+low)/2; if(low<high) { //處理左邊 mergeSort(arr, low, middle); //處理右邊 mergeSort(arr, middle+1, high); //歸併 merge(arr,low,middle,high); } } public static void merge(int[] arr,int low,int middle, int high) { //用於存儲歸併後的臨時數組 int[] temp = new int[high-low+1]; //記錄第一個數組中須要遍歷的下標 int i=low; //記錄第二個數組中須要遍歷的下標 int j=middle+1; //用於記錄在臨時數組中存放的下標 int index=0; //遍歷兩個數組取出小的數字,放入臨時數組中 while(i<=middle&&j<=high) { //第一個數組的數據更小 if(arr[i]<=arr[j]) { //把小的數據放入臨時數組中 temp[index]=arr[i]; //讓下標向後移一位; i++; }else { temp[index]=arr[j]; j++; } index++; } //處理多餘的數據 while(j<=high) { temp[index]=arr[j]; j++; index++; } while(i<=middle) { temp[index]=arr[i]; i++; index++; } //把臨時數組中的數據從新存入原數組 for(int k=0;k<temp.length;k++) { arr[k+low]=temp[k]; } } }
並歸排序算法是一種穩定的排序算法,最好時間複雜度狀況:T(n) = O(n) 最差時間複雜度狀況:T(n) = O(nlogn) 平均時間複雜度狀況:T(n) = O(nlogn)
基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,複雜度爲O(kn),爲數組長度,k爲數組中的數的最大的位數;基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。
<font color=red>小提示:注意進度條擋住的0~9的數字歸類</font>
public class RadixSort { public static void main(String[] args) { int[] arr = new int[] {23,6,189,45,9,287,56,1,798,34,65,652,5}; radixSort(arr); System.out.println(Arrays.toString(arr)); } public static void radixSort(int[] arr) { //存最數組中最大的數字 int max=Integer.MIN_VALUE; for(int i=0;i<arr.length;i++) { if(arr[i]>max) { max=arr[i]; } } //計算最大數字是幾位數 int maxLength = (max+"").length(); //用於臨時存儲數據的數組 int[][] temp = new int[10][arr.length]; //用於記錄在temp中相應的數組中存放的數字的數量 int[] counts = new int[10]; //根據最大長度的數決定比較的次數 for(int i=0,n=1;i<maxLength;i++,n*=10) { //把每個數字分別計算餘數 for(int j=0;j<arr.length;j++) { //計算餘數 int ys = arr[j]/n%10; //把當前遍歷的數據放入指定的數組中 temp[ys][counts[ys]] = arr[j]; //記錄數量 counts[ys]++; } //記錄取的元素須要放的位置 int index=0; //把數字取出來 for(int k=0;k<counts.length;k++) { //記錄數量的數組中當前餘數記錄的數量不爲0 if(counts[k]!=0) { //循環取出元素 for(int l=0;l<counts[k];l++) { //取出元素 arr[index] = temp[k][l]; //記錄下一個位置 index++; } //把數量置爲0 counts[k]=0; } } } } }
基數排序算法是一種穩定的排序算法,最好時間複雜度狀況:T(n) = O(n * k) 最差時間複雜度狀況:T(n) = O(n * k) 平均時間複雜度狀況:T(n) = O(n * k)。
堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。
在堆的數據結構中,堆中的最大值老是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。堆中定義如下幾種操做: 最大堆調整(Max Heapify):將堆的末端子節點做調整,使得子節點永遠小於父節點 建立最大堆(Build Max Heap):將堆中的全部數據從新排序 堆排序(HeapSort):移除位在第一個數據的根節點,並作最大堆調整的遞歸運算
public class HeapSort { public static void main(String[] args) { int[] arr = new int[] {9,6,8,7,0,1,10,4,2}; heapSort(arr); System.out.println(Arrays.toString(arr)); } public static void heapSort(int[] arr) { //開始位置是最後一個非葉子節點,即最後一個節點的父節點 int start = (arr.length-1)/2; //調整爲大頂堆 for(int i=start;i>=0;i--) { maxHeap(arr, arr.length, i); } //先把數組中的第0個和堆中的最後一個數交換位置,再把前面的處理爲大頂堆 for(int i=arr.length-1;i>0;i--) { int temp = arr[0]; arr[0]=arr[i]; arr[i]=temp; maxHeap(arr, i, 0); } } public static void maxHeap(int[] arr,int size,int index) { //左子節點 int leftNode = 2*index+1; //右子節點 int rightNode = 2*index+2; int max = index; //和兩個子節點分別對比,找出最大的節點 if(leftNode<size&&arr[leftNode]>arr[max]) { max=leftNode; } if(rightNode<size&&arr[rightNode]>arr[max]) { max=rightNode; } //交換位置 if(max!=index) { int temp=arr[index]; arr[index]=arr[max]; arr[max]=temp; //交換位置之後,可能會破壞以前排好的堆,因此,以前的排好的堆須要從新調整 maxHeap(arr, size, max); } } }
基數排序算法是一種原地、不穩定的排序算法,最好時間複雜度狀況:T(n) = O(nlogn) 最差時間複雜度狀況:T(n) = O(nlogn) 平均時間複雜度狀況::T(n) = O(nlogn)
基本的知識都講完了,不知道各位有木有想過這樣一個問題:冒泡排序和插入排序的時間複雜度都是O(n2),都是原地排序算法,爲何插入排序要比冒泡排序更受歡迎呢?
咱們前面分析冒泡排序和插入排序的時候講到,冒泡排序無論怎麼優化,元素交換的次數是一個固定值,是原始數據的逆序度。插入排序是一樣的,無論怎麼優化,元素移動的次數也等於原始數據的逆序度。
可是,從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序須要3個賦值操做,而插入排序只須要1個。咱們來看這段操做:
冒泡排序中數據的交換操做: if (a[j] > a[j+1]) { // 交換 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; } 插入排序中數據的移動操做: if (a[j] > value) { a[j+1] = a[j]; // 數據移動 } else { break; }
咱們把執行一個賦值語句的時間粗略地計爲單位時間(unit_time),而後分別用冒泡排序和插入排序對同一個逆序度是K的數組進行排序。用冒泡排序,須要K次交換操做,每次須要3個賦值語句,因此交換操做總耗時就是3*K單位時間。而插入排序中數據移動操做只須要K個單位時間。
這個只是咱們很是理論的分析,爲了實驗,針對上面的冒泡排序和插入排序的Java代碼,我寫了一個性能對比測試程序,隨機生成10000個數組,每一個數組中包含200個數據,而後在個人機器上分別用冒泡和插入排序算法來排序,冒泡排序算法大約700ms才能執行完成,而插入排序只須要100ms左右就能搞定!
因此,雖然冒泡排序和插入排序在時間複雜度上是同樣的,都是O(n2),可是若是咱們但願把性能優化作到極致,那確定首選插入排序。插入排序的算法思路也有很大的優化空間,咱們只是講了最基礎的一種。若是你對插入排序的優化感興趣,能夠自行再溫習一下希爾排序。
下面是八大經典算法的分析圖:
到這裏,以上八大經典算法分析,都是基於數組實現的。若是數據存儲在鏈表中,這些排序算法還能工做嗎?若是能,那相應的時間、空間複雜度又是多少呢?期待大牛評論出來~
若是本文章對你有幫助,哪怕是一點點,請點個讚唄,謝謝~
歡迎各位關注個人公衆號,一塊兒探討技術,嚮往技術,追求技術...說好了來了就是盆友喔...