【go源碼分析】strings.go 裏的那些騷操做

go version go1.11 darwin/amd64
/src/strings/strings.go

strings.go 文件中定義了近40個經常使用的字符串操做函數(公共函數)。如下是主要的幾個函數。python

函數 簡介
Index(s, substr string) int 返回 substrs 中第一次出現的位置,不存在返回 -1;採用RabinKarp算法
Split(s, sep string) []string 根據 sep 把字符串 s 進行切分,返回切分後的數組
Join(a []string, sep string) string Split 功能恰好相反
Repeat(s string, count int) string 返回字符串 s 重複 count 次獲得的字符串
Trim(s string, cutset string) string 返回去除首尾存在於 cutset 的字符的切片
Replace(s, old, new string, n int) string 字符串替換
EqualFold(s, t string) bool 判斷兩個字符串表明的文件夾是否相等(忽略大小寫)

以及 ToUpper ToLower TitleTitle 函數把單詞轉換成標題形式,不是ToTitle)。算法

還有一些以上函數派生出的其餘函數。好比:Contains 基本是經過 Index 函數實現的;與 Index 原理一致的 LastIndex 函數;與 Trim 有關的 TrimLeft TrimRight 等。數組

接下來,本文會對 Index Trim Join Repeat Replace 函數進行分析。ide

ps: len 返回的是字符串的字節數,不是字符數。字符數請使用 utf8.RuneCountInString函數

Index: RabinKarp 算法實現

Index(s, substr string) int, 返回 substrs 中第一次出現的位置,不存在返回 -1;採用 RabinKarp算法

Index 函數會先對 substr 的長度 n 進行判斷,對特殊狀況作快速處理。ui

其次,若是長度 n 以及 len(s) 足夠小,則使用BruteForce算法:即暴力匹配,拿 substrs[i:i+n] 進行比較,若是相等,返回 i,其中 i = (from 0 to len(s) - n)...spa

最後,會先嚐試暴力匹配,若是匹配失敗次數超過臨界點,則換成 RabinKarp 算法。code

(爲了方便閱讀,文中不放所有代碼,只展現核心代碼與部分結構代碼)圖片

Index內存

func Index(s, substr string) int {
    n := len(substr)  // len 返回的是字節數
    switch {
    case n == 0:
        return 0
    case n == 1:
        // substr 是單字節字符串,則直接單字節進行比較
        return IndexByte(s, substr[0])
    case n == len(s):
        if substr == s {
            return 0
        }
        return -1
    case n > len(s):
        return -1
    case n <= bytealg.MaxLen:
        // Use brute force when s and substr both are small
        if len(s) <= bytealg.MaxBruteForce {
            return bytealg.IndexString(s, substr)
        }
        
        // 這裏有一大段省略的代碼
        // 循環嘗試 substr == s[i:i+n]
        // 若是失敗次數過多,則使用 bytealg.IndexString(s, substr)
    }
    
    // 這裏有一大段省略的代碼
    // 循環嘗試 substr == s[i:i+n]
    // 若是失敗次數過多,則使用 indexRabinKarp(s[i:], substr)
    
    t := s[:len(s)-n+1]
    for i < len(t) {
        
        // ... 省略代碼
        
        // 若是失敗次數過多,則使用 RabinKarp 算法
        if fails >= 4+i>>4 && i < len(t) {
            // 這裏使用 s[i:] 做爲參數
            // 是由於前面的 s[:i] 都已經嘗試過了
            j := indexRabinKarp(s[i:], substr)
            if j < 0 {
                return -1
            }
            return i + j
        }
    }
    
    return -1
}

在看 indexRabinKarp 函數以前,咱們先了解一下 RabinKarp 算法。

RobinKarp算法是由 Robin 和 Karp 提出的字符串匹配算法。該算法在實際應用中有較好的表現。

算法的核心步驟:

  • const primeRK = 16777619 // 大素數
  • substr 構造 hash 值。 n = len(substr)hash = (substr[0]*pow(primeRK, n-1) + substr[1]*pow(primeRK, n-2) + ... + substr[n-1]*pow(primeRK, 0)) % anotherBiggerPrime
  • s 的每 n 個子串按照相同邏輯構造 hash 值,判斷與 substrhash 是否相等;若是 hash 相等,則比較子串是否真的與 substr 相等
  • 重複第三步,直到找到,或者未找到。

ps:

  • 該算法之因此快,是由於 s[i+1, i+n+1]hash 值能夠由 s[i, i+n]hash 值計算出。即h(i+1) = ((h(i) - s[i] * pow(primeRK, n-1)) * primeRK + s[i+n+1]) % anotherBiggerPrime
  • 另外,go 計算 hash 時並無 % anotherBiggerPrime,而是定義了 hashuint32 類型,利用整型溢出實現了對 2**32 取模的效果。(通常來講是對另外一個大素數取模,顯然這裏不是,不過畢竟這麼大的數也沒啥大影響)

該算法預處理時間爲 O(n)nlen(substr),運行最壞時間爲 O((n-m+1)m)mlen(s)。最壞狀況爲每一個子串的 hash 都與 substr 的同樣。在平均狀況下,運行時間仍是很好的。

除了 RabinKarp 算法外,還要一些其餘的字符串匹配算法。《算法》導論中介紹了另外兩種優秀的算法,分別是 有限自動機Knuth-Morris-Pratt 算法(即 KMP 算法),這兩個算法的運行時間都爲 O(m)

下面是 indexRabinKarp 函數

indexRabinKarp

func indexRabinKarp(s, substr string) int {
    // Rabin-Karp search
    // hashss 是 substr 根據上述方法計算出的 hash 值
    // pow 是 primeRK 的 len(substr) 次冪
    hashss, pow := hashStr(substr)
    n := len(substr)

    // 計算 s[:n] 的 hash 值
    var h uint32
    for i := 0; i < n; i++ {
        h = h*primeRK + uint32(s[i])
    }
    if h == hashss && s[:n] == substr {
        return 0
    }

    for i := n; i < len(s); {
        // 計算下一個子串的 hash 值
        h *= primeRK
        h += uint32(s[i])
        h -= pow * uint32(s[i-n])
        i++
        
        // 若是 hash 相等 且子串相等,則返回對應下標
        if h == hashss && s[i-n:i] == substr {
            return i - n
        }
    }
    return -1
}

hashStr 函數跟計算 s[:n] 的 邏輯一致。不過不得不提一下 pow 的計算方法。

hashStr

func hashStr(sep string) (uint32, uint32) {
    hash := uint32(0)
    for i := 0; i < len(sep); i++ {
        hash = hash*primeRK + uint32(sep[i])
    }
    
    // 下面我用 python 的乘方元素符 ** 表明乘方
    // 咱們已 len(sep) 爲 6 爲例來看此函數
    // 6 的二進制是 110
    // 每次循環,pow 和 sq 分別爲
    // i: 110  pow: 1  sq: rk
    // i: 11   pow: 1  sq: rk ** 2
    // i: 1    pow: 1 * (rk ** 2)  sq: rk ** 4
    // i: 0    pow: 1* (rk ** 2) * (rk ** 4)  sq: rk ** 8
    // pow: 1* (rk ** 2) * (rk ** 4) = 1 * (rk ** 6) 便是 pow(rk, 6)
    var pow, sq uint32 = 1, primeRK
    for i := len(sep); i > 0; i >>= 1 {
        if i&1 != 0 {
            pow *= sq
        }
        sq *= sq
    }
    return hash, pow
}

以上是 Index 函數的實現邏輯。

Trim: 出神入化的位操做

Trim(s string, cutset string) string 返回去除首尾存在於 cutset 的字符的切片。
執行 fmt.Println(strings.Trim("hello world", "hld"))
輸出 ello wor

Trim的本質邏輯也比較簡單:

  • 根據 cutset 構造一個函數,該函數簽名爲 func(rune) bool,接受一個 rune類型的值,返回該值是否在 cutset
  • 而後調用 TrimLeft TrimRight;這兩個函數調用了 indexFunc,其邏輯也比較簡單,再也不贅述

函數 makeCutsetFunc(cutset string) func(rune) bool 就是剛纔提到的構造 判斷 rune 值是否在 cutset 中的函數 的函數。

makeCutsetFunc

func makeCutsetFunc(cutset string) func(rune) bool {
    // 若是 cutset 是單個字符,則直接返回一個簡單函數,
    // 該函數判斷入參 r 是否與 cutset[0] 相等
    if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {
        return func(r rune) bool {
            return r == rune(cutset[0])
        }
    }

    // 若是 cutset 全是 ASCII 碼
    // 則使用構造的 as (asciiSet類型)判斷 rune 是否在 cutset 中
    if as, isASCII := makeASCIISet(cutset); isASCII {
        return func(r rune) bool {
            return r < utf8.RuneSelf && as.contains(byte(r))
        }
    }

    // 調用 IndexRune 方法判斷 r 是否在 cutset 中
    // IndexRune 其實就是 Index 的變種
    return func(r rune) bool { return IndexRune(cutset, r) >= 0 }
}

其中,最有意思的要數 makeASCIISet 函數,該函數用了一個 [8]uint32 數組實現了 128 個 ASCII 碼的 hash 表。

asciiSet

// asciiSet 一共 32 個字節,一共 256 位,
// 其中低 128 位分別表明了 128 個 ascii 碼
type asciiSet [8]uint32

// makeASCIISet creates a set of ASCII characters and reports whether all
// characters in chars are ASCII.
func makeASCIISet(chars string) (as asciiSet, ok bool) {
    for i := 0; i < len(chars); i++ {
        c := chars[i]
        // const utf8.RuneSelf = 0x80
        // 小於 utf8.RuneSelf 的值是 ASCII 碼
        // 大於 utf8.RuneSelf 的值是其餘 utf8 字符的部分
        if c >= utf8.RuneSelf {
            return as, false
        }
        
        // ASCII 的範圍是 0000 0000 - 0111 1111
        // c >> 5 的範圍是 000 - 011,即最大爲 3
        // 31 的二進制是 0001 1111
        // 1 << uint(c&31) 的結果恰好也在 uint 範圍內
        
        as[c>>5] |= 1 << uint(c&31)
    }
    return as, true
}

// contains reports whether c is inside the set.
// 爲了兼容入參 c 爲 byte 類型, c >> 5 < 8
// 因此 asciiSet 類型爲 [8]uint32,數組長度爲 8
// 不然若是隻考慮 128 個 ascii 碼的話,[4]uint32 就夠了
func (as *asciiSet) contains(c byte) bool {
    return (as[c>>5] & (1 << uint(c&31))) != 0
}

以上是 Trim 函數及其位操做。

Join Repeat Replace 看字符串如何生成

這三個函數的邏輯都很簡單,再也不贅述。

頻繁申請內存是很耗費時間的,因此在生成某個字符串時,若是可以預知字符串的長度,就能直接申請對應長度的內存,而後調用 copy(dst, src []Type) int 函數把字符複製到對應位置,最後把 []byte 強轉成字符串類型便可。

Join

func Join(a []string, sep string) string {
    // 省略了部分特殊狀況處理的代碼

    // 計算目標字符串總長度
    n := len(sep) * (len(a) - 1)
    for i := 0; i < len(a); i++ {
        n += len(a[i])
    }
    
    // 申請內存
    b := make([]byte, n)
    bp := copy(b, a[0])
    // 複製內容
    for _, s := range a[1:] {
        bp += copy(b[bp:], sep)
        bp += copy(b[bp:], s)
    }
    // 返回數據
    return string(b)
}

Repeat

func Repeat(s string, count int) string {
    // 特殊狀況處理
    if count < 0 {
        panic("strings: negative Repeat count")
    } else if count > 0 && len(s)*count/count != len(s) {
        panic("strings: Repeat count causes overflow")
    }

    b := make([]byte, len(s)*count)
    bp := copy(b, s)

    // 2倍,4倍,8倍的擴大,直到 bp 不小於目標長度
    for bp < len(b) {
        copy(b[bp:], b[:bp])
        bp *= 2
    }
    return string(b)
}

Replace

func Replace(s, old, new string, n int) string {
    // 省略一下特殊狀況代碼

    // 計算新字符串的長度
    t := make([]byte, len(s)+n*(len(new)-len(old)))
    w := 0
    start := 0
    for i := 0; i < n; i++ {
        j := start
        if len(old) == 0 {
            if i > 0 {
                _, wid := utf8.DecodeRuneInString(s[start:])
                j += wid
            }
        } else {
            j += Index(s[start:], old)
        }
        // 查找舊字符串的位置,複製
        w += copy(t[w:], s[start:j])
        w += copy(t[w:], new)
        start = j + len(old)
    }
    w += copy(t[w:], s[start:])
    return string(t[0:w])
}

以上是關於生成字符串時避免屢次分配內存的高效作法。


圖片描述

相關文章
相關標籤/搜索