標題長坑,就拿最近有在用的來弄吧,先寫個大概,以后逐漸完善。
對於以下代碼,默認包含頭文件:
#include<algorithm>
1.冒泡排序(Bubble Sort)
先從課本上最常見的冒泡排序說起吧,C++代碼如下(個人習慣介紹算法使用C++,比偽代碼還通俗易懂.......)
void BubbleSort(int *A, int n) { for (int i = 0;i < n;++i)//遍歷數組 { for (int j = n - 1;j > i;--j)//倒敘到i { if (A[j - 1] > A[j]) std::swap(A[j], A[j - 1]); }//每輪循環后,都會把一個最小的元素放在最左邊不遍歷區 }//i輪后,數組排序完成 }
在下文中,我會給出實驗測試算法復雜度的方法,我們不急,我先下結論,冒泡排序的復雜度是O(n^2)。
2.插入排序(Insert Sort)
從個人角度講,在冒泡排序之后,個人喜歡先介紹插入排序,原因在於,在基本算法中,實驗證明,insert sort是當n<=9時的最優秀算法,個人喜歡合並使用quick sort 和 insert sort。
void insertSort(int* A, int length) { for (int j = 1;j < length;++j)//從數組第二個數開始 { int key = A[j];//讓選取的數做key //Insert A[j] into the sorted sequence A[0,...j-1] int i = j - 1;//從key前面的值開始倒敘遍歷 while (i >= 0 && A[i]>key) { A[i + 1] = A[i];//不斷地移動數組中元素,留出key的位置 i--; } A[i + 1] = key;//插入key } }
insert sort 同樣是一個O(n^2)的算法。
賦值除了采用這種移動數組的方法,也可以使用對換插入元素的方法,方法如下:
//另一種賦值方法 #include <algorithm> void insertSortAnother(int A[], int length) { for (int j = 1;j < length;++j) { int k = j; int i = j - 1; while (i >= 0 && A[i] > A[k]) { std::swap(A[i], A[k]); k = i--; } } }
效率:T(n)=T(n-1)+n=T(1)+(1+2+3+...+n-1)=T(1)+n*(n-1)/2(最差情況),是一個O(n^2)算法,實際上,式中的n是最差情況,及式前每一個數都比所選數字大,如果只是亂序微調,n可以降到很小的數,以至於在極限情況下
T(n)=T(n-1)+C=T(1)+C*n,是一個O(n)算法,因而insert sort適合對已經基本排好序但是部分退化的數據進行調整。
另外在賦值過程中,可以采用binary search 尋找合適的插入節點來進一步優化算法,使insert sort在大數據前仍然不太遜色。有時間時我會補充此種插入排序。
3.快速排序(Quick Sort)
選擇快排作為第三個介紹的算法,也是因為他在n值足夠大時無與倫比的優越性,這里有時間我會展開說,先簡單放一下代碼。
void quickSort(int* A, int low, int high) { if (low + 1 >= high) return; int i = low; int j = high; do { while (A[i] <= A[high]) { ++i; } while (j > i && A[j] >= A[high]) { --j; } swap(A[i], A[j]); } while (i < j); swap(A[high], A[i]); quickSort(A, low, i - 1); quickSort(A, i + 1, high); }
快速排序是一種O(NlogN)的排序算法。當測試數據足夠大時,其將會顯現出無與倫比的優越性,接下來的測試比較中會有說明。
簡單分析一下復雜度:
∵quicksort先從兩邊一起遍歷將數組遍歷完整的一遍,然后分成兩部分變為兩個子排序,假設兩個自排序的數據規模是a和b
∴T(n)=T(a)+T(b)+a+b
如果a和b很均勻,及a+b+1=n,考慮到數字很大,a+b≈n
T(n)=2*T(n/2)+n
設n=2^k, T(2^k)=2*T(k-1)+2^k
=(2^(k-1))*T(1)+(1+2+4+8+...+k)*(2^k)
=(2^(k-1))*T(1)+1*(1-2*k)/(1-2)*(2^k)
T(n)=(n/2)*T(1)+(2*log(n)-1)*n=n*(T(1)/2+2*log(n)-1)
我們由此得出,在數據均勻(最佳情況)快速排序是一個O(n*log(n))的算法。
而在最差情況下,及每次分出的a和b規模都相差懸殊,a=n-1,b=1
T(n)=T(n-1)+T(1)+n=T(1)+(n-1)*(n+T(1))
我們由此得出,在數據極端(最差情況)快速排序是一個O(n^2)的算法。
而其他情況,快速排序的效率在這二者之間,因而,Pivot的選擇十分重要,如果Pivot能盡量讓數據均勻的分布在兩側,則能獲得更高的效率。一種常用的做法是從數據的頭,中,尾各選取一個數字,並選擇三者的中間值作為pivot,這種做法大幅度的減少了極端數據的可能,但同時也要考慮選取pivot的這個算法本身的時間和空間消耗。
若不通過Pivot選擇來優化,也可以通過根據數據量優化的方法,即當所排列的數據小於10個時,改用對小數據效率更高的InsertSort來進行排序(實驗證明,在數據量為9個時,insert sort有着更佳的效率,考慮到被快排后的數據比較整齊,可以考慮在數據量更多時就停止快排),這種方法排除了quick sort遇到極端數據時的尷尬。另一種做法是當快速排序需要處理的數據規模更小時停止快速排序,之后對所有數據進行一次InsertSort,因為處理這種幾乎排列完畢的數組時,insert sort的效率接近O(n)。
當快排到達一定深度后,用堆排序來完成剩下的工作也是一種不錯的選擇,這樣避免了堆排序的最壞情況,使其保持了O(n*log(n))的復雜度,同時避開了快排的小規模數據處理問題。
在允許出現相同數字的一組數據中,產生最壞情況往往是相同數據導致的,即到某個子排序,所有數據都相同,導致快排進行了無意義的操作。一種可行的方案是將數據分為小於Pivot,等於Pivot,大於Pivot三組,考慮到分三組后實現難度大大增加,一種更簡單的替換方法是,每次獲取數據后檢查一下最左端和最右端的數據,如果相等的話就采用其他排序如insert sort來進行處理。
4.歸並排序(MergeSort)
歸並排序以及其相關優化排序在一定數值范圍內是一個十分優秀的排序算法,此處也將展開說明,代碼如下:
#include<cstdio> const int INT_MAX = 100000000; const int INDEX_MAX = 10000; void mergeSort(int* A, int p, int r) { if (p < r) { int q = (p + r) / 2; mergeSort(A, p, q); mergeSort(A, q + 1, r); //實際排序 int n1 = q - p + 1; int n2 = r - q; int L[INDEX_MAX], R[INDEX_MAX]; //兩個子數組均為從第二位開始使用,務必注意 for (int i = 1;i <= n1;++i) { L[i] = A[p + i - 1]; } for (int i = 1;i <= n2;++i) { R[i] = A[q + i]; } L[n1 + 1] = INT_MAX; R[n2 + 1] = INT_MAX; int i = 1; int j = 1; for (int k = p;k <= r;++k) { if (L[i] <= R[j]) { A[k] = L[i]; ++i; } else { A[k] = R[j]; ++j; } } } }
Merge Sort同樣是一種O(NlogN)的算法。
5.選擇排序(Selection Sort)
作為一種被我剛剛遺忘的O(N^2)算法,還是要說下的,就先給代碼:
void SelectionSort(int *A, int length) { int f; for (int i = 0;i <= length - 2;++i) { f = i; for (int k = i + 1;k < length;++k) { if (A[k] < A[f]) f = k; } std::swap(A[i], A[f]); } }
直接選擇排序的原理是遍歷數組每一個位置,將從該位置到結束的所有點中最小的點放置在該位置。
效率:T(n)=T(n-1)+n=T(1)+(1+n-1)*(n-1)/2=T(1)+n*(n-1)/2,是一個O(n^2)算法。