演算法系列筆記1(排序),演算法系列筆記排序
本次主要記錄一些經典的排序演算法,其中包括冒泡排序、直接選擇排序、插入排序、歸併排序、快速排序、堆排序、希爾排序、桶排序以及計數排序和基數排序。首先會給出這些排序演算法的基本思想,然後給出實現的代碼,最後會給出其時間複雜度。
1:冒泡排序
思想:
(1):比較相鄰的前後兩個元素,如果後面的資料小於前面的資料,則交換這兩個資料的位置。這樣經過一次遍曆,最小的元素將在第0個位置,屬於”冒泡”.
(2):重複第一步,依次將第二小…的元素排列到數組的頂端。
// 交換資料的三種方法void swap(int &t1, int &t2){int tmp = t1;t1 = t2;t2 = tmp;}
<pre name="code" class="cpp">// 使用異或進行交換void swap2(int &a, int &b){if(a != b){ /// 防止swap2(a, a)自身交換自身a ^= b;b ^= a;a ^= b;}}void swap3(int &a, int &b){if(a != b){a = a + b;b = a - b;a = a - b;}}
#include <iostream>using namespace std;// 可以進行最佳化 設定監視哨,當該趟沒有進行任何交換時,則退出 時間複雜度為n^2void swap(int &t1, int &t2){int tmp = t1;t1 = t2;t2 = tmp;}void bubbleSort(int r1[], int n){for(int i = 0; i < n; i++){for(int j = n-1; j > i; j--){if(r1[j] < r1[j-1]){swap(r1[j], r1[j-1]);}}}}int main(){int a[10] ={6,5,4,7,2,4,1,8,5,10};bubbleSort(a, 10);cout <<"冒泡排序後: ";for(int i = 0; i < 10; i++){cout << a[i] << " ";}cout << endl;return 0;}
時間複雜度:O(n^2) 為穩定的排序演算法
注意:可以進行最佳化設定監視哨,當該趟沒有進行任何交換時,則退出。
2:直接選擇排序
思想:第一次從R[0]~R[n-1]中選取最小值,與R[0]交換,第二次從R[1]~R[n-1]中選取最小值,與R[1]交換,....,第i次從R[i-1]~R[n-1]中選取最小值,與R[i-1]交換,.....,第n-1次從R[n-2]~R[n-1]中選取最小值,與R[n-2]交換,總共通過n-1次,得到一個按排序碼從小到大排列的有序序列.
代碼:
#include <iostream>using namespace std;// 使用臨時變數進行交換void swap(int &a, int &b){int tmp = a;a = b;b = tmp;}void display(int a[], int n){for(int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;}void SelectSort(int a[], int n){for(int i = 0; i < n-1; i++){int minIndex = i;int minElement = a[minIndex];for(int j = i+1; j < n; j++){if(a[j] < minElement){minElement = a[j];minIndex = j;}}swap3(a[i], a[minIndex]);}}int main(){int a[10] ={6,5,4,7,2,4,1,8,5,10};SelectSort(a, 10);display(a, 10);return 0;}
時間複雜度:O(n^2)。 為不穩定的排序演算法
3:插入排序
思想:(1)初始時,a[0]自成1個有序區,無序區a[1,..n-1]。令i=1
(2)令temp=a[i],j從i-1到0,如果a[j]>temp 則a[j+1]=a[j],這樣就將a[i]併入當前的有序區a[0,…i-1]中形成a[0,..i]的有序區間。
(3)i++並重複第二步直到i==n-1。排序完成。
insertSort.h
#ifndef INSERTSORT#define INSERTSORT#include "sortAlg.h"class InsertSort:public SortAlg{public:InsertSort(int *is, const int &len):SortAlg(is, len){}void sort(){for(int i = 1; i < length; i++){int j = i-1, tmp = sortElement[i];while(sortElement[j] > tmp && j >= 0){sortElement[j+1] = sortElement[j];j--;}sortElement[j+1] = tmp;}}};#endif
平均時間複雜度為O(n^2),如果該序列已經排序好了,此時效率最高為O(n)。是穩定的排序演算法。
4:歸併排序
思想:(1)如果只有一個元素則已經排序好,返回。(2)分別歸併排序A[1,…n/2]和A[n/2+1,…n]。
(1) 對兩個已經歸併好的序列進行歸併O(n).
代碼:
#include <iostream>using namespace std;void mergeSort(int r1[], int r2[], int s, int m , int t){int i = s, j = m+1;int k = s;while(i <= m && j <= t){if(r1[i] <= r1[j])r2[k++] = r1[i++];elser2[k++] = r1[j++];}if(i > m){while(j <= t)r2[k++] = r1[j++];}if(j > t){while(i <= m)r2[k++] = r1[i++];}for(int n = s; n <= t; n++){ /// 注意 這個是必不可少r1[n] = r2[n];}}void merge(int r1[], int r2[], int s, int t){if(s < t){int m = (s+t)/2;merge(r1, r2, s, m); // 歸併排序A[1,,n/2]merge(r1, r2, m+1, t); // 歸併排序A[n/2+1,, n]mergeSort(r1, r2, s, m, t); // 對已排序的兩個子序列進行歸併}}void display(int *a, int length){for(int i = 0; i < length; i++){cout << a[i] << " ";}cout << endl;}int main(){int r1[10] = {3,2,5,1,7,4,9,7,8,7}, r2[10];merge(r1, r2, 0, 9);display(r2, 10);return 0;}
時間複雜度:O(nlgn). 為穩定的排序演算法
5:快速排序
思想:(1)通常選擇一個元素作為主元x(通常為第一個元素,隨機化快排則是隨機播放一個元素,然後與第一個元素進行交換)。將數組A中小於x的元素放在其左邊,大於等於x的元素放在其右邊。<這步劃分通常有兩種方法,書中是挖坑填數,從兩邊進行。而另外一種方法則是從一邊開始,代碼也給出來了>。
(2)對左右子序列分別進行快速排序即可。
代碼:
#include <iostream>#include <ctime>using namespace std;void swap(int &t1, int &t2){int tmp = t1;t1 = t2;t2 = tmp;}// MIT視頻中劃分方法 只從一邊進行int Partition(int r1[], int p, int q){int i = p, j = p+1;int pivot = r1[p];for(j = p+1; j <= q; j++){if(r1[j] < pivot){i++;swap(r1[i], r1[j]);}}swap(r1[i], r1[p]);return i;}// 書中劃分方法 從兩邊進行分劃 挖坑填數 分而治之法 不穩定排序演算法int Partition2(int r1[], int p, int q){int i = p, j = q;int pivot = r1[p];while(i < j){while(j > i && r1[j] > pivot)j --;if(j > i)r1[i++] = r1[j];while(i < j && r1[i] < pivot)i++;if(i < j)r1[j--] = r1[i];}r1[i] = pivot;return i;}// 隨機化的快速排序演算法int randomPartition(int r1[], int p, int q){srand((unsigned)time(0));int randInt = rand()%(q-p+1)+p; // 隨機播放一個元素作為主元swap(r1[p], r1[randInt]);int i = p, j = p+1;int pivot = r1[p];for(j = p+1; j <= q; j++){if(r1[j] < pivot){i++;swap(r1[i], r1[j]);}}swap(r1[i], r1[p]);return i;}void QuickSort(int r1[], int p, int q){if(p < q){int m = Partition(r1, p, q);//int m = randomPartition(r1, p, q); QuickSort(r1, p, m-1);QuickSort(r1, m+1, q);}}int main(){int a[10] ={6,5,4,7,2,4,1,8,5,10};cout <<"排序前: ";for(int i = 0; i < 10; i++){cout << a[i] << " ";}cout << endl;QuickSort(a, 0, 9);cout <<"快排後: ";for(int i = 0; i < 10; i++){cout << a[i] << " ";}cout << endl;return 0;}
平均時間複雜度為O(nlgn)。當序列已經有序,此時為最壞情況,時間複雜度為O(n^2)。為不穩定的排序演算法。
6:堆排序
堆排序首先需要理解的就是堆的儲存,刪除,插入及堆化的過程(可以參考:http://blog.csdn.net/morewindows/article/details/6709644)。堆排序的思想就是將已經建好的堆(小堆舉例)的根項目與最後一個元素互換,然後對除最後一個元素的堆進行堆化變為二元堆積,這樣最後一個元素就是最小元素,重複這個操作,此時得到的數組即為降序排列的數組。
代碼:
#include <iostream>using namespace std;void display(int a[], int n){for(int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;}void swap(int &a, int &b){int tmp = a;a = b;b = tmp;}// 用數組來儲存堆, 插入元素相當於在數組最後插入元素 向上進行堆化void minHeapFixUp(int a[], int i){int tmp = a[i];int j = (i-1)/2;while(j >= 0 && i!= 0){ // 沒有i!=0 會出現死迴圈if(a[j] <= tmp) break;a[i] = a[j];i = j;j = (i-1)/2; // -1/2 == 0}a[i] = tmp;}// 在最小堆中添加元素void minHeapAddNumber(int a[], int n, int x){a[n-1] = x;minHeapFixUp(a, n-1);}// 刪除元素都是刪除第0個元素,並將最後一個元素放在第0個元素的位置 進行堆化void minHeapFixDown(int a[], int i, int n){ // 向下 刪除元素int j = 2*i +1;int tmp = a[i];while(j < n){if(j+1 < n && a[j+1] < a[j])j++;if(tmp <= a[j]) break;a[i] = a[j];i = j;j = j*2+1;}a[i] = tmp;}// 在最小堆中刪除數void minHeapDeleteNumber(int a[], int n){swap(a[0], a[n-1]);minHeapFixDown(a, 0, n-2);}// 堆化數組 建立最小堆void makeMinHeap(int a[], int n){for(int i = n/2 - 1; i >= 0; i--){minHeapFixDown(a, i, n);}}// 堆排序void minHeapSortToDes(int a[], int n){for(int i = n-1; i > 0; i--){swap(a[0], a[i]);minHeapFixDown(a, 0, i);}}int main(){int a[11] = {9, 12, 17,30, 50, 20, 60, 65, 4, 19};// 建堆makeMinHeap(a, 10);display(a, 10);// 添加元素minHeapAddNumber(a, 11, 3);display(a, 11);// 刪除元素 //minHeapDeleteNumber(a, 11);//display(a, 11);// 堆排序minHeapSortToDes(a, 11);display(a, 11);return 0;}
時間複雜度為O(nlgn)。為不穩定的排序演算法。
7:希爾排序
思想:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
步長一般以n/2開始,每次減半,直到最後為1.
代碼:
#include <iostream>using namespace std;void ShellSort(int a[], int n){for(int gap = n/2; gap > 0; gap --){ // 步長for(int i = gap; i < n; i++){ int temp = a[i];int j = i-gap;while(j >= 0 && a[j] > temp){ // 進行直接插入排序a[j+gap] = a[j];j -= gap;}a[j+gap] = temp;}}}void display(int a[], int n){for(int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;}int main(){int a[10] ={6,5,4,7,2,4,1,8,5,10};ShellSort(a, 10);display(a, 10);return 0;}
時間複雜度一般要取決於步長,在O(nlgn)與O(n^2)之間,為不穩定排序。
8:桶排序
桶排序 (Bucket sort)或所謂的箱排序,是一個排序演算法,工作的原理是將陣列分到有限數量的桶子裡。每個桶子再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間(Θ(n))。但桶排序並不是 比較排序,他不受到 O(n log n) 下限的影響。
例如要對大小為[1..1000]範圍內的n個整數A[1..n]排序,可以把桶設為大小為10的範圍,具體而言,設集合B[1]儲存[1..10]的整數,集合B[2]儲存(10..20]的整數,……集合B[i]儲存((i-1)*10,i*10]的整數,i = 1,2,..100。總共有100個桶。然後對A[1..n]從頭到尾掃描一遍,把每個A[i]放入對應的桶B[j]中。 然後再對這100個桶中每個桶裡的數字排序,這時可用冒泡,選擇,乃至快排,一般來說任何排序法都可以。最後依次輸出每個桶裡面的數字,且每個桶中的數字從小到大輸出,這樣就得到所有數字排好序的一個序列了。
假設有n個數字,有m個桶,如果數字是平均分布的,則每個桶裡面平均有n/m個數字。如果對每個桶中的數字採用快速排序,那麼整個演算法的複雜度是O(n+m*n/m*log(n/m))=O(n+nlogn-nlogm)
從上式看出,當m接近n的時候,桶排序複雜度接近O(n)
當然,以上複雜度的計算是基於輸入的n個數字是平均分布這個假設的。這個假設是很強的,實際應用中效果並沒有這麼好。如果所有的數字都落在同一個桶中,那就退化成一般的排序了。
#include <iostream>using namespace std;// MIT視頻中劃分方法 只從一邊進行int Partition(int r1[], int p, int q){int i = p, j = p+1;int pivot = r1[p];for(j = p+1; j <= q; j++){if(r1[j] < pivot){i++;swap(r1[i], r1[j]);}}swap(r1[i], r1[p]);return i;}void QuickSort(int r1[], int p, int q){if(p < q){int m = Partition(r1, p, q);//int m = randomPartition(r1, p, q);QuickSort(r1, p, m-1);QuickSort(r1, m+1, q);}}// 每個桶中的元素 為一個數組 並且記錄了該桶中存放元素的數目struct bucket{int node[10]; // 應該為待排序數組的大小 這裡簡單的定義為了10 很局限int count;};void BucketSort(int r1[], int n){// 桶的大小為10bucket* pBucket = new bucket[10];// 初始化桶for(int i = 0; i < 10; i++){(pBucket+i)->count = 0;}int maxNum = r1[0], minNum = r1[0];for(int i = 1; i < n; i++){if(r1[i] > maxNum) maxNum = r1[i];if(r1[i] < minNum) minNum = r1[i];}// 將資料放入桶中for(int i = 0; i < n; i++){int index = ((r1[i]-minNum)/(maxNum-minNum))*9;(pBucket+index)->node[(pBucket+index)->count] = r1[i]; (pBucket+index)->count++;}// 對每個桶進行快排 或者其它排序方法int k = 0;for(int i = 0; i < 10; i++){QuickSort((pBucket+i)->node, 0, (pBucket+i)->count-1);for(int j = 0; j < (pBucket+i)->count; j++){r1[k++] = (pBucket+i)->node[j];}}delete []pBucket;}void display(int a[], int n){for(int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;}int main(){int a[10] ={6,5,4,7,2,4,1,8,5,10};BucketSort(a, 10);display(a, 10);return 0;}
時間複雜度O(n),為不穩定排序演算法
9:計數排序和基數排數
這兩個排序演算法都是線性時間排序演算法,計數排序需假設輸入資料為一個小區間內的整數,為小規模的數字。而基數排序一般用來線上性時間內處理大範圍的資料。
計數排序的代碼:
#include <iostream>using namespace std;void CountSort(int r1[], int c[], int n, int k, int r2[]){for(int i = 0; i < k; i++) c[i] = 0;for(int i = 0; i < n; i++) c[r1[i]]++; // 統計每個元素出現的次數for(int i = 1; i < k; i++) c[i] = c[i-1] + c[i]; // 統計小於等於該元素的個數 即該元素即將放在r2中的位置//for(int i = n-1; i >= 0; i--){ // 為了保證該演算法是穩定的for(int i = 0; i < n; i++){r2[c[r1[i]] - 1] = r1[i];c[r1[i]] = c[r1[i]] - 1;}}int main(){int a[10] = {2,3,1,4,5,4,2,3,0,3}, b[10];int c[6];cout << "排序前: ";for(int i = 0; i < 10; i++){cout << a[i] <<" ";}cout << endl;CountSort(a, c, 10, 6, b);cout << "計數排序後: ";for(int i = 0; i < 10; i++){cout << b[i] <<" ";}cout << endl;return 0;}
基數排序代碼可能局限於實際的應用,先從個位排序,再到十位,最後到高位等,可以對每位採用桶排序。
它們的時間複雜度都為O(n)
總結:(1)冒泡排序、直接選擇排序、插入排序、歸併排序、快速排序、堆排序、希爾排序為基於比較的排序演算法,其平均時間複雜度不會低於O(nlgn)。而計數和基數排序都為線性時間排序演算法。桶排序比較特殊
(2) 冒泡排序、直接選擇排序、插入排序的平均時間複雜度為O(n^2)。歸併排序、快速排序、堆排序的平均時間複雜度為O(nlgn),為漸進最好的比較排序演算法。桶排序、計數排序及基數排序為O(n)
(3)直接選擇排序、快速排序、堆排序、希爾排序(跨躍步長交換)和桶排序都為不穩定的排序演算法,他們都跨躍交換了。其它的都為穩定排序演算法。
參考文獻
1:http://blog.csdn.net/column/details/algorithm-easyword.html白話白話經典演算法系列
2:http://blog.csdn.net/houapple/article/details/6480100桶排序