Go字符串查找的20種實(shí)現(xiàn)方式
字符串查找(在主串中找模式串第一次或全部出現(xiàn)的位置)是最常見的算法??此浦灰恍?strings.Index,但背后有幾十年的算法演進(jìn)——同一個(gè)任務(wù),樸素算法 O(m×n),KMP 是 O(m+n),Boyer-Moore 在自然文本上接近 O(n/m),Bitap 把位并行做到極致。本文整理 Go 字符串查找的 20 種寫法,按 5 個(gè)策略分類。
為什么有這么多算法?
最簡(jiǎn)單的寫法,把模式串與主串的每個(gè)位置對(duì)齊,逐字節(jié)比較:
func find(text, pattern string) int {
n, m := len(text), len(pattern)
for i := 0; i <= n-m; i++ {
j := 0
for j < m && text[i+j] == pattern[j] {
j++
}
if j == m {
return i
}
}
return -1
}
問題在于"匹配失敗時(shí)把所有已匹配的信息都丟了"——回到 i+1 重頭比,復(fù)雜度退化成 O(m×n)。
優(yōu)化思路:讓"匹配失敗"也帶來信息
- 預(yù)處理模式串:KMP 算 next 數(shù)組、BM 算壞字符表、Sunday 算下一字符位置
- 滑動(dòng)得更遠(yuǎn):BM/Sunday 一次跳很多位,對(duì)長(zhǎng)模式串極快
- 哈希指紋:Rabin-Karp 用滾動(dòng)哈希把"逐字符比較"壓成 O(1)
- 位并行:Bitap 用 uint64 表示"模式的所有前綴是否匹配",一次 CPU 指令推進(jìn)多位
- 多模式合并:AC 自動(dòng)機(jī)把 N 個(gè)模式串合成一個(gè) Trie,掃一遍主串找出所有
- 數(shù)據(jù)結(jié)構(gòu):Trie 用于前綴查詢、后綴數(shù)組用于多次查詢同一文本
Go 的特殊點(diǎn):
- string 是只讀的
[]byte,按字節(jié) O(1) 索引;但 UTF-8 解碼需要for ... range取rune - 標(biāo)準(zhǔn)庫
strings是工業(yè)級(jí)實(shí)現(xiàn)(含 Rabin-Karp 大模式優(yōu)化) bytes包對(duì)[]byte提供同樣 API,處理二進(jìn)制流不必經(jīng) string 轉(zhuǎn)換
推薦方案
| 需求 | 代碼 | 性能 |
|---|---|---|
| 單次查找 | strings.Index(text, pat) | 標(biāo)準(zhǔn)庫實(shí)測(cè)最快 |
| 判斷是否存在 | strings.Contains(text, pat) | 內(nèi)部就是 Index ≥ 0 |
| 復(fù)雜模式 | regexp.MustCompile(...).FindIndex | 內(nèi)部是 RE2,O(n) |
| 字節(jié)流 | bytes.Index(buf, pat) | 避免 string 拷貝 |
| 多模式同時(shí)查 | AC 自動(dòng)機(jī) | O(n + 輸出) |
| 模糊匹配 | Bitap | O(n × k) |
第1類:標(biāo)準(zhǔn)庫 API(方法1-5)
策略原理:Go 的 strings 包對(duì)短模式用樸素 + 字節(jié)掃描優(yōu)化,長(zhǎng)模式用 Rabin-Karp。regexp 用 RE2 引擎,保證線性時(shí)間,無 ReDoS 風(fēng)險(xiǎn)。生產(chǎn)代碼默認(rèn)應(yīng)該先用這些。
// 方法1:strings.Index —— 標(biāo)準(zhǔn)庫最常用
// 短模式走樸素,長(zhǎng)模式自動(dòng)切到 Rabin-Karp(見 Go 源碼 strings/search.go)
func find1(text, pattern string) int {
return strings.Index(text, pattern)
}
// 方法2:strings.Contains —— 只關(guān)心"是否存在"
// 等價(jià)于 Index(...) >= 0,語義更明確
func find2(text, pattern string) bool {
return strings.Contains(text, pattern)
}
// 方法3:HasPrefix 滑動(dòng)窗口
// 不構(gòu)造子串、零分配
func find3(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
if strings.HasPrefix(text[i:], pattern) {
return i
}
}
return -1
}
// 方法4:regexp 正則——支持復(fù)雜模式、大小寫不敏感
// regexp.QuoteMeta 把字符串里的元字符轉(zhuǎn)義,防止把用戶輸入當(dāng)正則解析
func find4(text, pattern string) []int {
re := regexp.MustCompile(regexp.QuoteMeta(pattern))
locs := re.FindAllStringIndex(text, -1)
result := make([]int, 0, len(locs))
for _, loc := range locs {
result = append(result, loc[0])
}
return result
}
// 方法5:bytes.Index —— []byte 流上的查找
// 處理網(wǎng)絡(luò)包、文件讀取緩沖區(qū)時(shí),比 string(buf) 轉(zhuǎn)換零拷貝
func find5(buf, pattern []byte) int {
return bytes.Index(buf, pattern)
}
小心兩個(gè)坑:① Go 的 string 索引是字節(jié)而非 rune,對(duì)中文/emoji 直接 text[i] 拿到的是 UTF-8 的某個(gè)字節(jié),不是字符;② 不可信用戶輸入做正則前必須 regexp.QuoteMeta 轉(zhuǎn)義,否則一個(gè) .* 就會(huì)引發(fā)性能問題(雖然 RE2 沒有 ReDoS,但仍可能慢)。
第2類:樸素與暴力(方法6-9)
策略原理:不依賴任何預(yù)處理,純靠下標(biāo)掃描。每個(gè)位置都重新比較 O(m) 次,最壞復(fù)雜度 O(m×n)。這是所有高級(jí)算法的起點(diǎn)——理解樸素算法的"浪費(fèi)在哪里",才能理解 KMP/BM 的優(yōu)化點(diǎn)。
// 方法6:雙循環(huán)樸素——最經(jīng)典的 Brute Force
// 已是 nativesearch/string_search.go 中的實(shí)現(xiàn)
func find6(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
j := 0
for j < m && text[i+j] == pattern[j] {
j++
}
if j == m {
return i
}
}
return -1
}
// 方法7:[]byte 數(shù)組版——對(duì) string 直接做字節(jié)索引就是這種效果
// Go 的 string 已經(jīng)是只讀 byte 數(shù)組,[]byte(text) 會(huì)觸發(fā)拷貝
// 對(duì) string 直接 text[i] 沒有邊界檢查開銷,與方法6性能一致
func find7(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
match := true
for j := 0; j < m; j++ {
if text[i+j] != pattern[j] {
match = false
break
}
}
if match {
return i
}
}
return -1
}
// 方法8:rune 版本——按 Unicode 字符比較
// 處理中文、emoji 時(shí)必須用 rune 而非 byte
// 注意:rune 切片會(huì)消耗 O(n) 額外空間
func find8(text, pattern string) int {
tr, pr := []rune(text), []rune(pattern)
n, m := len(tr), len(pr)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
j := 0
for j < m && tr[i+j] == pr[j] {
j++
}
if j == m {
// 注意:返回的是 rune 下標(biāo),要算字節(jié)位置需要遍歷前綴
return i
}
}
return -1
}
// 方法9:反向樸素——從右往左對(duì)齊
// BM 算法的雛形:從模式串最右開始比,失敗時(shí)直接跳到下一對(duì)齊
func find9(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
for i := n - m; i >= 0; i-- {
j := m - 1
for j >= 0 && text[i+j] == pattern[j] {
j--
}
if j < 0 {
return i
}
}
return -1
}
樸素算法的最壞案例:text = "AAAAA...AAB"、pattern = "AAAB"——前 m-1 個(gè)字符總是匹配,最后一個(gè)總是失敗。每對(duì)齊一次浪費(fèi) O(m),總浪費(fèi) O(m×n)。
第3類:經(jīng)典高效算法(方法10-14)
策略原理:通過對(duì)模式串的預(yù)處理,讓"失敗時(shí)"不再從頭開始。代價(jià)是 O(m) 或 O(σ) 的預(yù)處理空間。這五種是字符串匹配的"教科書算法"。
// 方法10:KMP 算法——利用已匹配信息避免回溯
// 完整實(shí)現(xiàn)見 KMPsearch/kmp_search.go
// 核心:next[i] = pattern[0..i] 的最長(zhǎng)真前綴也是真后綴的長(zhǎng)度
func find10(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
// 構(gòu)建 next 數(shù)組
next := make([]int, m)
k := 0
for i := 1; i < m; i++ {
for k > 0 && pattern[i] != pattern[k] {
k = next[k-1]
}
if pattern[i] == pattern[k] {
k++
}
next[i] = k
}
// 主串掃描,j 永不回退
j := 0
for i := 0; i < n; i++ {
for j > 0 && text[i] != pattern[j] {
j = next[j-1]
}
if text[i] == pattern[j] {
j++
}
if j == m {
return i - m + 1
}
}
return -1
}
// 方法11:Boyer-Moore(壞字符規(guī)則)
// 從模式串右端開始比,失配按"該字符在模式中最右出現(xiàn)位置"跳躍
// 在自然文本中常常能跳過 m 位(接近線性時(shí)間)
func find11(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
// 壞字符表:256 字節(jié)字符集
var badChar [256]int
for i := range badChar {
badChar[i] = -1
}
for i := 0; i < m; i++ {
badChar[pattern[i]] = i
}
shift := 0
for shift <= n-m {
j := m - 1
for j >= 0 && pattern[j] == text[shift+j] {
j--
}
if j < 0 {
return shift
}
// max(1, ...) 防止跳到負(fù)數(shù)
delta := j - badChar[text[shift+j]]
if delta < 1 {
delta = 1
}
shift += delta
}
return -1
}
// 方法12:Sunday 算法——BM 的簡(jiǎn)化變種
// 關(guān)鍵:失配時(shí)看"窗口右側(cè)外那一格",它將來必然要參與對(duì)齊
// 在英文文本上常常比 BM 還快
func find12(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
// shift[c] = 字符 c 在模式串中最右出現(xiàn)位置,讓其與新窗口對(duì)齊
var shift [256]int
for i := range shift {
shift[i] = m + 1
}
for i := 0; i < m; i++ {
shift[pattern[i]] = m - i
}
i := 0
for i <= n-m {
j := 0
for j < m && text[i+j] == pattern[j] {
j++
}
if j == m {
return i
}
if i+m >= n {
return -1
}
i += shift[text[i+m]]
}
return -1
}
// 方法13:Horspool 算法——BM 的另一種簡(jiǎn)化
// 失配時(shí)只看"主串對(duì)齊到模式串末尾的字符",不計(jì)算 j-badChar[c]
// 代碼更短,性能在多數(shù)場(chǎng)景接近 BM
func find13(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
var shift [256]int
for i := range shift {
shift[i] = m
}
for i := 0; i < m-1; i++ {
shift[pattern[i]] = m - 1 - i
}
i := 0
for i <= n-m {
j := m - 1
for j >= 0 && pattern[j] == text[i+j] {
j--
}
if j < 0 {
return i
}
i += shift[text[i+m-1]]
}
return -1
}
// 方法14:Rabin-Karp(滾動(dòng)哈希)
// 完整實(shí)現(xiàn)見 pattern-matching/ 中類比版本
// 核心:先比較窗口的哈希,相等再確認(rèn)(解決沖突)
// 多模式同時(shí)查找時(shí)極其有用——所有模式串預(yù)算哈希存 map,掃一遍主串
func find14(text, pattern string) int {
n, m := len(text), len(pattern)
if m == 0 {
return 0
}
if m > n {
return -1
}
const (
D = 256
Q = 1000000007 // 大素?cái)?shù)防沖突
)
h := uint64(1)
for i := 0; i < m-1; i++ {
h = (h * D) % Q
}
var p, t uint64
for i := 0; i < m; i++ {
p = (D*p + uint64(pattern[i])) % Q
t = (D*t + uint64(text[i])) % Q
}
for i := 0; i <= n-m; i++ {
if p == t {
j := 0
for j < m && text[i+j] == pattern[j] {
j++
}
if j == m {
return i
}
}
if i < n-m {
// Go 中 uint 減法是模運(yùn)算,不會(huì)成負(fù),但 (t - text[i]*h) 可能溢出
// 用 +Q 保正再取模
t = (D*((t+Q-uint64(text[i])*h%Q)%Q) + uint64(text[i+m])) % Q
}
}
return -1
}
第4類:數(shù)據(jù)結(jié)構(gòu)輔助(方法15-17)
策略原理:當(dāng)"一次查找"變成"反復(fù)多次"時(shí),把成本前置——預(yù)處理一次,之后查詢接近 O(m) 或 O(log n)。
// 方法15:Trie 前綴樹——多個(gè)模式串的前綴查詢
// 適合自動(dòng)補(bǔ)全、敏感詞字典查詢等場(chǎng)景
type Trie struct {
children [26]*Trie // 僅小寫英文,工程里用 map[byte]*Trie
isEnd bool
}
func (t *Trie) Insert(word string) {
cur := t
for i := 0; i < len(word); i++ {
idx := word[i] - 'a'
if cur.children[idx] == nil {
cur.children[idx] = &Trie{}
}
cur = cur.children[idx]
}
cur.isEnd = true
}
func (t *Trie) Contains(word string) bool {
n := t.walk(word)
return n != nil && n.isEnd
}
func (t *Trie) StartsWith(prefix string) bool {
return t.walk(prefix) != nil
}
func (t *Trie) walk(s string) *Trie {
cur := t
for i := 0; i < len(s); i++ {
cur = cur.children[s[i]-'a']
if cur == nil {
return nil
}
}
return cur
}
// 方法16:AC 自動(dòng)機(jī)——Trie + 失敗指針
// 一次掃描主串,找出所有模式串的所有出現(xiàn)
// 是敏感詞過濾、入侵檢測(cè)的標(biāo)準(zhǔn)算法
type ACNode struct {
children map[byte]*ACNode
fail *ACNode
hits []string // 該節(jié)點(diǎn)結(jié)尾的模式串
}
type AhoCorasick struct {
root *ACNode
}
func NewAhoCorasick() *AhoCorasick {
return &AhoCorasick{root: &ACNode{children: map[byte]*ACNode{}}}
}
func (ac *AhoCorasick) Add(p string) {
cur := ac.root
for i := 0; i < len(p); i++ {
c := p[i]
if cur.children[c] == nil {
cur.children[c] = &ACNode{children: map[byte]*ACNode{}}
}
cur = cur.children[c]
}
cur.hits = append(cur.hits, p)
}
func (ac *AhoCorasick) Build() {
queue := []*ACNode{}
for _, child := range ac.root.children {
child.fail = ac.root
queue = append(queue, child)
}
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
for c, v := range u.children {
// v 的失敗指針:從 u.fail 沿 c 走
f := u.fail
for f != nil && f.children[c] == nil {
f = f.fail
}
if f == nil {
v.fail = ac.root
} else {
v.fail = f.children[c]
}
// 累加失敗鏈上的命中
v.hits = append(v.hits, v.fail.hits...)
queue = append(queue, v)
}
}
}
func (ac *AhoCorasick) Search(text string) [][2]int {
var result [][2]int
cur := ac.root
for i := 0; i < len(text); i++ {
c := text[i]
for cur != ac.root && cur.children[c] == nil {
cur = cur.fail
}
if next := cur.children[c]; next != nil {
cur = next
}
for _, hit := range cur.hits {
result = append(result, [2]int{i - len(hit) + 1, len(hit)})
}
}
return result
}
// 方法17:后綴數(shù)組 + 二分查找
// 適合"主串固定、反復(fù)查不同模式"的場(chǎng)景
// 樸素構(gòu)建 O(n2 log n),sort 接口帶來便利
type SuffixArray struct {
text string
sa []int
}
func NewSuffixArray(text string) *SuffixArray {
n := len(text)
sa := make([]int, n)
for i := range sa {
sa[i] = i
}
// 樸素 O(n2 log n);工程上用 SA-IS 實(shí)現(xiàn) O(n)
sort.Slice(sa, func(i, j int) bool {
return text[sa[i]:] < text[sa[j]:]
})
return &SuffixArray{text: text, sa: sa}
}
func (s *SuffixArray) Search(pattern string) int {
lo, hi := 0, len(s.sa)-1
for lo <= hi {
mid := (lo + hi) / 2
suf := s.text[s.sa[mid]:]
if strings.HasPrefix(suf, pattern) {
return s.sa[mid]
}
if suf < pattern {
lo = mid + 1
} else {
hi = mid - 1
}
}
return -1
}
Go 標(biāo)準(zhǔn)庫 index/suffixarray 已提供 SuffixArray,O(n log n) 構(gòu)建,Lookup(pat, n) 直接給出全部匹配位置。生產(chǎn)代碼用它就夠了。
第5類:高級(jí)技巧(方法18-20)
// 方法18:strings.Reader + 流式查找
// 不能一次讀進(jìn)內(nèi)存的大文件可以這么處理
func find18(reader *strings.Reader, pattern string) []int {
// 簡(jiǎn)化:先全部讀出來。真實(shí)場(chǎng)景用 bufio.Reader 分塊 + 上下文保留
data, _ := io.ReadAll(reader)
var result []int
text := string(data)
for i := 0; ; {
pos := strings.Index(text[i:], pattern)
if pos < 0 {
break
}
result = append(result, i+pos)
i += pos + 1
}
return result
}
// 方法19:Z 算法——線性時(shí)間擴(kuò)展前綴
// Z[i] = 以 i 開頭的后綴與原串的最長(zhǎng)公共前綴長(zhǎng)度
// 拼接 "pattern + 分隔符 + text" 跑一遍 Z,所有 Z[i]==m 的位置就是匹配
func find19(text, pattern string) int {
m := len(pattern)
if m == 0 {
return 0
}
s := pattern + "#" + text
z := computeZ(s)
for i := m + 1; i < len(s); i++ {
if z[i] == m {
return i - m - 1
}
}
return -1
}
func computeZ(s string) []int {
n := len(s)
z := make([]int, n)
l, r := 0, 0
for i := 1; i < n; i++ {
if i < r {
if z[i-l] < r-i {
z[i] = z[i-l]
} else {
z[i] = r - i
}
}
for i+z[i] < n && s[z[i]] == s[i+z[i]] {
z[i]++
}
if i+z[i] > r {
l, r = i, i+z[i]
}
}
return z
}
// 方法20:Bitap (Shift-And) ——位并行匹配
// 用 uint64 的每一位表示"模式串前綴 i 是否匹配到當(dāng)前位置"
// 一次位運(yùn)算推進(jìn)所有前綴,CPU 上極快
// 限制:模式串長(zhǎng)度 ≤ 64
// 真正威力:模糊匹配——k 個(gè) uint64 數(shù)組同時(shí)跟蹤 k 個(gè)錯(cuò)誤內(nèi)的所有匹配
func find20(text, pattern string) int {
m := len(pattern)
if m == 0 {
return 0
}
if m > 63 {
panic("Bitap 單 uint64 版只支持 m <= 63")
}
var mask [256]uint64
for i := 0; i < m; i++ {
mask[pattern[i]] |= 1 << i
}
var state uint64
matchBit := uint64(1) << (m - 1)
for i := 0; i < len(text); i++ {
// 推進(jìn)所有前綴進(jìn)度,然后與當(dāng)前字符的 mask 相與
state = ((state << 1) | 1) & mask[text[i]]
if state&matchBit != 0 {
return i - m + 1
}
}
return -1
}
選擇指南
| 類別 | 時(shí)間復(fù)雜度 | 空間 | 主要場(chǎng)景 |
|---|---|---|---|
| 標(biāo)準(zhǔn)庫 API | O(m+n) 實(shí)測(cè) | O(1) | 日常 95% 場(chǎng)景 |
| 樸素與暴力 | O(m×n) | O(1) | 教學(xué)、面試、極短模式 |
| 經(jīng)典算法 | O(m+n) ~ O(n/m) | O(m+σ) | 單次查找的標(biāo)準(zhǔn)方案 |
| 數(shù)據(jù)結(jié)構(gòu) | 預(yù)處理 O(n),查詢 O(m) | O(總規(guī)模) | 海量查詢 |
| 高級(jí)技巧 | O(n) | O(m) ~ O(σ) | 模糊匹配 / 流式 |
實(shí)際項(xiàng)目里怎么選
絕大多數(shù)情況一行就夠:
// 單次查找:標(biāo)準(zhǔn)庫已經(jīng)夠好 pos := strings.Index(text, pattern) // 找全部位置 re := regexp.MustCompile(regexp.QuoteMeta(pattern)) locs := re.FindAllStringIndex(text, -1) // [[start1,end1], [start2,end2], ...]
模式串很長(zhǎng)(≥ 20)且文本是自然語言:
// 標(biāo)準(zhǔn)庫已經(jīng)在長(zhǎng)模式上自動(dòng)切到 Rabin-Karp // 一般不需要自己實(shí)現(xiàn) BM/Sunday pos := strings.Index(text, longPattern)
需要在同一文本上反復(fù)查多個(gè)模式:
// AC 自動(dòng)機(jī)
ac := NewAhoCorasick()
for _, p := range patterns {
ac.Add(p)
}
ac.Build()
hits := ac.Search(text)
需要在同一文本上做大量不相關(guān)查詢:
// 標(biāo)準(zhǔn)庫后綴數(shù)組 import "index/suffixarray" sa := suffixarray.New([]byte(largeText)) positions := sa.Lookup([]byte(pattern), -1) // 返回全部出現(xiàn)位置
需要前綴查詢、自動(dòng)補(bǔ)全:
trie := &Trie{}
for _, w := range dictionary {
trie.Insert(w)
}
exists := trie.Contains("apple")
prefix := trie.StartsWith("app")
多模式匹配的處理
不要循環(huán)調(diào)用 strings.Index:
// ? 反例:N 個(gè)模式 × M 次掃描 = O(N × M × n)
for _, p := range patterns {
if strings.Contains(text, p) {
hit(p)
}
}
正確做法(按規(guī)模選擇):
| 模式數(shù) N | 推薦方案 |
|---|---|
| N ≤ 5,模式短 | 直接循環(huán) strings.Index |
| N ≤ 100 | 多模式 Rabin-Karp + map[uint64]string |
| N 上千 | AC 自動(dòng)機(jī) |
| 海量動(dòng)態(tài)增刪 | AC 自動(dòng)機(jī) + 失敗鏈懶更新 |
正則的 alternation:
// 把多個(gè)模式拼成一個(gè) RE2 正則
// RE2 內(nèi)部會(huì)編譯為 NFA/DFA,效率取決于實(shí)現(xiàn)
parts := make([]string, len(patterns))
for i, p := range patterns {
parts[i] = regexp.QuoteMeta(p)
}
re := regexp.MustCompile(strings.Join(parts, "|"))
locs := re.FindAllStringIndex(text, -1)
大文本與流式查找
文本不能一次性讀進(jìn)內(nèi)存(GB 級(jí)日志、網(wǎng)絡(luò)流)時(shí):
// 關(guān)鍵:跨緩沖區(qū)邊界的匹配會(huì)被切斷,需要保留 m-1 字符的"上下文"
func streamSearch(r io.Reader, pattern string) []int {
m := len(pattern)
buf := make([]byte, 0, 8192+m)
chunk := make([]byte, 8192)
var result []int
var totalOffset int
for {
n, err := r.Read(chunk)
if n > 0 {
buf = append(buf, chunk[:n]...)
// 在 buf 里找匹配
offset := 0
for {
idx := bytes.Index(buf[offset:], []byte(pattern))
if idx < 0 {
break
}
result = append(result, totalOffset+offset+idx)
offset += idx + 1
}
// 保留末尾 m-1 字節(jié),避免跨緩沖區(qū)漏匹配
if len(buf) > m-1 {
drop := len(buf) - (m - 1)
totalOffset += drop
buf = buf[drop:]
}
}
if err != nil {
break
}
}
return result
}
工程里更常見的方式是用 bufio.Scanner 逐行掃,或者直接 mmap 大文件。
字節(jié)、rune 與 Unicode
Go 的 string 是只讀 byte 序列,按字節(jié)索引:
text := "你好world" fmt.Println(len(text)) // 11("你"、"好"各 3 字節(jié),"world" 5 字節(jié)) fmt.Println(text[0]) // 228(第一個(gè)字節(jié),不是字符 '你')
按字符(rune)處理:
// 方式1:轉(zhuǎn) []rune
runes := []rune(text)
fmt.Println(len(runes)) // 7
// 方式2:for ... range
for i, r := range text {
fmt.Printf("byte %d: %c\n", i, r)
}
// 方式3:utf8 包
import "unicode/utf8"
fmt.Println(utf8.RuneCountInString(text)) // 7
大小寫不敏感查找:
// 簡(jiǎn)單粗暴:全部轉(zhuǎn)小寫
strings.Contains(strings.ToLower(text), strings.ToLower(pattern))
// 嚴(yán)格 Unicode 折疊(處理土耳其 i/I、德語 ? 等)
strings.EqualFold(s1, s2) // 僅適用于"全等"判斷
// 正則
re := regexp.MustCompile("(?i)" + regexp.QuoteMeta(pattern))
自定義對(duì)象:在切片中查找子序列
strings.Index 只查 string 子串。在 []T 中查 []T 模式,思路完全一樣:
// 在 []T 中查找 []T 模式(T 必須可比較)
func IndexSeq[T comparable](haystack, needle []T) int {
n, m := len(haystack), len(needle)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
j := 0
for j < m && haystack[i+j] == needle[j] {
j++
}
if j == m {
return i
}
}
return -1
}
KMP/BM 等算法都可以泛化為 [T comparable]。如果元素是不可比較的(含 slice、map 字段的 struct),需要自己提取一個(gè)可比較"鍵"再查。
總結(jié)
工程上的快捷選擇:
- 默認(rèn)用
strings.Index(text, pattern):標(biāo)準(zhǔn)庫已經(jīng)優(yōu)化得很好 - 找全部位置用
regexp.MustCompile(QuoteMeta(p)).FindAllStringIndex - 字節(jié)流上用
bytes.Index,零拷貝 - 多個(gè)模式同時(shí)查,AC 自動(dòng)機(jī)
- 同一主串反復(fù)查不同模式,標(biāo)準(zhǔn)庫
index/suffixarray - 前綴查詢、自動(dòng)補(bǔ)全,Trie
- 模糊匹配,Bitap 或 levenshtein
- Unicode 處理用
unicode/utf8包,不要直接對(duì) string 做字節(jié)索引
核心思路:
- 同一個(gè)問題可以從多個(gè)角度切入——樸素到 KMP 是"利用失敗信息",KMP 到 BM 是"換方向比較",BM 到 Bitap 是"換數(shù)據(jù)表示"
- 選對(duì)算法往往比寫更聰明的代碼更重要——AC 自動(dòng)機(jī)一次掃描勝過 N 次 strings.Index
- O(m×n) 與 O(m+n) 在數(shù)據(jù)變大時(shí)是幾百倍的實(shí)際差距,但常數(shù)也很重要——KMP 不一定比 strings.Index 快
- 不要過度優(yōu)化——能用 strings.Index 就別繞彎
- Go 的標(biāo)準(zhǔn)庫已經(jīng)覆蓋了 80% 的場(chǎng)景,理解算法是為了選對(duì)工具,不是替代標(biāo)準(zhǔn)庫
20 種實(shí)現(xiàn)的本質(zhì)是 4 個(gè)升維:
- 把"匹配失敗"變成信息(樸素 → KMP)
- 把"逐字符比較"變成"批量跳躍"(KMP → BM/Sunday)
- 把"字符比較"變成"哈希/位運(yùn)算"(BM → Rabin-Karp/Bitap)
- 把"一次查詢"變成"多次查詢"(→ Trie/AC/SuffixArray)
理解這 4 個(gè)升維方向,寫出第 21、第 22 種都不在話下。
以上就是Go字符串查找的20種實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于Go字符串查找的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
搭建Go語言的ORM框架Gorm的具體步驟(從Java到go)
很多朋友不知道如何使用Goland軟件,搭建一個(gè)ORM框架GORM,今天小編給大家分享一篇教程關(guān)于搭建Go語言的ORM框架Gorm的具體步驟(從Java到go),感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2022-09-09
Go語言實(shí)現(xiàn)23種設(shè)計(jì)模式的使用
設(shè)計(jì)模式是軟件工程中各種常見問題的經(jīng)典解決方案,,本文主要介紹了Go語言實(shí)現(xiàn)23種設(shè)計(jì)模式的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
詳解如何使用unsafe標(biāo)準(zhǔn)庫突破Golang中的類型限制
在使用c語言編程時(shí),常常因?yàn)轭愋偷膯栴}大傷腦筋,而,golang提供了一些方式用于喜歡hack的用戶,下面我們就來講講如何使用unsafe標(biāo)準(zhǔn)庫突破Golang中的類型限制吧2024-03-03
GO語言中創(chuàng)建切片的三種實(shí)現(xiàn)方式
這篇文章主要介紹了GO語言中創(chuàng)建切片的三種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09

