Golang源碼分析之sort

排序是工程中必不可少的功能,很多編程語言SDK都提供了排序相關的實現。作為軟件工程師,我們在學習各類排序算法的同時,是否有思考過,如何去實現一個工業級的排序算法?如果你是Go語言的作者之一,該如何去實現一種能適應多種情況的排序算法?

Go SDK中排序相關的實現主要在sort/sort.go中,本文主要基于該文件進行相關實現的分析。

首先來看看Go對排序接口的定義,利用Go的interface特性可以輕松實現多種數據類型的排序功能。想要調用sort包的排序功能我們需要實現這個排序接口,排序接口主要定義了三個方法:

  • Len() int: 返回傳入數據的總數
  • Less(i, j int) bool: 返回數組中下標為i的數據是否小于下標為j的數據
  • Swap(i, j int): 表示執行交換數組中下標為i的數據和下標為j的數據
// 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接口的定義后,再來看看sort包對外提供的主要接口Sort,源碼如下:

// 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方法時,該方法會調用一次data.Len(),之后會以O(n*log(n))的時間復雜度調用data.Lessdata.Swap。我們可以看到,Sort內部調用了包私有的quickSort方法,也就是我們熟悉的快排,同時傳了4個參數,學過快排的同學都能理解前三個參數的含義,但是我們還看到了一個陌生的函數調用maxDepth(n),這里的depth究竟代表什么呢?所以先探究一下這個函數,代碼如下:

// 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
}

簡單來說,maxDepth方法返回的深度表示了數據的量級,qiuckSort方法會根據這個量級選擇使用快排還是堆排序,學過堆排序的同學都知道,堆排序的時間復雜度穩定在O(nlogn),有時候比快排還穩定,但是堆排序對數據是跳著訪問的,對CPU緩存不友好。

了解了maxDepth方法以后就可以來看看quickSort的源碼了

func quickSort(data Interface, a, b, maxDepth int) {
    for b-a > 12 { // Use ShellSort for slices <= 12 elements
        if maxDepth == 0 {
            heapSort(data, a, b)
            return
        }
        maxDepth--
        mlo, mhi := doPivot(data, a, b)
        // Avoiding recursion on the larger subproblem guarantees
        // a stack depth of at most lg(b-a).
        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)
        }
    }
    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)
    }
}

這里代碼的實現方式比較好理解,首先對于數組元素大于12個的情況會在快排和堆排之間選擇,除此之外的情況會使用希爾排序(間隔為6)和插入排序進行排序。

包中對于heapSort的實現中規中矩,使用從上往下堆化的方式建堆。這里就不詳細介紹,對于快排的實現方式,有的同學就發現不同了,這里調用了一個尋找分區點的函數doPivot,但是doPivot返回了兩個值(這里就利用了Go中函數可以有多個返回值的特性)。同時這里可以看到返回mlo,mhi以后并沒有繼續遞歸地在左右分區查找,而是做了一個比較,原因也正如注釋所說,由于使用了遞歸的方式實現排序,就必須要考慮到棧溢出的問題,所以對分區的兩半,把數量多的放到下一次循環繼續切分循環,小的直接遞歸。這里也表明了調用quickSort的最高棧深度為log(b-a),也就是log(n)。

接下來可以看看doPivot函數,為什么會返回兩個分區點呢?因為mlo到mhi之間的數已經被確定了位置,這里考慮到取中位數的時候數組出現大量重復的數會影響到排序性能的問題,可以發現Go作者對這種情況的解決方式充滿著智慧。具體代碼如下:

func doPivot(data Interface, lo, hi int) (midlo, midhi int) {
    m := int(uint(lo+hi) >> 1) // 首先用位運算的方式求中間點,防止溢出
    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)

    // 接下來要對數據達成以下劃分結果
    //  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--
    }
        // 如果data[c <= i < hi-1] > pivot,hi-c<3 這表明數據中有重復的數,
        // 這里保守一些,認為hi-c<5 為邊界,如果重復的數較多,
        // 會以直接掃描跳過的方式把pivot左右兩邊的區間縮小
    // 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
}

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • 有句話很有趣:Stay hungry, stay foolish. 個人根據對這句話的理解 以一個有強烈求知欲的小...
    一根薯條閱讀 3,932評論 1 6
  • 1 初級排序算法 排序算法關注的主要是重新排列數組元素,其中每個元素都有一個主鍵。排序算法是將所有元素主鍵按某種方...
    深度沉迷學習閱讀 1,430評論 0 1
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,270評論 0 2
  • Python語言特性 1 Python的函數參數傳遞 看兩個如下例子,分析運行結果: 代碼一: a = 1 def...
    時光清淺03閱讀 501評論 0 0
  • 一休哥馬上就要32個月,從他出生以來我沒有單獨跟他待過完整的一天。 今天是就只有我們兩個人,完完全全的只有我和他。...
    桔子plus閱讀 455評論 0 0