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)
算法改进
当前的算法
移动位数 = 已匹配的字符数 - 对应的部分匹配值
转为伪代码。
1 | Move = (j - 1) - next[j - 1] |
优化
使用部分匹配的时候,每次失败都需要找到前一个元素匹配的部分匹配值,使用起来不太方便。将数组右移一位,这样一旦失败直接取到对应的值
| 序号 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| S | a | b | c | a | c |
| next | -1 | 0 | 0 | 0 | 1 |
- 第一个元素为右移之后空缺,用-1填充。目的是第一位不匹配,向右移动一位。就是:移动位数 = 已匹配的字符数(0) + 1 = 1
- 最后一个元素溢出,因为根本用不到。
这样上面的式子就改为
1 | //按书上的那个序号算 |
那么计算移动应该到达的位置(子串中指针j应该移动到的位置)
1 | j = j - Move |
因为每次j = next[j] + 1每次都要+1,很麻烦,把+1放到数组的值里面。这样一旦不匹配就移动到第next[j]的位置即可。
对于数组是从0开始计算的话,不用这个+1操作了,本身就已经是对应的坐标位置了。
OK,以上就是next怎么来的原因。
接下来就是计算next的代码了。
next数组计算代码
书上的错误
1 | //这段是书上写的程序代码,测试用例:"abcac" |
这块部分我照着改成C++,Kotlin的方式写了,得出来的结果都不对。自己推逻辑感觉没问题,但是计算机算出来就是不对。最终的问题还是出在指针的位置问题。因为书上讲的编号都是以1开头来算的,但是字符串可不是从1开始算的啊。导致字符串和next数组根本是错开的。问题就出在 T.ch[i] == T.ch[j]上面。得到的next数组就是 0,1,1,1,1。当然如果传进来的字符串也是从1开始算的就没问题了。
书上代码纠正
1 | //书上代码更正 |
按照从0开始计算的代码
从0开始的话就不会出现上面的问题,从1开始逻辑没理清就很容易出现错位问题。
1 | void get_next(String T,int next[]){ |
1 | //kotlin版 |
代码讲解
额,这块还是按照书上从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全部代码
1 | //kotlin版 |
这块没有什么难度了。大家自行分析吧。
速成KMP,推荐一个视频。
觉得文章有些长,可以看这个b站大佬的KMP视频,讲的非常好。