字符串匹配是leetcode刷题和笔试时经常出现的题型,一般的要求就是给定一个主串S,求模式串P是否为主串的连续子串或者在主串中的位置。
对于这类题其实思路是很确定的 ——— 逐一对比两个字符串的元素,看是否相等。
下面是Go版本暴力遍历的代码思路(没编译过可能有错,只是描述下大概思路)
target := "abcabcd"
pattern := "abcd"
i,j := 0,0
for ;i<len(target) && j<len(pattern) ;{
if target[i] == pattern[j]{
i++
j++
}else{
//不相同,对比下一个子串
i = i-j+1
j = 0
}
}
if j == len(pattern){
//找到子串
}else{
//没找找到子串
}
从实质上讲,对比字符串只能使用这种逐一对比的方式,但是,对于太长的子串,这种匹配方式是非常耗时的O()
如果我们在对比的过程中,可以省略掉一些重复的过程,提前找到并跳过一些一定匹配失败的步骤,提高匹配速度。
比如下图中,对于index为2,3,4,5等元素的对比,其实是可以跳过的,很显然可以看出这几个匹配是会失败的
KMP算法就是找到了这样的规律,跳过一些匹配失败的步骤,来优化匹配过程。
下面来分析一下KMP算法
KMP可行的基础
如果我们用人眼来匹配字符串,当当前元素匹配失败后,我们并不会把子串的起点移动到B处,而是移动到下一个A处。通过这种方式来减少匹配的次数
kmp算法要做的就是把这种方式,抽象成数学公式
- 我们首先假设主串是T,当前对比元素的下标是i;模式串是P,当前对比元素的下表是j
- 由于已经对比到了下标i和j,所以前面的子串必然是相等的。即P[0:j-1] == T[i-j:i-1]
- 如果此时模式串的P[0:j-1]中,存在一个k(有多个则取最大的k),满足P[0:k-1] == P[j-k:j-1]
- 那么 T[i-k:i-1]==P[0:k-1]
- 那么,我们是不是可以直接以主串的i-k位置为起点,i位置开始继续匹配,从而跳过中间的元素
直接讲公式可能不太好理解,结合下面的图理解
根据上图,此时i=5,j=5;P[0:4] = T[0:4] => 即前面的abcab这个子串是两边共有的
同时我们发现 P[0:1] = P[3:4],那么我们可以跳过一些匹配过程,从下图开始匹配,此时 i=5,j=2
这就是KMP的理论基础。
ps.如果前缀的子串完全一致(如aaaab,aaaac那么则k=j-1,即挪一个位置进行比较,和暴力法一样)
如何实现
那么,在我们匹配的过程中,当前元素不匹配了,如何确定要从哪个位置开始重新匹配呢。
其实就是找到这个共同子串最长的 前缀=后缀 的位置
还是这幅图,共同子串abcab有
- 前缀:a,ab,abc,abca,abacb
- 后缀:b,ab,cab,bcab,abcab
所以最长相同前后缀为ab,使用k=2
如果我们提前对模式串做下处理,得到他每一个位置的最长前后缀长度,不就可以很快的知道要从什么位置开始匹配了。这就是KMP算法中next数组的作用
Next数组
我们在匹配字符串之前,先对模式串做一次遍历,计算他的最长相同前后缀(实质上类似自己和自己做了一次匹配)
- 我们定义now为当前前缀最后一个元素的位置,x为后缀最后一个元素的位置
- next数组中,next[x]为模式串P[0:x]中最长相同前后缀的长度
- 遍历过程中,如果当前元素相等,最长前后缀长度可以加1,即next[x]=now+1
- 如果元素不相同,需要缩短最长前后缀,now = next[now-1]进行递归,直到相等或值now=0,
用下面的图来理解不想等的时候,(now =k,x=j)
当P[j] != P[k]时,最长前后缀就得缩短,相当于把前缀后移,如下图,直到找到P[j] == P[k],如果找不到,最长前后缀就是0
pattern := "patternString"
next := make([]int,len(pattern))
now := 0
x := 1
next[0] = 0
for ;x<len(pattern);{
if ( pattern[now]==pattern[x])
{
now ++
next[x]=now
x++
}elif now {
// 不想等且now !=0
now = next[now-1]
}else{
next[x]=0
x++
}
}
有了这个最长前后缀数组,就可以进行字符串匹配了
匹配
target := "abcabcd"
pattern := "abcd"
i,j := 0,0
for ;i<len(target) && j<len(pattern) ;{
if target[i] == pattern[j]{
i++
j++
}else if j==0{
i++
}else{
j = next[j] // 前面建立好的前后缀数组
}
}
if j == len(pattern){
//找到子串
}else{
//没找找到子串
}