go version go1.11 darwin/amd64
/src/strings/strings.go
strings.go 文件中定義了近40個經常使用的字符串操做函數(公共函數)。如下是主要的幾個函數。python
函數 | 簡介 |
---|---|
Index(s, substr string) int |
返回 substr 在 s 中第一次出現的位置,不存在返回 -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 Title
(Title
函數把單詞轉換成標題形式,不是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
, 返回substr
在s
中第一次出現的位置,不存在返回-1
;採用RabinKarp
算法
Index
函數會先對 substr
的長度 n
進行判斷,對特殊狀況作快速處理。ui
其次,若是長度 n
以及 len(s)
足夠小,則使用BruteForce
算法:即暴力匹配,拿 substr
與 s[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
值,判斷與 substr
的 hash
是否相等;若是 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
,而是定義了 hash
爲 uint32
類型,利用整型溢出實現了對 2**32
取模的效果。(通常來講是對另外一個大素數取模,顯然這裏不是,不過畢竟這麼大的數也沒啥大影響)該算法預處理時間爲 O(n)
,n
爲 len(substr)
,運行最壞時間爲 O((n-m+1)m)
,m
爲 len(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]) }
以上是關於生成字符串時避免屢次分配內存的高效作法。