KMP详解
KMP的本质是通过已经掌握的而信息来规避重复的运算
只需要一次主串的遍历就可以完成匹配,next数组的存在让j的回溯不需要循环,而是提前准备好表备查,这样没有循环的嵌套,复杂度仅为O(n)
pattern | a | b | c | a | b | c |
---|---|---|---|---|---|---|
j | 0 | 1 | 2 | 3 | 4 | 5 |
next | -1 | 0 | 0 | 0 | 1 | 2 |
可以看出来,next数组存的是如果该位置匹配失败那么该跳到哪里,j=4如果匹配失败了,那么此时前后缀长度为1,应该回溯到j=1。这样的话,我们构建next数组的时候只需要把当前位置产生的前后缀信息放到下一个位置便可以了,也就是说,最后一个位置的前后缀信息是不用比较的
1 |
|
构建next
- k前缀长度一定是依次增加,不能跳
- pattern[k] != pattern[j]的时候前缀需要缩短,怎么缩短?k回溯一次
- 课本的这个版本next数组情况稍有不同,j位置的next值存的是以j-1为尾的后缀的长度,所以前缀指针匹配失败的时候,直接找next[k]就能找到k-1位置字符上一次出现的位置,回溯略微方便一点
1 |
|
改进next数组
通过例子来看为什么需要改进
target | a | b | c | a | b | b | a | b | c |
---|---|---|---|---|---|---|---|---|---|
pattern | a | b | c | a | b | c | |||
next | -1 | 0 | 0 | 0 | 1 | 2 |
那么从上述例子来看的话,由于target串中的b对比失败,需要将指针j回溯到j=2,但是很明显如果j=5没有匹配上的话,j=2也匹配不上,因为j=5和j=2两个位置的字符一样!
所以,next数组还能进行改进,使得时间复杂度进一步下降
pattern | a | b | c | a | b | c |
---|---|---|---|---|---|---|
前后缀长度k(第一版next) | -1 | 0 | 0 | 0 | 1 | 2 |
改进后的next | -1 | 0 | 0 | -1 | 0 | 0 |
这里的-1怎么理解?
回溯到-1和0是不一样的,0还需要比较一下i和j=0两位置的字符,失败的那个位置没有可以用的子串信息,只能和第0号字符比较了,但是如果回溯到了-1,说明,第0号元素也不用比较了,这一次的匹配失败已经证明了和第0个肯定不相同,直接回溯到-1,就能在下一次循环中让两个指针+1,不用浪费一次比较
总结一下怎么个改进思想:回溯j的时候,我们回溯到的位置是可利用子串的后一个位置,但是如果这个位置和匹配失败的位置的字符一样的话,那其实一定匹配不上,可以利用旧next表回溯的位置的next值接着缩短前后缀,因为那个位置的next值是匹配不上的时候用的,如果我们提前知道匹配不上那其实也就可以提前用这个信息,一直循环缩短到和j位置不一样为止,这样和j位置不一样的话起码还有得比,不然回溯位置一样的话根本不用比
一句话,回溯一定要回溯到不一样的字符,要是回溯位置和原来一样,那一定匹配失败,还得回溯,不如我们在next数组里就把这一步工作做了,减少indexOf()函数里while的工作量
改进就是避免跳转之后相同
1 |
|
如果改进版的next需要连续缩短前缀两次或者更多呢呢?
如果需要连续缩短,那么中间那个位置的它当年也是想这么想的,所以它存的就是匹配失败时需要跳转的位置,见下方例子
pattern | a | b | c | a | b | d | a | b | c | a | b | c | a | a |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
第一代next | -1 | 0 | 0 | 0 | 1 | 2 | 0 | 1 | 2 | 3 | 4 | 5 | 3 | 4 |
改进版next | -1 | 0 | 0 | -1 | 0 | 2 | -1 | 0 | 0 | -1 | 0 | 5 | -1 | 4 |
这个例子中,计算改进版next时,9号元素a如果匹配失败需要返回3,但是3号元素也是a,所以此时需要k = next[k],next[k]是-1而不是0,所以连续缩短前缀并不需要循环,一次就能到位
可以看到改进版的next数组各个数字并不连续