部分示意图来源于《算法导论》
1. 朴素字符串匹配算法
思路:模板沿着文本滑动,对每个偏移都进行检测
最坏情况下,算法运行时间为
O
(
(
n
−
m
+
1
)
m
)
O((n-m+1)m)
O((n−m+1)m)
2. Rabin-Karp算法
该算法是基于散列的,用的是哈希表。
模板:P[1…m]
文本:T[1…n]
对T的所有m长度的子字符串计算d^{m-1}(利用霍纳法则可以简化),通过取模,减少哈希值的长度。
哈希值
t
s
=
d
m
−
1
t_s= d^{m-1}
ts=dm−1 mod q,q为一个素数,且需满足dq在一个计算机字长内(比如十进制时,d=10,10q需要比 ?小,因此q的值选为13。)
通过比较n-m+1次哈希值:
t
s
=
p
t_s= p
ts=p mod q,再用朴素法来检测有效偏移s是真的有效还是伪命中点。
图例:
算法性能分析:
预处理时间为
θ
(
m
)
\theta(m)
θ(m),最坏情况下,匹配时间是
θ
(
(
n
−
m
+
1
)
∗
m
)
\theta((n-m+1)*m)
θ((n−m+1)∗m),选取的q大于模式p的长度时,匹配时间为
O
(
n
+
m
)
O(n+m)
O(n+m),由于m≤n,算法的平均匹配时间为
O
(
n
)
O(n)
O(n)。
3. KMP算法
用的是前缀表,使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配。
模式P的前缀函数
π
\pi
π包含模式与自身偏移进行匹配的信息。
图例:
步骤:
- 利用双指针求出模式p的前缀表next
- 对原字符串T进行匹配
go 代码
go语言实现kmp算法
package main
import "fmt"
func strStr(t string, p string) int {
if len(p)==0{
return 0
}
next := compute_next(p)
j := 0
for i := 0; i < len(t); i++ {
for t[i] != p[j] { //边界j=0时的处理
if j == 0 {
break
}
j = next[j-1]
}
if t[i] == p[j] {
j++
}
if j == len(p) {
return i-len(p)+1
}
}
return -1
}
func compute_next(p string) []int {
m := len(p)
next := make([]int, m) //next[0]=0
for left, right := 0, 1; right < m; right++ {
for p[left] != p[right] {
//边界left=0时的处理
if left == 0 {
break
}
left = next[left-1]
}
if p[left] == p[right] {
left++
}
next[right] = left
}
return next
}
func main() {
haystack := "aaaaa"
needle := "bba"
next := strstr(haystack, needle)
fmt.Println(next)
}