排序算法可以說是計算機專業學生要學習的最基礎的算法,但其實也是最重要的,現在大部分互聯網公司筆試面試也都會涉及到排序算法的知識。除了瞭解思想之外,還應該動手寫一寫,分析一些具體思路、時間複雜度、空間複雜度和穩定性等。
我們面試討論小分隊也簡單討論了一下排序算法,爲了加深記憶,我自己也動手寫了一些代碼(Linux平臺寫的,自己測試是通過了),並做一些分析(由於水平較水,代碼可能有誤!)。
9種排序算法分別爲:選擇排序、冒泡排序、插入排序、希爾排序、歸併排序、堆排序、快速排序、計數排序、基數排序!
1. 選擇排序
基本思想:從第一個位置開始依次選擇該位置的元素,第i次掃描就可以選出第i小的元素,思想很簡單,現在用的較少。
特點:平均時間複雜度O(n^2),最壞時間複雜度O(n^2),額外空間O(1),不穩定排序(舉例:序列5 8 5 2 9, 第一遍選擇第1個元素5會和2交換,原序列中2個5的相對前後順序就被破壞了),n較小時較好!
代碼:
- void select_sort(int *a, int n)
- {
- for(inti = 1; i <= n; i++) {
- intmin_pos = i;
- for(intj = i+1; j <= n; j++)
- if(a[j] < a[min_pos])
- min_pos = j;
- if(min_pos != i) {
- inttemp = a[i];
- a[i] = a[min_pos];
- a[min_pos] = temp;
- }
- }
- }
2. 冒泡排序
基本思想:顧名思義,每一趟都通過相鄰元素兩兩比較,通過交換將較小的元素往前移動,一趟下來就可以將最小的元素(氣泡)移動到最前面。一般會加一個標誌flag,若一趟掃描沒有任何元素交換,則說明序列已經有序,flag爲false,直接退出。
特點:平均時間複雜度O(n^2),最壞時間複雜度O(n^2),額外空間O(1),穩定排序(因爲比較和交換都是兩相鄰元素,相等時不交換),n較小時較好!
代碼:
- void bubble_sort(int *a, int n)
- {
- boolflag =false;
- for(inti = 1; i <= n; i++) {
- for(intj = n; j > i; j--)
- if(a[j] < a[j-1]) {
- inttemp = a[j];
- a[j] = a[j-1];
- a[j-1] = temp;
- flag =true;
- }
- if(!flag)
- return;
- }
- }
3. 插入排序
基本思想:假定一個已排好序的序列和一個元素,只需將該元素從序列末尾向前比較,找到第一個小於它的序列元素,排在其之後即可。思想類似於玩撲克牌時整理牌面。
特點:平均時間複雜度O(n^2),最壞時間複雜度O(n^2),額外空間O(1),穩定排序(比較元素和序列時,找到序列中相等元素的話,排在其之後),序列大部分已排好序時(時間複雜度可提升至O(n))較好!
代碼:
- void insert_sort(int *a, int n)
- {
- inttemp;
- for(inti =2; i <= n; i++) {
- intj = i - 1;
- temp = a[i];
- while(j >= 1) {
- if(a[j] > temp) {
- a[j+1] = a[j];
- j--;
- }else
- break;
- }
- a[j+1] = temp;
- }
- }
4. 希爾排序
基本思想:插入排序的升級版(根據其特點:序列大部分已排好序時效率很高),將數據分爲不同的組,先對每一組進行排序,然後對所有元素進行一次排序(即最後步長必須爲1),步長的選擇是遞減的,比如5、3、1,現在一般使用D.E.Knuth分組方法(n很大是,用h(n+1)=3h(n)+1來分組,即1、4、13......)。
特點:平均時間複雜度O(n*logn),最壞時間複雜度O(n^s)(1<s<2),額外空間O(1),不穩定排序(相等元素在不同組裏,交換後相對順序可能改變)!
代碼:
- void shell_sort(int *a, int n)
- { //我這裏步長爲5、3、1,僅爲舉例
- for(intgap = 5; gap > 0; gap -= 2)
- for(inti = gap + 1; i <=n; i++) {
- intj = i - gap;
- inttemp = a[i];
- while(j >= 1) {
- if(a[j] > temp) {
- a[j + gap] = a[j];
- j -= gap;
- }else
- break;
- }
- a[j+gap] = temp;
- }
- }
5. 歸併排序
基本思想:分治的思想,就是用遞歸先將序列分解成只剩一個元素的子序列,然後逐漸向上進行合併,每次合併過程就是將兩個內部已排序的子序列進行合併排序,只需O(n)時間。
特點:平均時間複雜度O(n*logn),最壞時間複雜度O(n*logn),額外空間O(n)(另外需要一個數組),穩定排序,當n較大時較好(當也不能太大,用了遞歸就要考慮棧溢出)!
代碼:
- int b[MAX] = {0};
- void merge(int *a,intlow, int mid, inthigh)
- {
- inti = low, j = mid + 1;//左邊和右邊的初始位置
- intk = i;
- while(i <= mid && j <= high) {
- if(a[i] <= a[j]) {
- b[k++] = a[i];
- i++;
- }else{
- b[k++] = a[j];
- j++;
- }
- }
- while(i <= mid){
- b[k++] = a[i++];
- }
- while(j <= high){
- b[k++] = a[j++];
- }
- for(intx = 1, i = low; x <= high-low+1; x++, i++)
- a[i] = b[i];
- }
- voidmerge_sort(int*a,int low, int high)
- {
- intmid;
- if(low < high) {
- mid = (low + high) / 2;
- merge_sort(a, low, mid);
- merge_sort(a, mid+1, high);
- merge(a, low, mid, high);
- }
- }
6. 堆排序
基本思想:利用最大堆的性質——父節點擁有最大值,所以不斷的將堆的根節點與最後節點交換,減小堆長度,然後再恢復堆性質,堆排序主要就是建立最大堆和不斷恢復堆性質兩個過程。堆排序不需要用到遞歸,所以適合海量數據處理,同時堆還可以用於優先級隊列。
特點:平均時間複雜度O(n*logn),最壞時間複雜度O(n*logn),額外空間O(1),不穩定排序(涉及根節點與最後節點的交換,可能會破壞兩相等元素的相對位置!),當n較大時較好(海量數據)!
代碼:
- void max_heapify(int *a, int p, int n)
- {
- intleft = 2 * p;
- intright = 2 * p + 1;
- intlarge = p;
- if(left <= n && a[left] > a[p])
- large = left;
- if(right <= n && a[right] > a[large])
- large = right;
- if(large != p) {
- inttemp = a[p];
- a[p] = a[large];
- a[large] = temp;
- max_heapify(a, large, n);
- }
- }
- voidheap_sort(int*a,int n)
- {
- //build_max_heap
- for(inti = n/2; i > 0; i--)
- max_heapify(a, i, n);
- inttemp;
- while(n > 1){
- temp = a[n];
- a[n] = a[1];
- a[1] = temp;
- --n;
- max_heapify(a, 1, n);
- }
- }
7. 快速排序
基本思想:快排是目前使用最多的排序算法,每次都是先選擇一個位置的元素(可以爲序列的最左或最右位置)作爲中間值,將比其小的元素放在其左邊,比其大的元素放在右邊,然後遞歸對其左邊和右邊的子序列進行相同操作,直到子序列爲單個元素。
特點:平均時間複雜度O(n*logn),最壞時間複雜度O(n^2)(序列基本有序時,退化爲冒泡排序),額外空間O(logn),不穩定排序(舉例:序列爲 5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂),當n較大時較好(當也不能太大,用了遞歸就要考慮棧溢出)!
代碼:
- void quick_sort(int *a, int p, int r)
- {
- if(p < r) {
- inttemp;
- intx = a[r];
- inti = p - 1;
- for(intj = p; j < r; j++)
- if(a[j] < x) {
- i++;
- temp = a[j];
- a[j] = a[i];
- a[i] = temp;
- }
- temp = a[i+1];
- a[i+1] = a[r];
- a[r] = temp;
- quick_sort(a, p, i);
- quick_sort(a, i+2, r);
- }
- }
8. 計數排序
基本思想:假定輸入是有一個小範圍內的整數構成的(比如年齡等),利用額外的數組去記錄元素應該排列的位置,思想比較簡單,看代碼即可瞭解。
特點:在一定限制下時間複雜度爲O(n),額外空間O(n)(需要兩個數組),穩定排序!
代碼:
- int b[MAX] = {0};
- int c[MAX] = {0};
- void counting_sort(int *a, int n)
- {
- for(inti=1; i <= n; i++)
- c[a[i]]++; //c[i]包含等於i的元素個數
- for(inti=1; i < MAX; i++)
- c[i] += c[i-1];//c[i]包含小於等於i的元素個數
- for(inti = n; i>0; i--){
- b[c[a[i]]] = a[i];
- c[a[i]]--;
- }
- for(inti = 1; i <=n; i++)
- a[i] = b[i];
- }
9. 基數排序
基本思想:只適用於整數排序,確定序列中元素的最大位數d,只要進行d次循環,從低位開始根據相應位置的數進行排序。(我的代碼中具體排序是參考了計數排序,數據結構中還可以用鏈式相關的方法)。
特點:在一定限制下時間複雜度爲O(n),額外空間O(n)(需要兩個數組),穩定排序!
代碼:
- int b[MAX] = {0};
- int counter[10] = {0};
- int get_value(int v, int d) //獲取第d位上的值
- {
- for(inti = 1; i < d; i++)
- v = v/10;
- returnv%10;
- }
- //只能排序d位的十進制數
- voidradix_sort(int*a,int n, int d)
- {
- intx;
- for(intk = 1; k <= d; k++) {
- for(inti = 0; i < 10; i++)
- counter[i] = 0;//注意,一定要清零
- for(inti = 1; i <= n; i++) {
- x = get_value(a[i], k);
- counter[x]++;
- }
- for(inti = 1; i < 10; i++)
- counter[i] += counter[i-1];
- for(inti = n; i > 0; i--) {
- x = get_value(a[i], k);
- b[counter[x]] = a[i];
- counter[x]--;
- }
- for(inti = 1; i <= n; i++)
- a[i] = b[i];
- }
- }
排序總結
穩定性:選擇排序、快速排序、希爾排序、堆排序不是穩定的排序算法,而冒泡排序、插入排序、歸併排序和基數排序是穩定的排序算法。
快速排序算法使用最廣泛,大數據量時適合使用快速排序、歸併排序和堆排序,需要O(n)時間複雜度時(當然要考慮數值範圍的限制),可以考慮使用計數排序、基數排序、桶排序(上面未介紹,思想很簡單,假設數據分佈均勻!)等。
最後是我用來測試排序算法的main函數,非常簡單!
- #include <iostream>
- usingnamespacestd;
- constintMAX = 255;
- int main ()
- {
- intn;
- inta[MAX];
- cin >> n;
- for(inti = 1; i <= n; i++)
- cin >> a[i];
- cout <<"Before sort:";
- for(inti = 1; i <= n; i++)
- cout << a[i] <<" ";
- cout << endl;
- //radix_sort(a, n, 2);
- //select_sort(a, n);
- //insert_sort(a, n);
- //bubble_sort(a, n);
- //quick_sort(a, 1, n);
- //heap_sort(a, n);
- //merge_sort(a, 1, n);
- //counting_sort(a, n);
- //shell_sort(a, n);
- cout <<"Sort:";
- for(inti = 1; i <= n; i++)
- cout << a[i] <<" ";
- cout << endl;
- return0;
- }