一. 堆排序
堆排序是利用堆這種數據結構而設計的一種排序算法。以大堆為例利用堆頂記錄的是最大關鍵字這一特性,每一輪取堆頂元素放入有序區,就類似選擇排序每一輪選擇一個最大值放入有序區,可以把堆排序看成是選擇排序的改進。它的最壞,最好,平均時間復雜度均為O(nlogn),它也是不穩定排序。首先簡單了解下堆結構。
堆
堆是一棵完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖:
對堆中的結點按層進行編號,將這種邏輯結構映射到數組中:
由於它是一顆完全二叉樹,所以滿足序號
leftchild = parent * 2 + 1;
rightchild = parent * 2 + 2;
這樣的特性,利用這一特性,每次將parent與child進行比較然后向下調整元素的位置。
實現堆排序
- 將初始待排序關鍵字序列(R0,R1,R2....Rn)構建成大頂堆,此堆為初始的無序區;初始堆滿足大頂堆性質,但是元素無序。
- 依次將將堆頂元素R[0]與最后一個元素R[n]交換,此時得到新的無序區(R0,R1,R2,......Rn-1)和新的有序區(Rn);
- 交換后進行向下調整無序區,使其滿足大頂堆性質。
- 循環執行 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] 中的全部內容即是一個有序序列。
為了使桶排序更加高效,我們需要做到這兩點:
- 在額外空間充足的情況下,盡量增大桶的數量
- 使用的映射函數能夠將輸入的 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的數,方便實現。另一種是取數列的最大值和最小值然后均分作桶。