我們通常所說的排序算法往往指的是內部排序算法,即數據記錄在內存中進行排序。
排序算法大體可分為兩種:
一種是比較排序,時間復雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸並排序,堆排序,快速排序等。
另一種是非比較排序,時間復雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等。
下表給出了常見比較排序算法的性能:
為了便於以下描述,接下來全部算法的排序對象均為亂序數組int a[n];
-
冒泡排序(BubbleSort)
思路:對相鄰的兩個元素進行比較,這樣每輪比較完當前最大(小)的元素就會移到尾部,如此重復n輪,便可實現排序。
實現:
1 void bubbleSort(int a[],int begin,int end) 2 { 3 for(int i = begin;i<end;i++) 4 { 5 for(int j = i+1;j<end;j++) 6 { 7 if(a[i]>a[j]) 8 { 9 std::swap(a[i],a[j]); 10 } 11 } 12 } 13 }
具體排序過程如圖:
總結:
最簡單排序算法,穩定,由於兩層循環因此復雜度為O(n2)。(但是想到當年第一次找工作時被問到,結果不出所料沒有答出來,真是讓我覺得難堪的算法。)
-
歸並排序
歸並排序是經典排序算法中三個復雜度為O(nlgn)中唯一的穩定算法,其主要思想就是將當前數組划分成兩個有序部分,再利用O(n)的時間把兩個有序部分進行合並。其中划分最小數組存在迭代與遞歸兩種版本。
兩種版本通用部分Merge算法:
1 void merge(int a[], int begin, int mid, int end) 2 { 3 int count = end - begin + 1; 4 int* p = new int[count](); 5 int i = begin, j = mid + 1, index = 0; 6 while (i <= mid && j <= end) 7 { 8 p[index++] = a[i] <= a[j] ? a[i++] : a[j++]; 9 } 10 while (j <= end) 11 p[index++] = a[j++]; 12 while (i <= mid) 13 p[index++] = a[i++]; 14 for (int i = 0; i < count; i++) 15 { 16 a[begin++] = p[i]; 17 } 18 delete[]p; 19 }
實現(遞歸):
1 void mergeSort_Recursion(int a[], int begin, int end) 2 { 3 if (begin >= end) return; 4 int mid = (begin + end) / 2; 5 mergeSort_Recursion(a, begin, mid); 6 mergeSort_Recursion(a, mid + 1, end); 7 merge(a, begin, mid, end); 8 }
思路:
- 遞歸對該數組每次進行二分,如此不斷重復下去直到當前數組被划分成n個大小為1數組,然后兩兩合並,當合成更大的有序數組時,再次進行兩兩合並,以此類推直至整個數組有序。
- 歸並排序的端點情況有點麻煩,至少我在實現的時候被坑了很多次,最后參考別人代碼實現(尷尬)。
實現(迭代):
1 void mergeSort_Iteration(int a[], int begin, int end) 2 { 3 int count = end - begin + 1; 4 int left; 5 for (int step = 1; step < count; step *= 2) 6 { 7 //immitate recursion mannully 8 left = begin; 9 while (left + step < end) 10 { 11 int mid = left + step - 1; 12 int right = (mid + step ) < end ? (mid + step) : end; 13 merge(a, left, mid, right); 14 left = right + 1; 15 } 16 } 17 }
思路:迭代與遞歸的唯一區別在於如何獲取到最小的數組,迭代在於從大小為1的數組開始,每次處理的數組大小長度擴大2倍,直到處理長度大於整個數組的長度為止;而遞歸則是每次二分直到最后划分成大小為1的數組,二者可以理解為相反的過程。
其實現可以看圖:
-
堆排序
來源百度百科:
堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。
前面我已經有二叉樹入門的文章了,當時講解的是二叉查找樹,那上面所說的完全二叉樹是怎么樣的一種二叉樹呢??還有滿二叉樹又是怎么的一種二叉樹呢??甚至還有完滿二叉樹??
- 完全二叉樹: 除了最后一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊。
- 滿二叉樹:除了葉子結點之外的每一個結點都有兩個孩子,每一層(當然包含最后一層)都被完全填充。
- 完滿二叉樹:除了葉子結點之外的每一個結點都有兩個孩子結點。
下面用圖來說話:
- 完全二叉樹(Complete Binary Tree):
- 滿二叉樹(Perfect Binary Tree):
- 完滿二叉樹(Full Binary Tree):
參考資料:https://www.cnblogs.com/Java3y/p/8639937.html
簡單來說:堆排序是將數據看成是完全二叉樹、根據完全二叉樹的特性來進行排序的一種算法
- 最大堆要求節點的元素都要不小於其孩子,最小堆要求節點元素都不大於其左右孩子
- 那么處於最大堆的根節點的元素一定是這個堆中的最大值
實現:
1 void heapify(int a[],int cur,int size) 2 { 3 int lChild = 2*cur+1; 4 int rChild =2*cur+2; 5 int max = cur; 6 if(lChild < size && a[max] < a[lChild]) 7 max = lChild; 8 if(rChild < size && a[max] < a[rChild]) 9 max = rChild; 10 if(cur != max) 11 { 12 std::swap(a[cur],a[max]); 13 //當前節點移動到其左右孩子節點,繼續遞歸調用,直到該節點比其孩子都大 14 heapify(a,max,size); 15 } 16 }
思考:堆排序最重要的就是構成最大(小)堆,而對於每一個非葉子節點元素,需要保證其的值始終比其左右節點(如果有的話)大(小),那么Heapify()函數則實現了把某一個元素在建堆時的正確歸位。
建堆:
1 void buildHeap(int a[],int n) 2 { 3 //對於n個元素,最后一個元素的父節點可以表示為n/2-1; 4 for(int i = n/2-1;i>=0;i--) 5 //除去全部葉子節點,生產最大堆 6 heapify(a,i,n); 7 }
思考:除去葉子節點,其余全部節點從最后一個節點的父節點開始逆序執行上面的heapify函數進行建堆。這里由於一共n個元素,因此最后一個元素為n-1,那么其父節點可以表示成(n-1)/2(具體可以畫圖很容易得到關系)。
堆排:
1 void heapSort(int a[],int left,int right) 2 { 3 int size = right - left+1; 4 buildHeap(a,size); 5 //當未排序的數目大於1時 6 while(size>1) 7 { 8 //堆頂為當前堆中最大元素,置換到數組尾部 9 std::swap(a[0],a[--size]); 10 //堆頂元素不滿足條件,重新建堆 11 heapify(a,0,size); 12 } 13 }
思考:當建堆完畢后,第一個元素即堆頂元素將會是最大(小)的元素,那么與最后一個元素交換即可得到一個已排序好的數組,但這會導致整個堆亂序,於是再對堆頂元素調用一次heapify函數,如此反復,直到整個堆中元素只剩下一個為止。
實現過程:
堆排序是不穩定的排序算法,不穩定發生在堆頂元素與A[i]交換的時刻。
-
快速排序
快速排序可以理解為冒泡的進階,在最優條件下,即每次都找到的基准都可以均分數組,此時可以得到O(nlgn)的復雜度,而最差情況則退化成冒泡排序O(n2)。
思路:對於一個數組,尋找一個基准,然后凡是比基准小均放到左側,最后便可以按照記住把當前數組分成兩部分。然后對兩部分再尋找新基准重復上面的方法,直到每個部分被划分為大小為1的部分,至此完成排序。
或者可以參考百度百科解釋:
通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序。
尋找基准分區:
1 int partition(int a[],int left, int right) 2 { 3 //基准 4 int pivot = a[right]; 5 int tail = left-1; 6 for(int i = left;i<right;i++) 7 { 8 if(a[i]<=pivot) 9 { 10 std::swap(a[i],a[++tail]); 11 } 12 } 13 std::swap(a[tail+1],a[right]); 14 return tail+1; 15 }
遞歸實現:
1 void quickSort(int a[],int left, int right) 2 { 3 if(left<right) 4 { 5 int pivot = partition(a,left,right); 6 quickSort(a,left,pivot-1); 7 quickSort(a,pivot+1,right); 8 } 9 }
思考:先計算出基准,然后按照基准划分的兩部分,遞歸調用快排算法即可。
優化:
1 void quickSort(int a[],int left, int right) 2 { 3 while(left < right) 4 { 5 int pivot = partition(a,left,right); 6 quickSort(a,left,pivot-1); 7 left = pivot+1; 8 } 9 }
思考:與普通遞歸相比,由於尾遞歸的調用處於方法的最后,因此方法之前所積累下的各種狀態對於遞歸調用結果已經沒有任何意義,因此完全可以把本次方法中留在堆棧中的數據完全清除,把空間讓給最后的遞歸調用。這樣的優化1便使得遞歸不會在調用堆棧上產生堆積,意味着即時是“無限”遞歸也不會讓堆棧溢出。