一. 堆排序

  堆排序是利用這種資料結構而設計的一種排序演算法。以大堆為例利用堆頂記錄的是最大關鍵字這一特性,每一輪取堆頂元素放入有序區,就類似選擇排序每一輪選擇一個最大值放入有序區,可以把堆排序看成是選擇排序的改進。它的最壞,最好,平均時間複雜度均為O(nlogn),它也是不穩定排序。首先簡單瞭解下堆結構。

  堆是一棵完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖:

對堆中的結點按層進行編號,將這種邏輯結構對映到陣列中:

由於它是一顆完全二叉樹,所以滿足序號

leftchild = parent * 2 + 1;
rightchild = parent * 2 + 2;

這樣的特性,利用這一特性,每次將parent與child進行比較然後向下調整元素的位置。

實現堆排序

  1. 將初始待排序關鍵字序列(R0,R1,R2....Rn)構建成大頂堆,此堆為初始的無序區;初始堆滿足大頂堆性質,但是元素無序。
  2. 依次將將堆頂元素R[0]與最後一個元素R[n]交換,此時得到新的無序區(R0,R1,R2,......Rn-1)和新的有序區(Rn);
  3. 交換後進行向下調整無序區,使其滿足大頂堆性質。
  4. 迴圈執行 2.3 步驟 直到遍歷完陣列。
 1 func HeapSort(arr []int)  {
2 arrLen := len(arr)
3 for i := (arrLen-2)/2; i >= 0; i-- {
4 arrJustDown(arr, i, arrLen)
5 }
6 end := arrLen - 1
7 for end != 0 {
8 arr[0], arr[end] = arr[end], arr[0]
9 arrJustDown(arr, 0, end)
10 end--
11 }
12 fmt.Println(arr)
13 }
14 func arrJustDown(arr []int, root, n int) {
15 parent := root
16 child := parent * 2 + 1
17 for child < n {
18 if child + 1 < n && arr[child + 1] > arr[child] {
19 child++
20 }
21 if arr[child] > arr[parent] {
22 arr[child], arr[parent] = arr[parent], arr[child]
23 parent = child
24 child = parent * 2 + 1
25 } else {
26 break
27 }
28 }
29 }

  建堆和每次向下調整的時間複雜度都是long2N ,所以整個陣列處理完後,需要執行Nlong2N遍,調整過程中,最後一個元素和堆頂元素交換後需要向下調整,所以不保證相同大小元素的位置不變,它是不穩定排序。

二. 快速排序

排序思想

快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。

排序實現

步驟為:(1)從數列中挑出一個元素,稱為 "基準"(pivot);

         (2)重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。

    (3)遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

  當我們每次劃分的時候選擇的基準數接近於整組資料的最大值或者最小值時,快速排序就會發生最壞的情況,但是每次選擇的基準數都接近於最大數或者最小數的概率隨著排序元素的增多就會越來越小,我們完全可以忽略這種情況。但是在陣列有序的情況下,它也會發生最壞的情況,為了避免這種情況,我們在選擇基準數的時候可以採用三數取中法來選擇基準數。 
三數取中法: 選擇這組資料的第一個元素、中間的元素、最後一個元素,這三個元素裡面值居中的元素作為基準數。

 1 func QuickSort(arr []int)  {
2 arrLen := len(arr)
3 quickSort(arr, 0, arrLen - 1)
4 fmt.Println(arr)
5 }
6 func quickSort(arr []int, left, right int) {
7 if left < right {
8 mid := partSort(arr, left, right)
9 quickSort(arr, left, mid - 1)
10 quickSort(arr, mid + 1, right)
11 }
12 }
13 func partSort(arr []int, left, right int) (ret int) {
14 key := arr[right]
15 for left < right {
16 for left < right && arr[left] <= key {
17 left++
18 }
19 arr[right] = arr[left]
20 for left < right && arr[right] >= key {
21 right--
22 }
23 arr[left] = arr[right]
24 }
25 arr[left] = key
26 ret = left
27 return
28 }

快速排序是一種快速的分而治之的演算法,其平均執行時間為O(N*1ogN) 。它的速度主要歸功於一個非長緊湊的並且高度優化的內部迴圈。但是他也是一種不穩定的排序,當基準數選擇的不合理的時候他的效率又會程式設計O(N*N)。快速排序的最好情況: 快速排序的最好情況是每次都劃分後左右子序列的大小都相等,其執行的時間就為O(N*1ogN)。快速排序的最壞情況: 快速排序的最壞的情況就是當分組重複生成一個空序列的時候,這時候其執行時間就變為O(N*N)快速排序的平均情況: 平均情況下是O(N*logN)。

三. 桶排序

介紹

  基本原理是將陣列分到有限數量的桶裡。每個桶再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序),最後依次把各個桶中的記錄列出來記得到有序序列。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間(Θ(n))。但桶排序並不是比較排序,他不受到O(n log n)下限的影響。

排序思想

  桶排序的思想近乎徹底的分治思想。假設待排序的一組數均勻獨立的分佈在一個範圍中,並將這一範圍劃分成幾個子範圍(桶)。然後基於某種對映函式f ,將待排序列的關鍵字 k 對映到第i個桶中 (即桶陣列B 的下標i) ,那麼該關鍵字k 就作為 B[i]中的元素 (每個桶B[i]都是一組大小為N/M 的序列 )。接著將各個桶中的資料有序的合併起來 : 對每個桶B[i] 中的所有元素進行比較排序 (可以使用快排)。然後依次列舉輸出 B[0]….B[M] 中的全部內容即是一個有序序列。

  為了使桶排序更加高效,我們需要做到這兩點:

  1. 在額外空間充足的情況下,儘量增大桶的數量
  2. 使用的對映函式能夠將輸入的 N 個數據均勻的分配到 K 個桶中

實現邏輯

  • 設定一個定量的陣列當作空桶子。
  • 尋訪序列,並且把專案一個一個放到對應的桶子去。
  • 對每個不是空的桶子進行排序。
  • 從不是空的桶子裡把專案再放回原來的序列中。

動圖演示排序過程:

設有陣列 array = [63, 157, 189, 51, 101, 47, 141, 121, 157, 156, 194, 117, 98, 139, 67, 133, 181, 13, 28, 109]

對其進行桶排序:

複雜度

  • 平均時間複雜度:O(n + k)
  • 最佳時間複雜度:O(n + k)
  • 最差時間複雜度:O(n ^ 2)
  • 空間複雜度:O(n * k)
  • 穩定性:穩定

  桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間資料進行排序的時間複雜度,因為其它部分的時間複雜度都為O(n)。很顯然,桶劃分的越小,各個桶之間的資料越少,排序所用的時間也會越少。但相應的空間消耗就會增大。

go程式碼實現

 1 func bin_sort(li []int, bin_num int) {
2 min_num, max_num := li[0], li[0]
3 for i := 0; i < len(li); i++ {
4 if min_num > li[i] {
5 min_num = li[i]
6 }
7 if max_num < li[i] {
8 max_num = li[i]
9 }
10 }
11 bin := make([][]int, bin_num)
12 for j := 0; j < len(li); j++ {
13 n := (li[j] - min_num) / ((max_num - min_num + 1) / bin_num)
14 bin[n] = append(bin[n], li[j])
15 k := len(bin[n]) - 2
16 for k >= 0 && li[j] < bin[n][k] {
17 bin[n][k+1] = bin[n][k]
18 k--
19 }
20 bin[n][k+1] = li[j]
21 }
22 o := 0
23 for p, q := range bin {
24 for t := 0; t < len(q); t++ {
25 li[o] = bin[p][t]
26 o++
27 }
28 }
29 }

  桶排序是計數排序的變種,它利用了函式的對映關係,高效與否的關鍵就在於這個對映函式的確定。把計數排序中相鄰的m個”小桶”放到一個”大桶”中,在分完桶後,對每個桶進行排序(一般用快排),然後合併成最後的結果。

  演算法思想和雜湊中的開雜湊法差不多,當衝突時放入同一個桶中;可應用於資料量分佈比較均勻,或比較側重於區間數量時。

  桶排序最關鍵的建桶,如果桶設計得不好的話桶排序是幾乎沒有作用的。通常情況下,上下界有兩種取法,第一種是取一個10n或者是2n的數,方便實現。另一種是取數列的最大值和最小值然後均分作桶。