kmp算法本身匹配的方法理解感觉不太难,最大的难点可能就是next数组的计算,不容易理解。匹配的原理就是,提前算出子串的一个next数组,这个next数组记录了子串中到当前位置的字符串的最长前后缀(前后部分的相同部分的最长长度),利用这个next表,当子串和主串比对的时候,如果不同,就会找next表,利用这个最长长度,计算移动到对应的位置,使得最长前面的部分不用在去匹配,用来提高匹配效率。本文是参考的《王道考研数据结构》上写的,只是记录的一些自己的理解哈。
前缀,后缀,部分匹配值
前缀:除最后一个字符以外,字符串的所有头部子串
后缀:除第一个字符以外,字符串的所有尾部子串
部分匹配值:字符串的前后缀的最长相等前后缀长度
以 “ababa”为例
字符串 | 前缀 | 后缀 | 部分匹配值 |
---|---|---|---|
a | ∅ | ∅ | 0 |
ab | {a} | {b} | 0 |
aba | {a,ab} | {a,ba} | 1 |
abab | {a,ab,aba} | {b,ab,bab} | 2 |
ababa | {a,ab,aba,abab} | {a,ba,aba,baba} | 3 |
部分匹配值 = (前缀集合 ∩ 后缀集合 ) 的长度
恭喜!这里next数组的雏形就出来了,说实话这就是next数组。真正最后用的也是在这个基础上优化出来的Next数组。接下来就是使用这个数组,和如何优化。
S | a | b | a | b | a |
---|---|---|---|---|---|
next | 0 | 0 | 1 | 2 | 3 |
部分匹配值的使用
主串:a b a b c a b c a c b a b
子串:a b c a c
首先拿到next数组
序号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 0 | 0 | 1 | 0 |
第一次匹配:
ababcabcacbab
abc
a 和 c不匹配。 前面 ‘ab’ 匹配。最后一个匹配字符b对应的匹配值为0
按照 : 移动位数 = 已匹配的字符数 - 对应的部分匹配值
得出: 移动位数 = 2 - 0 = 2
第二次匹配:
ababcabcacbab
abcac
b 和 c不匹配。前面’abca’匹配,最后一个匹配字符a对应的匹配值为1
移动位数 = 4 - 1 = 4
第三次匹配:
ababcabcacbab
abcac
子串比对完成。主串没有回退,所以KMP算法时间复杂度为O(n + m)
算法改进
当前的算法
移动位数 = 已匹配的字符数 - 对应的部分匹配值
转为伪代码。
Move = (j - 1) - next[j - 1]
//这里 j的值是对应上面的next表里的序号,如果代码里面数组是从0开始算的话。就应该是
Move = j - next[j - 1]
优化
使用部分匹配的时候,每次失败都需要找到前一个元素匹配的部分匹配值,使用起来不太方便。将数组右移一位,这样一旦失败直接取到对应的值
序号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | -1 | 0 | 0 | 0 | 1 |
- 第一个元素为右移之后空缺,用-1填充。目的是第一位不匹配,向右移动一位。就是:移动位数 = 已匹配的字符数(0) + 1 = 1
- 最后一个元素溢出,因为根本用不到。
这样上面的式子就改为
//按书上的那个序号算
Move = (j - 1) - next[j]
//数组是从0开始算
Move = j - next[j]
那么计算移动应该到达的位置(子串中指针j应该移动到的位置)
j = j - Move
= j - ((j - 1) - next[j]) = next[j] + 1 //书上的
= j - (j - next[j]) = next[j] //从0计算
因为每次j = next[j] + 1每次都要+1,很麻烦,把+1放到数组的值里面。这样一旦不匹配就移动到第next[j]的位置即可。
对于数组是从0开始计算的话,不用这个+1操作了,本身就已经是对应的坐标位置了。
OK,以上就是next怎么来的原因。
接下来就是计算next的代码了。
next数组计算代码
书上的错误
//这段是书上写的程序代码,测试用例:"abcac"
void get_next(String T,int next[]){
int i = 1, j = 0;
next[1] = 0;
while(i < T.length){
if(j == 0 || T.ch[i] == T.ch[j]){
++i;++j;next[i] = j;
}
else
j = next[j];
}
}
这块部分我照着改成C++,Kotlin的方式写了,得出来的结果都不对。自己推逻辑感觉没问题,但是计算机算出来就是不对。最终的问题还是出在指针的位置问题。因为书上讲的编号都是以1开头来算的,但是字符串可不是从1开始算的啊。导致字符串和next数组根本是错开的。问题就出在 T.ch[i] == T.ch[j]上面。得到的next数组就是 0,1,1,1,1
书上代码纠正
//书上代码更正
void get_next(String T,int next[]){
int i = 1, j = 0;
next[1] = 0;
while(i < T.length){
if(j == 0 || T.ch[i - 1] == T.ch[j - 1]){
++i;++j;next[i] = j;
}
else
j = next[j];
}
}
按照从0开始计算的代码
从0开始的话就不会出现上面的问题,从1开始逻辑没理清就很容易出现错位问题。
void get_next(String T,int next[]){
int i = 0, j = -1;
next[1] = -1;
while(i < T.length - 1){
if(j == -1 || T.ch[i] == T.ch[j]){
++i;++j;next[i] = j;
}
else
j = next[j];
}
}
//kotlin版
fun String.getNextArr():Array<Int>{
val list = Array(this.length){i -> -1}
var i = 0
var j = -1
while (i < this.length - 1){
if(j == -1 || this[i] == this[j]){
++i
++j
list[i] = j
}
else
j = list[j]
}
return list
}
代码讲解
额,这块还是按照书上从1开始的方式讲解吧。首先是初始化,i = 1,j = 0。i是持续向后走的,j指针是来回跳的。j主要作用就是和i进行比较,如果i,j的字符相等,i,j同时向后移动,并将j的当前位置赋给i对应的next元素,为什么这么做?分开说
- i,j同时向后移动做的事就是为了下一次循环再次比对,是否还是相等
- 将j的当前位置赋给i对应的next元素,为的是记录当出现不匹配的时候,跳转的位置。同时也是完成了前面说的右移操作。也就是说这个位置是记录在匹配好的时候i位置的后一位,也就是做了++i之后的操作。
- 还有一个操作就是判断了 j == 0,刚好是第一个的前一个,j = 0是为了方便后面+1直接刚好是第一个,这样只要不匹配,得到的坐标就是第一个的位置,直接子串拉回第一个,从头判断
- else部分就是当不匹配也不是最初状态的时候,j回退到上一次匹配到的位置,下次循环紧接着之前的位置继续匹配。
KMP全部代码
//kotlin版
fun main(){
var substr = "abcac"
val next = substr.getNextArr()
val str = "ababcabcacbab"
var i = 0
var j = 0
var pos = -1
while (i + substr.length - j != str.length){
if(j == -1 || str[i] == substr[j]){
i++
j++
}else{
j = next[j]
}
if(j == substr.length){
pos = i - j
break
}
}
println(pos)
}
这块没有什么难度了。大家自行分析吧。
速成KMP,推荐一个视频。
觉得文章有些长,可以看这个b站大佬的KMP视频,讲的非常好。
KMP字符串匹配算法2
欢迎关注菠萝菠萝卜的博客 (也是我 😜😜😜)