歸併排序算法還能怎麼優化?本文給你講清楚

排序算法是一種能將一系列數據按照特定順序進行排列的算法,好比說一個學校的考試分數從高到低排名、一個公司的數據報表從大到小排列,都須要用到排序算法。常見的排序算法有冒泡排序、快速排序、字典排序、歸併排序、堆排序等等,而歸併排序是其中的一種較爲穩定、高效的排序算法,時間複雜度N*log2N。 本文經過Go語言開源社區的歸併排序算法優化案例爲例,講解通用的算法分析、優化方法。git

摘自OptimizeLab: https://github.com/OptimizeLab/docsgithub

做者:zaneChou1 golang


1. Go語言的歸併排序算法

Go語言的sort包對插入排序、歸併排序、快速排序、堆排序作了支持,歸併排序是sort包實現的穩定排序算法。 Go語言基於遞歸實現了歸併排序算法,代碼以下:算法

// 優化前的歸併排序代碼,使用遞歸實現兩個有序子序列data[a:m]和data [m:b]的合併
func symMerge(data Interface, a, m, b int) {
    // 初始化mid,start,r的值,mid表示[a:b]的中位數,start表示[a:m]區間進行數據交換的開始下標,r表示[a:m]區間進行數據交換的最右下標
    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

    // 折半查找對稱比較數據塊[a:m]和數據塊[m:b],找出數據塊[a:m]開始大於數據[m:b]的下標位置start
    for start < r {
        c := int(uint(start+r) >> 1)
        if !data.Less(p-c, c) {
            start = c + 1
        } else {
            r = c
        }
    }

    // 翻轉數據塊[start:m] 和 [m:end], 使得數據塊[start:end]有序
    end := n - start
    if start < m && m < end {
        rotate(data, start, m, end)
    }
    // 調用遞歸函數,排序兩組子數據塊[a:start]和[start:mid],[mid:end]和[end:b]
    if a < start && start < mid {
        symMerge(data, a, start, mid)
    }
    if mid < end && end < b {
        symMerge(data, mid, end, b)
    }
}

2. 場景及問題分析

歸併排序算法的應用場景主要是數據塊有序歸併,好比說有序切片data[0:20]和data[20:40]的合併有序場景,主要處理過程以下:函數

  1. 調用遞歸函數symMerge,初始化切片參數。
  2. 通過折半查找、對稱比較、翻轉等方法使得切片data[0:20]總體小於切片data[20:40]
  3. 再次調用遞歸函數symMerge歸併排序子切片data[0:start]和data[start:20],回到步驟1
  4. 再次調用遞歸函數symMerge歸併排序子切片data[20:end]和data[end:40],回到步驟1
  5. 直到排序完全部的子切片,使得整個切片data[0:40]有序。

從這個常見的應用場景的處理過程當中發現,歸併排序須要不斷調用遞歸函數處理子序列,而這也是歸併排序算法的性能損耗的主要緣由。在切片的歸併排序場景中,若是能避免或減小調用遞歸處理子切片,算法的運行性能就能夠獲得提高。好比說在長度爲1的子切片data[0]和長度爲9的子切片data[1:10]進行歸併排序的場景下,使用插入排序來避免調用遞歸,能夠減小算法的運行耗時,優化算法的性能。性能

3. 優化方案及實現

經過上述對Go語言歸併排序算法的代碼和場景分析,找到了一種優化方法,即在歸併排序中,針對長度爲1的數據塊,利用二分插入排序算法處理數據塊排序,能夠減小函數遞歸的調用,提升算法的性能。 Go語言開源社區也應用了基於二分插入排序的優化方法,提高了歸併排序算法的性能,優化先後的代碼對好比下:image學習

優化代碼以下:測試

// 優化代碼增長了對切片長度的判斷,且當切片長度爲1時,使用二分插入排序直接排序切片返回。
func symMerge(data Interface, a, m, b int) {
    // data[a:m]只有一個元素時,使用二分插入排序找到有序的插入點,插入data[a]到data[m:b]
    // 避免調用遞歸,提升算法性能
    if m-a == 1 {
        i := m
        j := b
        // 二分查找,找到合適的有序插入點
        for i < j {
            h := int(uint(i+j) >> 1)
            if data.Less(h, a) {
                i = h + 1
            } else {
                j = h
            }
        }
        // 插入數據
        for k := a; k < i-1; k++ {
            data.Swap(k, k+1)
        }
        return
    }

    // data[m:b]只有一個元素時,使用二分插入排序找到合適的有序插入點,插入data[m]到data[a:m]
    // 避免調用遞歸,提升算法性能
    if b-m == 1 {
        i := a
        j := m
        // 二分查找,找到合適的有序插入點
        for i < j {
            h := int(uint(i+j) >> 1)
            if !data.Less(m, h) {
                i = h + 1
            } else {
                j = h
            }
        }
        // 插入數據
        for k := m; k > i; k-- {
            data.Swap(k, k-1)
        }
        return
    }

    // 歸併排序的遞歸實現
    ......
}

4. 優化結果

使用Go benchmark測試優化先後的算法性能,再用Go benchstat對比優化先後的性能測試結果,並統計了優化先後遞歸函數的調用次數,整理到以下表格:優化

測試項 測試用例 優化前每操做耗時 time/op 優化後每操做耗時 time/op 耗時對比 優化前遞歸總次數 優化後遞歸總次數 遞歸總次數對比
BenchmarkStableString1K-8 長度1K的字符串切片 302278 ns/op 288879 ns/op -2.71% 1110 790 -28.8%
BenchmarkStableInt1K-8 長度1K的整型切片 144207 ns/op 139911 ns/op -0.96% 689 586 -14.9%
BenchmarkStableInt64K-8 長度64K的整型切片 12291195 ns/op 12119536 ns/op -0.75% 48465 41280 -14.8%
BenchmarkStable1e2-8 長度1e2(100)的結構體切片 135357 ns/op 124875 ns/op -7.17% 1079 807 -25.2%
BenchmarkStable1e4-8 長度1e4(10000)的結構體切片 43507732 ns/op 40183173 ns/op -7.64% 449735 350205 -22.1%
BenchmarkStable1e6-8 長度1e6(1000000)的結構體切片 9005038733 ns/op 8440007994 ns/op -7.69% 79347843 61483110 -22.5%

[注]-8表示函數運行時的GOMAXPROCS值,ns/op表示函數每次執行的平均納秒耗時。ui

上述表格顯示了在字符串切片、整型切片、結構體切片等不一樣數據類型的場景下的性能測試結果,對比分析以下:

  • 對比長度爲1K的字符串和整型切片數據,優化後字符串切片的遞歸次數減小了28.8%,而整型切片的遞歸次數只減小了14.9%,說明優化技術在字符串切片數據集上發揮了更大的做用,性能提高更大;
  • 對比長度爲64K和1K的整型切片數據,優化後兩者的遞歸總次數減小的比例都在14.8%左右,性能提高相近在0.5%-1%之間;
  • 對比長度爲1e2\1e4\1e6的結構體切片數據,優化後三者的遞歸總次數都減小在22%左右,性能提高基本同樣在7%左右。

經過性能測試結果的對比分析,發現優化後的性能提高大小跟測試數據集的關聯性大,算法的遞歸嵌套中出現數據切片長度爲1的狀況越多,優化方法發揮的做用就越大,遞歸次數減小的更多,性能提高也會更大。

5. 總結

Go語言的歸併排序算法優化案例,從一個具體的場景出發分析歸併排序算法存在的性能問題,給出了基於二分插入排序的優化方案,並最終驗證了優化結果,是一個值得學習借鑑的算法優化實踐。

若是對開源或優化技術感興趣,歡迎下方留言或者經過https://github.com/OptimizeLab/docs/issues聯繫咱們。

相關文章
相關標籤/搜索