「算法01」排序算法小結

  排序算法是一類比較基礎的算法,也是在學習編程與算法的過程當中必須學習的一類問題。初學者常常在排序時摸不着頭腦,面對一衆的排序,不知從何處下手。下面筆者將以筆記的形式分享一下我在學習算法時整理的一些排序算法。git

  假設現有亂序數組:5, 2, 7, 4, 6, 1, 8, 咱們將其排序爲升序數組,各類方法過程以下:算法

  

  1.冒泡排序。編程

  冒泡排序是最簡單的一種排序手段,也是新手最容易想到的一種算法。它經過每一項與其後面的每一項依次比較,找到最大(最小)值並交換位置,通過一次遍歷,便可將數組排序。其過程大體以下:api

  首先提取第一個元素5, 與它後面每一項進行比較,找到最小項1,並與其交換位置,獲得數組:1, 2, 7, 4, 6, 5, 8。 數組

  隨後的第二個元素2, 經比較後發現沒有比2更小的項,數組不變。函數

  再依次提取7, 4, 6, 5,分別與它們後面的每一項進行對比,找到最小項並交換位置,最終數組將按照升序排序,即1, 2, 4, 5, 6, 7, 8。性能

  其代碼以下:學習

1 for (int i = 0; i < n - 1; i++)
2         for (int j = i + 1; j < n; j++)
3             if (arr[i] > arr[j])
4             {
5                 int tmp = arr[i];
6                 arr[i] = arr[j];
7                 arr[j] = tmp;
8             }

  由執行過程能夠看到,冒泡排序執行中須要雙層嵌套循環,一旦須要排序的數組過長,其效率也隨之迅速下降。ui

  在平常使用中,筆者僅在小型數組的排序中使用這種算法,由於其代碼簡單直接,工程量比較小,代碼也易於維護。  spa

 

  2.並歸排序。

  並歸排序採用的是分治思想,利用函數的遞歸,將龐大的排序問題逐級化簡,最後並歸獲得有序的數組。它的思想是將一個數組一分爲二,再將兩個子數組分別排序,再依次比較數組的首項,將較小值依次放入原數組中,最終獲得有序數組。其過程大體以下:

  首先將數組拆分爲array1:5, 2, 7, 4, 和array2:6, 1, 8, 分別對這兩個數組進行排序:array1:2, 4, 5, 7, 和array2:1, 6, 8。

  再將兩個數組並歸爲一個數組:首先比較兩個數組的首項:因爲1 < 2,將array2中的1提取,納入原數組。再比較兩個數組的首項:因爲2 < 6,本次提取值爲2。。。以此類推。同時,爲了防止數組發生越界問題,須要在數組末項以後人爲添加一個無窮項,保證該項大於數組中的每一項,從而防止數組越界的現象發生。通過並歸過程後,原數組排序完成。

  可是這裏會引出一個問題,該如何排序這兩個子數組呢?這裏便須要用到遞歸的思想。咱們可讓函數調用它自身從而完成對拆分開來的子項的排序。

  但這樣又會產生接下來的問題,該如何控制遞歸結束?這裏因爲最後無窮項的存在,當數組長度爲2時,數組內有效值便只有一個,在此時,遞歸結束,將這項返回,交由函數下面的內容完成並歸操做。

  其代碼以下:

 1 void merge(int* arr, int n)  //因爲採用指針傳遞的方式,函數將直接對原地址進行操做,故函數被定義爲無返回值
 2 {
 3     if (n > 1)  //筆者的函數傳遞的n是數組長度,當數組長度大於1,數組能夠被繼續拆分,繼續遞歸若等於1,沒法拆分,遞歸結束
 4     {
 5         int* arr1 = new int[n / 2 + n % 2 + 1], * arr2 = new int[n / 2 + 1];  //開闢新數組,爲了添加無窮項防止越界,每個數組長度增長1位
 6         for (int i = 0; i < n / 2 + n % 2; i++)
 7             arr1[i] = arr[i];
 8         arr1[n / 2 + n % 2] = 100;  //筆者代碼排序的數組最小項爲0, 最大項爲99, 故將無窮項設置爲100
 9         for (int i = n / 2 + n % 2; i < n; i++)
10             arr2[i - n / 2 - n % 2] = arr[i];
11         arr2[n / 2] = 100;
12         merge(arr1, n / 2 + n % 2);  //遞歸排序子數組
13         merge(arr2, n / 2);
14         for (int i = 0, j = 0, k = 0; i < n; i++)  //將兩個子數組並歸
15             if (arr1[j] <= arr2[k])
16             {
17                 arr[i] = arr1[j];
18                 j++;
19             }
20             else
21             {
22                 arr[i] = arr2[k];
23                 k++;
24             }
25     }
26   delete[] (arr1);  //釋放由new申請的內存
27   delete[] (arr2);
28 }

  並歸排序採用了遞歸的方式,因爲無需雙重循環,其執行效率相較冒泡排序在處理長數組時有所提高。可是因爲其代碼相對複雜,筆者在平常使用中不常使用這種算法。

 

  3.堆排序。

  堆排序利用了堆的性質,經過維護一個最大堆或最小堆,提取堆頂元素放入原數組完成排序。

  這裏首先要理解二叉堆的性質:二叉堆是一個數組,能夠近似爲一個徹底二叉樹。 

   

  如圖爲一個最大堆,a爲展開爲樹的形態,b爲數組形態。由堆的對應關係容易獲得,任意節點的下標除以2便可獲得其父節點的下標,而父節點下標乘2可得到子節點的左節點,乘2加1可得到右節點,而這個最大堆的根是最大值。

  若要維護堆的性質,須要自上而下比較,找到父節點與兩個子節點中的最大項,將最大的值與父節點交換位置,直到到堆的底層結束。

  而創建堆的過程就是對堆的每個節點執行維護堆的過程。而每次取出根節點,便須要從新執行維護堆,以確保堆始終爲最大堆。

  其代碼以下:

 1 void heap(int* arr, int n)  //利用堆進行排序,筆者傳值方式採用指針,無返回值
 2 {
 3     for (int i = n / 2; i >= 0; i--)  //建堆,自下而上對每個非底層節點執行維護,確保最終獲得最大堆
 4         heapify(arr, i, n);
 5     for (int i = n - 1; i >= 1; i--)  //依次取出堆頂最大值,插入數組末端
 6     {
 7         int tmp = arr[i];         //用堆底層值替換掉頂層值,從而取出頂層值
 8         arr[i] = arr[0];
 9         arr[0] = tmp;
10         heapify(arr, 0, --n);      //因爲取出最大值後,堆被破壞,從新維護,而此時須要維護的堆長度應該減1,由於末項已是咱們須要的值,所以從堆中剔除
11     }
12 }
13 
14 void heapify(int* arr, int n, int size)  //維護堆的性質
15 {
16     int largest, l, r;            //分別爲最大的節點,左節點,右節點
17     l = n * 2;
18     r = n * 2 + 1;
19     if (l < size && arr[l] > arr[n])    //兩次判斷,比較出三個節點中最大值的下標
20         largest = l;
21     else
22         largest = n;
23     if (r < size && arr[r] > arr[largest])
24         largest = r;
25     if (largest != n)             //若是父節點不是最大值,交換最大值到父節點,從新維護子節點所在分支堆的性質
26     {
27         int tmp = arr[n];
28         arr[n] = arr[largest];
29         arr[largest] = tmp;
30         heapify(arr, largest, size);
31     }
32 }

   堆排序一樣利用了遞歸的思想,效率相對較高,其主要時間消耗在建堆的過程,後期僅須要循環取值,從新維護堆便可。可是堆排序亦或是建堆,能夠用於決策算法等其它算法,這是堆的獨特優點,所以咱們在大型項目能夠重複利用建堆代碼,發揮其獨到優點,減小工程量。

 

  4.快速排序。

  快速排序,顧名思義它是排序速度最快的排序方式。它也利用了遞歸的思想。同並歸排序相似,它經過將數組一分爲二,兩側分別排序,從而達到排序的目的。但與並歸不一樣的是,快速排序沒有後期並歸的過程。在拆分數組的過程當中,快速排序能夠作到一側的最大值小於另外一側的最小值,故最後無需比較,直接鏈接便可。其思想以下:

  首先取數組末尾值爲中間值,對數組前面的全部值依次比較,小於該中間值的向前移動,大於的向後移動。即,當一個值小於中間值,它會與大於中間值部分的下標最小的值進行交換,同時令大於中間值部分的下標加一,實現大於中間支部分的移動。而當一個值大於中間值時,將大於中間值部分擴張包含該值便可。當大於中間值部分移動到中間值處,將中間值移動至兩個子數組之間。至此,中間值左側全部值都小於中間值,中間值右側全部值都大於中間值。將中間值兩側的數組再次分別排序,直到數組不可在分,排序結束。

  其代碼以下:

 1 void quick(int* arr, int m, int n)  //快速排序遞歸部分
 2 {
 3     if (m < n)
 4     {
 5         int i = partition(arr, m, n);  //每次遞歸尋找中間值下標,再依次遞歸中間值左側與右側部分
 6         quick(arr, m, i - 1);
 7         quick(arr, i + 1, n);
 8     }
 9 }
10 
11 int partition(int* arr, int m, int n)  //快速排序處理數組部分
12 {
13     int x = arr[n];             //數組末尾值做爲中間值
14     int i = m - 1;             //i用於小於中間值部分下標的計數
15     for (int j = m; j < n; j++)     //遍歷數組,將值歸檔
16     {
17         if (arr[j] <= x)         //對大於中間值的值的處理
18         {
19             i++;
20             int tmp = arr[i];
21             arr[i] = arr[j];
22             arr[j] = tmp;
23         }
24     }
25     int tmp = arr[i + 1];        //將中間值插入兩個子數列之間
26     arr[i + 1] = arr[n];
27     arr[n] = tmp;
28     return i + 1;             //返回中間值
29 }

  快速排序是運行效率極高的一種排序算法,在處理大型數組時極其有效,並且算法消耗的內存少,適合對性能有要求的項目。固然,對於小型數組的排序很難體現快速排序的優越性,微小型數組排序更多仍是冒泡等相對簡單的排序比較方便開發。

 

  5.線性時間排序。

  線性時間排序是一類特殊的排序算法,這類算法不是簡單的依賴比較數組元素的大小,而是巧妙地利用數組的下標對其進行排序。其時間消耗隨着須要排序的數組的特色會有所變化,有時效率會很高,甚至超過快速排序,而有時效率則通常。可是這類算法廣泛的特色是會有內存消耗,內存的開銷會大於快速排序,所以僅適用於特定數據的排序,不如快速排序普適。

    計數排序

    計數排序適用於一個連續正整數數組,數組的連續性越強,其效率也就越高。

    計數排序巧妙地將數組的元素看成數組下標,開闢一個新數組,利用數組元素充當新數組的下標,利用新數組的值來計數,最後直接將新數組的下標從小到大按統計的數量輸出,完成排序。

    其代碼以下:

 1 void count(int* arr, int n)
 2 {
 3     int max = arr[0];
 4     for (int i = 1; i < n; i++)    //找出數組最大值
 5         if (arr[i] > max)
 6             max = arr[i];
 7     max++;
 8     int* tmp = new int[n];
 9     int* count = new int[max];
10     for (int i = 0; i < max; i++)   //數組初始化
11         count[i] = 0;
12     for (int i = 0; i < n; i++)    //將數組下標用於計數
13         count[arr[i]]++;
14     for (int i = 1; i < max; i++)   //統計每一個下標的排位
15         count[i] += count[i - 1];
16     for (int i = n - 1; i > 0; i--)  //從新寫回原數組
17     {
18         tmp[count[arr[i]]] = arr[i];
19         count[arr[i]]--;
20     }
21     for (int i = 0; i < n; i++)
22         arr[i] = tmp[i];
23     delete[] (tmp);    //釋放空間
24     delete[] (count);
25 }

    基數排序:

    基數排序一樣適用於連續正整數數組,與計數排序不一樣的是,基數排序數組最大值位數越低,其排序效率越高。

    基數排序也是將數組元素做爲下標,依次統計在每一位上的數據的數量,通過屢次循環排序統計後,能夠獲得有序數組。

    其代碼以下:

 1 void radix(int* arr, int n)
 2 {
 3     int max = arr[0];          //找到最高爲位數
 4     for (int i = 1; i < n; i++)
 5         if (arr[i] > max)
 6             max = arr[i];
 7     int digit = 0;
 8     while (max > 10)
 9     {
10         max /= 10;
11         digit++;
12     }
13     int* tmp = new int[n];       //用於存儲中間數據
14     int* count = new int[10];     //用於位數的統計
15     for (int i = 0, radix = 1; i <= digit; i++, radix *= 10)
16     {
17         for (int j = 0; j < 10; j++)  //初始化統計數組
18             count[j] = 0;
19         for (int j = 0; j < n; j++)   //位數數量統計
20         {
21             int k = (arr[j] / radix) % 10;
22             count[k]++;
23         }
24         for (int j = 1; j < 10; j++)
25             count[j] += count[j - 1];
26         for (int j = n - 1; j >= 0; j--)  //將一輪排列的值寫入中間數組
27         {
28             int k = (arr[j] / radix) % 10;
29             tmp[count[k] - 1] = arr[j];
30             count[k]--;
31         }
32         for (int j = 0; j < n; j++)    //將中間數組值寫回
33             arr[j] = tmp[j];
34     }
35     delete[] (tmp);    //釋放空間
36     delete[] (count);
37

 

  以上就是筆者最近統計的一些排序算法,同時筆者也在不斷學習其它的算法。歡迎指正。

 

(算法參考自《算法導論(原書第三版)》--機械工業出版社)

相關文章
相關標籤/搜索