持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情
引言
KMP 算法是 D.E.Knuth、J.H.Morris和V.R.Pratt 发明的。它要解决的问题就是定位子串substr在字符串str中第一次出现的位置,不存在则返回-1。
这个问题,在各种语言中都有相应的库函数,比如java中可以用str.indexOf(substr);在Golang中可以用strings.Index(str,substr);
在面试中,我们如果没有明确要求需要用KMP算法实现,一般直接用库函数就行了。
老生常谈——暴力匹配
如果不用KMP算法,我们需要对主串从左往右开始遍历,对于每个位置,需要重复遍历子串来判断是否能够匹配。
代码写下来就是这样的:
func getIndex(str, substr string) int {
if len(str)==0 || len(substr)==0 || len(str)<len(substr) {
return -1
}
for i:=0;i<len(str)-len(substr);i++{
for j:=0;j<len(substr);{
if str[i+j] == substr[j] {
j++
}else{
break
}
}
if j == len(substr) {
return i
}
}
return -1
}
假设主串长度n,子串长度m,那么其时间复杂度为O(m*n)
最差的情况是:
主串str = "AAAAAAAAAAAAAAAAAAS"
子串substr = "AAAAAAAAB"
需要比较到最后一个才知道不匹配。
KMP—前缀与后缀最长匹配长度
为了理解KMP算法,我们先来理解一个概念——前缀与后缀最长匹配长度,用它来构造出next数组,而next数组能帮助我们减少无效的遍历。
假设我们需要求字符串substr="abcabcx"中最后一个字符x前面的字符串的前缀与后缀最长匹配长度:
x前面的字符串为"abcabc",那么它可以构造出的前缀和后缀有以下5种情况:
因此,前缀与后缀最长匹配长度为3,即x位置求得的next数组值为3。
KMP—next数组 快速求法
对于任何子串,next数组都是从-1,0开始。即next[0] = -1; next[1] = 0。
求子串索引i位置的next值,我们需要用i-1位置的字符与next[i-1]位置的字符进行比较:
-
如果
s[i-1] == s[next[i-1]],那么next[i] = next[i-1] + 1; -
如果
s[i-1]与s[next[i-1]]不相等,那么继续用s[i-1]与s[next[next[i-1]]]比较 ,即用next[i-1]位置的next位置的字符与s[i-1]进行比较,即更新了与s[i-1]比较的值,我们不妨把与s[i-1]比较的值记作变量cn,它在代码中会不断更新。 -
直到cn更新至0位置,此时
next[i]=0
具体代码如下:
func getNextArr(s string) []int {
if len(s) == 1 {
return []int{-1}
}
next := make([]int, len(s))
next[0] = -1
next[1] = 0
i := 2 //目前在i位置求next数组的值
cn := 0 // 用cn位置的值与s[i-1]比较
for i < len(s) {
if s[i-1] == s[cn] { //如果匹配上了
cn++
next[i] = cn
i++
}else if cn > 0 { //如果没匹配上,但cn位置值不为0,还可以继续往前更新
count = next[cn]
}else{ //没匹配上,但是cn已经跳到了0位置
next[i] = 0
i++
}
}
return next
}
在这个过程中,i一直在增加,只有一次遍历,时间复杂度为O(m)
KMP算法主要逻辑
有了next数组,我们可以一次遍历解决子串定位问题。
比如下面的场景,我们通过未匹配的字符下标5,可以找到next[5]=3,即最长可匹配前缀的下一个位置为3,那么我们就可以直接跳过主串5位置与子串0、1、2位置的比较,主串5位置直接与子串3位置的比较:
实现代码如下:
func getIndex(str, substr string) int {
if len(str)==0 || len(substr)==0 || len(str)<len(substr) {
return -1
}
next := getNextArr(substr) //求next数组
i,j := 0,0
for i<=len(str)-len(substr) && j<len(substr) {
if str[i] == substr[j] { //匹配上了,两个指针往后移继续比较
i++
j++
}else if next[j] == -1 { //没有匹配,且j等于0,更新i位置
i++
}else{ //没有匹配,且j不等于0,通过next值更新j位置
j = next[j]
}
}
if j == len(substr) { //跳出循环,要么j==len(substr)匹配成功了
return i-j
}
return -1 //跳出循环,要么i已经达到最大值,不能匹配子串
}
在这个过程中,i一直在增加,且没有回退的情况,时间复杂度为O(n)
总结
至此,KMP算法已经实现了,KMP算法有很多应用,比如求一篇文档中的关键词位置AC自动机就是从KMP演变过来的,KMP之所以如此重要,就是它产生了一种启发式的思想,面试高频。