常見排序算法總結和 Go 標準庫排序源碼分析

前言

排序算法是數組相關算法的基礎知識之一,它們的經典思想能夠用於不少算法之中。這裏詳細介紹和總結 7 種最多見排序算法,並用 Go 作了實現,同時對比這幾種算法的時間複雜度、空間複雜度和穩定性 。後一部分是對 Go 標準庫排序實現的源碼閱讀和分析, 理解官方是如何經過將以上排序算法進行組合來提升排序性能,完成生產環境的排序實踐。html

排序算法分類

常見的這 7 種排序算法分別是:c++

  • 選擇排序
  • 冒泡排序
  • 插入排序
  • 希爾排序
  • 歸併排序
  • 快速排序
  • 堆排序

咱們能夠根據算法特色像複雜度、是否比較元素、內外部排序等特色對它們作分類,好比上面的算法都是內部排序的。通常能夠基於算法是否比較了元素,將排序分爲兩類:git

  1. 比較類排序:經過比較來決定元素間的相對次序。因爲其平均時間複雜度不能突破$O(N\log N)$,所以也稱爲非線性時間比較類排序。
  2. 非比較類排序:不經過比較來決定元素間的相對次序。它能夠突破基於比較排序的時間下界,以線性時間運行,所以也稱爲線性時間非比較類排序。主要實現有: 桶排序、計數排序和基數排序。

經過這個的分類,能夠先有一個基本的認識,就是比較類排序算法的平均時間複雜度較好的狀況下是 $O(N\log N)$(一遍找元素 $O(N)$,一遍找位置$O(\log N)$)。程序員

注: 有重複大量元素的數組,能夠經過三向切分快速排序, 將平均時間複雜度下降到 $O(N)$github

比較類排序算法

由於非比較排序有其侷限性,因此它們並不經常使用。本文將要介紹的 7 種算法都是比較類排序。golang

選擇排序

原理:遍歷數組, 從中選擇最小元素,將它與數組的第一個元素交換位置。繼續從數組剩下的元素中選擇出最小的元素,將它與數組的第二個元素交換位置。循環以上過程,直到將整個數組排序。算法

時間複雜度分析:$O(N^{2})$。選擇排序大約須要 $N^{2}/2$ 次比較和 $N$ 次交換,它的運行時間與輸入無關,這個特色使得它對一個已經排序的數組也須要不少的比較和交換操做。shell

selection_sort

實現api

// 選擇排序 (selection sort)
package sorts

func SelectionSort(arr []int) []int {

    for i := 0; i < len(arr); i++ {
        min := i
        for j := i + 1; j < len(arr); j++ {
            if arr[j] < arr[min] {
                min = j
            }
        }

        tmp := arr[i]
        arr[i] = arr[min]
        arr[min] = tmp
    }
    return arr
}

冒泡排序

原理:遍歷數組,比較並將大的元素與下一個元素交換位置, 在一輪的循環以後,可讓未排序i的最大元素排列到數組右側。在一輪循環中,若是沒有發生元素位置交換,那麼說明數組已是有序的,此時退出排序。數組

時間複雜度分析: $O(N^{2})$

buble_sort

實現:

// 冒泡排序 (bubble sort)
package sorts

func bubbleSort(arr []int) []int {
    swapped := true
    for swapped {
        swapped = false
        for i := 0; i < len(arr)-1; i++ {
            if arr[i+1] < arr[i] {
                arr[i+1], arr[i] = arr[i], arr[i+1]
                swapped = true
            }
        }
    }
    return arr
}

插入排序

原理:數組先當作兩部分,排序序列和未排序序列。排序序列從第一個元素開始,該元素能夠認爲已經被排序。遍歷數組, 每次將掃描到的元素與以前的元素相比較,插入到有序序列的適當位置。

時間複雜度分析:插入排序的時間複雜度取決於數組的排序序列,若是數組已經部分有序了,那麼未排序元素較少,須要的插入次數也就較少,時間複雜度較低。

  • 平均狀況下插入排序須要 $N^{2}/4$ 次比較以及 $N^{2}/4$ 次交換;
  • 最壞的狀況下須要 $N^{2}/2$ 比較以及 $N^{2}/2$ 次交換,最壞的狀況是數組都是未排序序列(倒序)的;
  • 最好的狀況下須要 $ N-1$ 次比較和 0 次交換,最好的狀況就是數組已是排序序列。

insertion_sort

實現

// 插入排序 (insertion sort)
package sorts

func InsertionSort(arr []int) []int {
    for currentIndex := 1; currentIndex < len(arr); currentIndex++ {
        temporary := arr[currentIndex]
        iterator := currentIndex
        for ; iterator > 0 && arr[iterator-1] >= temporary; iterator-- {
            arr[iterator] = arr[iterator-1]
        }
        arr[iterator] = temporary
    }
    return arr
}

希爾排序

原理:希爾排序,也稱遞減增量排序算法,實質是插入排序的優化(分組插入排序)。對於大規模的數組,插入排序很慢,由於它只能交換相鄰的元素位置,每次只能將未排序序列數量減小 1。希爾排序的出現就是爲了解決插入排序的這種侷限性,經過交換不相鄰的元素位置,使每次能夠將未排序序列的減小數量變多。

希爾排序使用插入排序對間隔 d 的序列進行排序。經過不斷減少 d,最後令 d=1,就可使得整個數組是有序的。

時間複雜度:$O(dN*M)$, M 表示已排序序列長度,d 表示間隔, 即 N 的若干倍乘於遞增序列的長度

shell_sort

實現

// 希爾排序 (shell sort)
package sorts

func ShellSort(arr []int) []int {
    for d := int(len(arr) / 2); d > 0; d /= 2 { 
        for i := d; i < len(arr); i++ {
            for j := i; j >= d && arr[j-d] > arr[j]; j -= d {
                arr[j], arr[j-d] = arr[j-d], arr[j]
            }
        }
    }
    return arr
}

歸併排序

原理: 將數組分紅兩個子數組, 分別進行排序,而後再將它們歸併起來(自上而下)。

具體算法描述:先考慮合併兩個有序數組,基本思路是比較兩個數組的最前面的數,誰小就先取誰,取了後相應的指針就日後移一位。而後再比較,直至一個數組爲空,最後把另外一個數組的剩餘部分複製過來便可。

再考慮遞歸分解,基本思路是將數組分解成leftright,若是這兩個數組內部數據是有序的,那麼就能夠用上面合併數組的方法將這兩個數組合並排序。如何讓這兩個數組內部是有序的?能夠二分,直至分解出的小組只含有一個元素時爲止,此時認爲該小組內部已有序。而後合併排序相鄰二個小組便可。

歸併算法是分治法 的一個典型應用, 因此它有兩種實現方法:

  1. 自上而下的遞歸: 每次將數組對半分紅兩個子數組再歸併(分治)
  2. 自下而上的迭代:先歸併子數組,而後成對歸併獲得的子數組

時間複雜度分析: $O(N\log N)$

merge_sort

實現

// 歸併排序 (merge sort)
package sorts

func merge(a []int, b []int) []int {

    var r = make([]int, len(a)+len(b))
    var i = 0
    var j = 0

    for i < len(a) && j < len(b) {

        if a[i] <= b[j] {
            r[i+j] = a[i]
            i++
        } else {
            r[i+j] = b[j]
            j++
        }

    }

    for i < len(a) {
        r[i+j] = a[i]
        i++
    }
    for j < len(b) {
        r[i+j] = b[j]
        j++
    }

    return r

}

// Mergesort 合併兩個數組
func Mergesort(items []int) []int {

    if len(items) < 2 {
        return items

    }

    var middle = len(items) / 2
    var a = Mergesort(items[:middle])
    var b = Mergesort(items[middle:])
    return merge(a, b)

}

快速排序

原理:快速排序也是分治法的一個應用,先隨機拿到一個基準 pivot,經過一趟排序將數組分紅兩個獨立的數組,左子數組小於或等於 pivot,右子數組大於等於 pivot。 而後可在對這兩個子數組遞歸繼續以上排序,最後使整個數組有序。

具體算法描述

  1. 從數組中挑選一個切分元素,稱爲「基準」 (pivot)
  2. 排序數組,把全部比基準值小的元素排到基準前面,全部比基準值大的元素排到基準後面(相同元素不對位置作要求)。這個排序完成後,基準就排在數組的中間位置。這個排序過程稱爲「分區」 (partition)
  3. 遞歸地把小於基準值元素的子數組和大於基準值的子數組排序

空間複雜度分析:快速排序是原地排序,不須要輔助數據,可是遞歸調用須要輔助棧,最好狀況下是遞歸 $\log 2N$ 次,因此空間複雜度爲 $O(\log 2N)$,最壞狀況下是遞歸 $N-1$次,因此空間複雜度是 $O(N)$。

時間複雜度分析

  • 最好的狀況是每次基準都正好將數組對半分,這樣遞歸調用最少,時間複雜度爲 $O(N \log N)$
  • 最壞的狀況是每次分區過程,基準都是從最小元素開始,對應時間複雜度爲 $O(N^{^{2}})$

算法改進

  1. 分區過程當中更合理地選擇基準(pivot)。直接選擇分區的第一個或最後一個元素作 pivot 是不合適的,對於已經排好序,或者接近排好序的狀況,會進入最差狀況,時間複雜度爲 $O(N^{2})$
  2. 由於快速排序在小數組中也會遞歸調用本身,對於小數組,插入排序比快速排序的性能更好,所以在小數組中能夠切換到插入排序
  3. 更快地分區(三向切分快速排序):對於有大量重複元素的數組,能夠將數組切分爲三部分,分別對應小於 pivot、等於 pivot 和大於 pivot 切分元素

quick_sort

實現

// 三向切分快速排序 (quick sort)
package sorts

import (
    "math/rand"
)

func QuickSort(arr []int) []int {

    if len(arr) <= 1 {
        return arr
    }

    pivot := arr[rand.Intn(len(arr))]

    lowPart := make([]int, 0, len(arr))
    highPart := make([]int, 0, len(arr))
    middlePart := make([]int, 0, len(arr))

    for _, item := range arr {
        switch {
        case item < pivot:
            lowPart = append(lowPart, item)
        case item == pivot:
            middlePart = append(middlePart, item)
        case item > pivot:
            highPart = append(highPart, item)
        }
    }

    lowPart = QuickSort(lowPart)
    highPart = QuickSort(highPart)

    lowPart = append(lowPart, middlePart...)
    lowPart = append(lowPart, highPart...)

    return lowPart
}

堆排序

原理:堆排序是利用「堆積」(heap)這種數據結構的一種排序算法。由於堆是一個近似徹底二叉樹結構,知足子節點的鍵值或索引小於(或大於)它的父節點。

具體算法描述

  1. 將待排序數組構建成大根堆,這個堆爲初始的無序區
  2. 將堆頂元素 $R_{1}$ 與最後一個元素 $R_{n}$ 交換,此時獲得新的無序區($R_{1},R_{2},...R_{n-1}$)和新的有序區($R_{n}$),而且知足 $R_{1,2,...n-1}<= R_{n}$
  3. 因爲交換後新的堆頂 $R_{1}$可能違反堆的性質,須要對當前無序區調整爲新堆,而後再次將 $R_{1}$與無序區最後一個元素交換,獲得新的無序區 $R_{1},R_{2}...R_{n-2}$ 和新的有序區$R_{n-1},R_{n}$。不斷重複此過程直到有序區的元素個數爲$n-1$,則整個排序過程完成

時間複雜度分析:一個堆的高度爲 $\log N$,所以在堆中插入元素和刪除最大元素的時間複雜度爲 $O(\log N)$。堆排序會對 N 個節點進行下沉操做,由於時間複雜度爲 $O(N \log N)$

heap_sort

實現

// 堆排序 (heap sort)
package sorts

type maxHeap struct {
    slice    []int
    heapSize int
}

func buildMaxHeap(slice []int) maxHeap {
    h := maxHeap{slice: slice, heapSize: len(slice)}
    for i := len(slice) / 2; i >= 0; i-- {
        h.MaxHeapify(i)
    }
    return h
}

func (h maxHeap) MaxHeapify(i int) {
    l, r := 2*i+1, 2*i+2
    max := i

    if l < h.size() && h.slice[l] > h.slice[max] {
        max = l
    }
    if r < h.size() && h.slice[r] > h.slice[max] {
        max = r
    }
    if max != i {
        h.slice[i], h.slice[max] = h.slice[max], h.slice[i]
        h.MaxHeapify(max)
    }
}

func (h maxHeap) size() int { return h.heapSize } 

func HeapSort(slice []int) []int {
    h := buildMaxHeap(slice)
    for i := len(h.slice) - 1; i >= 1; i-- {
        h.slice[0], h.slice[i] = h.slice[i], h.slice[0]
        h.heapSize--
        h.MaxHeapify(0)
    }
    return h.slice
}

算法複雜度比較

下面是各排序算法的複雜度和穩定性比較:

排序算法 時間複雜度(平均) 時間複雜度(最好) 時間複雜度(最壞) 空間複雜度 穩定性 備註
選擇排序 $O(N^{2})$ $O(N^{2})$ $O(N^{2})$ $O(1)$ 不穩定
冒泡排序 $O(N^{2})$ $O(N)$ $O(N^{2})$ $O(1)$ 穩定
插入排序 $O(N^{2})$ $O(N)$ $O(N^{2})$ $O(1)$ 穩定 時間複雜度和初始順序有關
希爾排序 $O(N^{1.3})$ $O(N)$ $O(N^{2})$ $O(1)$ 不穩定 改進版插入排序
歸併排序 $O(N \log N)$ $O(N \log N)$ $O(N \log N)$ $O(N)$ 穩定
快速排序 $O(N \log N)$ $O(N \log N)$ $O(N^{2})$ $O(N \log N)$ 不穩定
堆排序 $O(N \log N)$ $O(N \log N)$ $O(N \log N)$ $O(1)$ 不穩定 沒法利用局部性原理

注:

  • 穩定:若是 a 本來在 b 前面,而 a=b,排序以後 a 仍然在 b 的前面。
  • 不穩定:若是 a 本來在 b 的前面,而 a=b,排序以後 a 可能會出如今 b 的後面。

對比這裏排序的時間複雜度,歸併排序、快速排序和堆排序的平均時間複雜度都是 $O(N \log N)$。可是再比較最壞的狀況, 能夠看到堆排序的下界也是 $O(N \log N)$,而快排最壞的時間複雜度是 $O(N^{2})$。 你可能會問,按分析結果來講,堆排序應該是實際使用的更好選擇,但爲何業界的排序實現更可能是快速排序?

實際上在算法分析中,大 $O$ 的做用是給出一個規模的下界,而不是增加數量的下界。所以,算法複雜度同樣只是說明隨着數據量的增長,算法時間代價增加的趨勢相同,並非執行的時間就同樣,這裏面有不少常量參數的差異,好比在公式裏各個排序算法的前面都省略了一個$c$,這個$c$ 對於堆排序來講是100,可能對於快速排序來講就是10,但由於是常數級因此不影響大 $O$。

這裏有一份平均排序時間的 Benchmark 測試數據(數據集是隨機整數,時間單位 s):

數據規模 快速排序 歸併排序 希爾排序 堆排序
1000 w 0.75 1.22 1.77 3.57
5000 w 3.78 6.29 9.48 26.54
1億 7.65 13.06 18.79 61.31

由於堆排序每次取一個最大值和堆底部的數據交換,從新篩選堆,把堆頂的X調整到位,有很大多是依舊調整到堆的底部(堆的底部X顯然是比較小的數,纔會在底部),而後再次和堆頂最大值交換,再調整下來,能夠說堆排序作了許多無用功。

總結起來就是,快排的最壞時間雖然複雜度高,可是在統計意義上,這種數據出現的機率極小,而堆排序過程裏的交換跟快排過程裏的交換雖然都是常量時間,可是常量時間差不少。

Go 標準庫排序源碼分析

梳理完最經常使用的7種排序算法後,咱們繼續來看下 Go 在標準庫裏是怎麼作的排序實現。

標準庫的 sort 包的目錄樹以下(以 Go 1.15.5爲例):

$ tree . 
.
├── example_interface_test.go // 提供對 []struct 排序的 example
├── example_keys_test.go // 根據 struct 裏對某一字段的自定義比較,來對 []struct 排序的 example 
├── example_multi_test.go // 根據用戶定義好的 less 方法作排序的 example
├── example_search_test.go // sort.Search 提供對排序數組二分查找某一元素的 example
├── example_test.go // 基本的各類數組排序的 example
├── example_wrapper_test.go // 對 sort.Interface 接口的實現 (封裝),排序的 example
├── export_test.go
├── genzfunc.go
├── search.go // 二分查找的實現
├── search_test.go
├── slice.go
├── slice_go113.go
├── slice_go14.go
├── slice_go18.go
├── sort.go // 主要代碼,提供對 slice 和自定義集合的排序實現
├── sort_test.go
└── zfuncversion.go

其中帶有 example_* 前綴的文件是 sort 包的示例代碼,有官方 example 來講明排序的使用方法。頗有必要看一遍,能夠理解 sort 包怎麼使用,和在一些相對複雜場景下如何排序。

排序的主要代碼在 sort.go 這個文件裏。實現的排序算法有: 插入排序(insertionSort)、堆排序(heapSort)、快速排序(quickSort)、希爾排序(ShellSort)和歸併排序(SymMerge)。

sort 包根據穩定性,將排序方法分爲兩類:不穩定排序和穩定排序

不穩定排序

不穩定排序入口函數是 Sort(data interface),爲支持任意元素類型的 slice 的排序,sort 包定義了一個 Interface 接口和接受該接口參數類型的 Sort 函數:

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
    n := data.Len()
    quickSort(data, 0, n, maxDepth(n))
}

只要排序數組的元素類型實現了 sort.Interface , 就能夠經過 sort.Sort(data)進行排序。其中 maxDepth(n) 是快排遞歸的最大深度,也是快排切換堆排的閾值,它的實現:

// maxDepth returns a threshold at which quicksort should switch
// to heapsort. It returns 2*ceil(lg(n+1)).
func maxDepth(n int) int {
    var depth int
    for i := n; i > 0; i >>= 1 {
        depth++
    }
    return depth * 2
}

須要注意的一點是, sort.Sort 調用的 quickSort 排序函數,並非最多見的快排(參考本文 3.6 小節), quickSort的總體框架比較複雜,流程以下:

func quickSort(data Interface, a, b, maxDepth int) {
    // a是第一個索引,b 是最後一個索引。若是 slice 長度大於 12,會一週走下面排序循環
    for b-a > 12 {
        // 若是遞歸到了最大深度, 就使用堆排序
        if maxDepth == 0 {
            heapSort(data, a, b)
            return
        }
        // 循環一次, 最大深度 -1, 至關於又深刻(遞歸)了一層
        maxDepth--
        // 這是使用的是 三向切分快速排序,經過 doPivot 進行快排的分區
        // doPivot 的實現比較複雜,a 是數據集的左邊, b 是數據集的右邊,
        // 它取一點爲軸,把不大於中位數的元素放左邊,大於軸的元素放右邊,
        // 返回小於中位數部分數據的最後一個下標,以及大於軸部分數據的第一個下標。
        // 下標位置 a...mlo,pivot,mhi...b
        // data[a...mlo] <= data[pivot]
        // data[mhi...b] > data[pivot]
        mlo, mhi := doPivot(data, a, b)
        // 避免較大規模的子問題遞歸調用,保證棧深度最大爲 maxDepth
        // 解釋:由於循環確定比遞歸調用節省時間,可是兩個子問題只能一個進行循環,另外一個只能用遞歸。這裏是把較小規模的子問題進行遞歸,較大規模子問題進行循環。
        if mlo-a < b-mhi {
            quickSort(data, a, mlo, maxDepth)
            a = mhi // i.e., quickSort(data, mhi, b)
        } else {
            quickSort(data, mhi, b, maxDepth)
            b = mlo // i.e., quickSort(data, a, mlo)
        }
    }

    // 若是元素的個數小於 12 個(不管是遞歸的仍是首次進入), 就先使用希爾排序,間隔 d=6
    if b-a > 1 {
        // Do ShellSort pass with gap 6
        // It could be written in this simplified form cause b-a <= 12
        for i := a + 6; i < b; i++ {
            if data.Less(i, i-6) {
                data.Swap(i, i-6)
            }
        }
        insertionSort(data, a, b)
    }
}

這裏 insertionSort 的和3.3節實現的插排的實現是同樣的; heapSort 這裏是構建最大堆,經過 siftDown 來對 heap 進行調整,維護堆的性質:

// siftDown implements the heap property on data[lo, hi).
// first is an offset into the array where the root of the heap lies.
func siftDown(data Interface, lo, hi, first int) {
    root := lo
    for {
        child := 2*root + 1
        if child >= hi {
            break
        }
        if child+1 < hi && data.Less(first+child, first+child+1) {
            child++
        }
        if !data.Less(first+root, first+child) {
            return
        }
        data.Swap(first+root, first+child)
        root = child
    }
}

func heapSort(data Interface, a, b int) {
    first := a
    lo := 0
    hi := b - a

    // Build heap with greatest element at top.
    for i := (hi - 1) / 2; i >= 0; i-- {
        siftDown(data, i, hi, first)
    }

    // Pop elements, largest first, into end of data.
    for i := hi - 1; i >= 0; i-- {
        data.Swap(first, first+i)
        siftDown(data, lo, i, first)
    }
}

在上面快速排序的原理咱們有提到過:若是每次分區過程,基準(pivot)都是從最小元素開始,那麼對應時間複雜度爲$O(N^{^{2}})$ , 這是快排最差的排序場景。爲避免這種狀況,quickSort 裏的 doPivot 選取了兩個基準,進行三向切分,提升快速排序的效率:doPivot 在切分以前,先使用 medianOfThree 函數選擇一個確定不是最大和最小的值做爲軸,放在了切片首位。而後把不小於 data[pivot] 的數據放在了 $[lo, b)$ 區間,把大於 data[pivot] 的數據放在了 $(c, hi-1]$ 區間(其中 data[hi-1] >= data[pivot])。即 slice 會被切分紅三個區間:

$$ \left\{\begin{matrix} data[lo, b-1) \\ data[b-1, c) \\ data[c, hi) \end{matrix}\right.$$

doPivot的實現以下:

// Quicksort, loosely following Bentley and McIlroy,
// ``Engineering a Sort Function,'' SP&E November 1993.

// medianOfThree moves the median of the three values data[m0], data[m1], data[m2] into data[m1].
func medianOfThree(data Interface, m1, m0, m2 int) {
    // sort 3 elements
    if data.Less(m1, m0) {
        data.Swap(m1, m0)
    }
    // data[m0] <= data[m1]
    if data.Less(m2, m1) {
        data.Swap(m2, m1)
        // data[m0] <= data[m2] && data[m1] < data[m2]
        if data.Less(m1, m0) {
            data.Swap(m1, m0)
        }
    }
    // now data[m0] <= data[m1] <= data[m2]
}

func doPivot(data Interface, lo, hi int) (midlo, midhi int) {
    m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow.
    if hi-lo > 40 {
        // Tukey's ``Ninther,'' median of three medians of three.
        s := (hi - lo) / 8
        medianOfThree(data, lo, lo+s, lo+2*s)
        medianOfThree(data, m, m-s, m+s)
        medianOfThree(data, hi-1, hi-1-s, hi-1-2*s)
    }
    medianOfThree(data, lo, m, hi-1)

    // Invariants are:
    //    data[lo] = pivot (set up by ChoosePivot)
    //    data[lo < i < a] < pivot
    //    data[a <= i < b] <= pivot
    //    data[b <= i < c] unexamined
    //    data[c <= i < hi-1] > pivot
    //    data[hi-1] >= pivot
    pivot := lo
    a, c := lo+1, hi-1

    for ; a < c && data.Less(a, pivot); a++ {
    }
    b := a
    for {
        for ; b < c && !data.Less(pivot, b); b++ { // data[b] <= pivot
        }
        for ; b < c && data.Less(pivot, c-1); c-- { // data[c-1] > pivot
        }
        if b >= c {
            break
        }
        // data[b] > pivot; data[c-1] <= pivot
        data.Swap(b, c-1)
        b++
        c--
    }
    // If hi-c<3 then there are duplicates (by property of median of nine).
    // Let's be a bit more conservative, and set border to 5.
    protect := hi-c < 5
    if !protect && hi-c < (hi-lo)/4 {
        // Lets test some points for equality to pivot
        dups := 0
        if !data.Less(pivot, hi-1) { // data[hi-1] = pivot
            data.Swap(c, hi-1)
            c++
            dups++
        }
        if !data.Less(b-1, pivot) { // data[b-1] = pivot
            b--
            dups++
        }
        // m-lo = (hi-lo)/2 > 6
        // b-lo > (hi-lo)*3/4-1 > 8
        // ==> m < b ==> data[m] <= pivot
        if !data.Less(m, pivot) { // data[m] = pivot
            data.Swap(m, b-1)
            b--
            dups++
        }
        // if at least 2 points are equal to pivot, assume skewed distribution
        protect = dups > 1
    }
    if protect {
        // Protect against a lot of duplicates
        // Add invariant:
        //    data[a <= i < b] unexamined
        //    data[b <= i < c] = pivot
        for {
            for ; a < b && !data.Less(b-1, pivot); b-- { // data[b] == pivot
            }
            for ; a < b && data.Less(a, pivot); a++ { // data[a] < pivot
            }
            if a >= b {
                break
            }
            // data[a] == pivot; data[b-1] < pivot
            data.Swap(a, b-1)
            a++
            b--
        }
    }
    // Swap pivot into middle
    data.Swap(pivot, b-1)
    return b - 1, c
}

穩定排序

sort 包中使用的穩定排序算法爲 symMerge, 這裏用到的歸併排序算法是一種原址排序算法:首先,它把 slice 按照每 blockSize=20 個元素爲一個 slice,進行插排;循環合併相鄰的兩個 block,每次循環 blockSize 擴大二倍,直到 blockSize > n 爲止。

func Stable(data Interface) {
    stable(data, data.Len())
}

func stable(data Interface, n int) {
    blockSize := 20 // 初始 blockSize 設置爲 20
    a, b := 0, blockSize
    // 對每一個塊(以及剩餘不足blockSize的一個塊)進行查詢排序
    for b <= n {
        insertionSort(data, a, b)
        a = b
        b += blockSize
    }
    insertionSort(data, a, n)

    for blockSize < n {
        a, b = 0, 2*blockSize
        // 每兩個 blockSize 進行合併
        for b <= n {
            symMerge(data, a, a+blockSize, b)
            a = b
            b += 2 * blockSize
        }
        // 剩餘一個多 blockSize 進行合併
        if m := a + blockSize; m < n {
            symMerge(data, a, m, n)
        }
        blockSize *= 2
    }
}

// SymMerge merges the two sorted subsequences data[a:m] and data[m:b] using
// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum
// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz
// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in
// Computer Science, pages 714-723. Springer, 2004.
//
// Let M = m-a and N = b-n. Wolog M < N.
// The recursion depth is bound by ceil(log(N+M)).
// The algorithm needs O(M*log(N/M + 1)) calls to data.Less.
// The algorithm needs O((M+N)*log(M)) calls to data.Swap.
//
// The paper gives O((M+N)*log(M)) as the number of assignments assuming a
// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation
// in the paper carries through for Swap operations, especially as the block
// swapping rotate uses only O(M+N) Swaps.
//
// symMerge assumes non-degenerate arguments: a < m && m < b.
// Having the caller check this condition eliminates many leaf recursion calls,
// which improves performance.
func symMerge(data Interface, a, m, b int) {
    // Avoid unnecessary recursions of symMerge
    // by direct insertion of data[a] into data[m:b]
    // if data[a:m] only contains one element.
    if m-a == 1 {
        // Use binary search to find the lowest index i
        // such that data[i] >= data[a] for m <= i < b.
        // Exit the search loop with i == b in case no such index exists.
        i := m
        j := b
        for i < j {
            h := int(uint(i+j) >> 1)
            if data.Less(h, a) {
                i = h + 1
            } else {
                j = h
            }
        }
        // Swap values until data[a] reaches the position before i.
        for k := a; k < i-1; k++ {
            data.Swap(k, k+1)
        }
        return
    }

    // Avoid unnecessary recursions of symMerge
    // by direct insertion of data[m] into data[a:m]
    // if data[m:b] only contains one element.
    if b-m == 1 {
        // Use binary search to find the lowest index i
        // such that data[i] > data[m] for a <= i < m.
        // Exit the search loop with i == m in case no such index exists.
        i := a
        j := m
        for i < j {
            h := int(uint(i+j) >> 1)
            if !data.Less(m, h) {
                i = h + 1
            } else {
                j = h
            }
        }
        // Swap values until data[m] reaches the position i.
        for k := m; k > i; k-- {
            data.Swap(k, k-1)
        }
        return
    }

    mid := int(uint(a+b) >> 1)
    n := mid + m
    var start, r int
    if m > mid {
        start = n - b
        r = mid
    } else {
        start = a
        r = m
    }
    p := n - 1

    for start < r {
        c := int(uint(start+r) >> 1)
        if !data.Less(p-c, c) {
            start = c + 1
        } else {
            r = c
        }
    }

    end := n - start
    if start < m && m < end {
        rotate(data, start, m, end)
    }
    if a < start && start < mid {
        symMerge(data, a, start, mid)
    }
    if mid < end && end < b {
        symMerge(data, mid, end, b)
    }
}

// Rotate two consecutive blocks u = data[a:m] and v = data[m:b] in data:
// Data of the form 'x u v y' is changed to 'x v u y'.
// Rotate performs at most b-a many calls to data.Swap.
// Rotate assumes non-degenerate arguments: a < m && m < b.
func rotate(data Interface, a, m, b int) {
    i := m - a
    j := b - m

    for i != j {
        if i > j {
            swapRange(data, m-i, m, j)
            i -= j
        } else {
            swapRange(data, m-i, m+j-i, i)
            j -= i
        }
    }
    // i == j
    swapRange(data, m-i, m, i)
}

以上是穩定排序方法 Stable的所有代碼。

排序 example

爲應用 sort 包裏排序函數 Sort不穩定排序),咱們須要讓被排序的 slice 類型實現 sort.Interface接口,以整形切片爲例:

type IntSlice []int

func (p IntSlice) Len() int  { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

func main() {
 sl := IntSlice([]int{89, 14, 8, 9, 17, 56, 95, 3})
 fmt.Println(sl) // [89 14 8 9 17 56 95 3]
 sort.Sort(sl)
 fmt.Println(sl) // [3 8 9 14 17 56 89 95]
}

總結

本文主要詳細介紹了咱們常見的7種排序算法的原理,實現和時間複雜度分析,並閱讀 Go 源碼裏 sort 包的實現,分析官方如何經過將以上排序算法進行組合來提升排序性能,完成生產環境的排序實踐。

參考

相關文章
相關標籤/搜索