字符串匹配算法-KMP算法
给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置,否则返回-1;
如S=“abcd”,P=“bcd”,则返回1;S=“abcd”,P=“acb”,返回-1。
朴素算法:回溯法
最简单的方法就是遍历主串S和模式串P,逐个字符进行匹配,不匹配时返回开始的位置,从主串S的下一个字符开始,继续域模式串P匹配。以一张动图来说明:
1 | //查找出模式串patn在主串src中第一次出现的位置 |
朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。
优化算法:KMP算法
KMP算法的核心,是一个被称为部分匹配表 (Partial Match Table) 的数组。
PMT表:对于模式字符串“abababca”,它的PMT如下表所示:
第一行:字符串的每个字符;
第二行:每个字符的索引;
重要的是value值:PMT中的value是字符串的前缀集合与后缀集合的交集中最长元素的长度。
例如,对于 “aba” ,它的前缀集合为{“a”, “ab”},后缀集合为{“ba”, “a”}。两个集合的交集为{“a”},那么长度最长的元素就是字符串 “a” 了,长度为1,所以对于 “aba” 而言,它在PMT表中对应的值就是1。再比如,对于字符串 “ababa”,它的前缀集合为{“a”, “ab”, “aba”, “abab”},它的后缀集合为{“baba”, “aba”, “ba”, “a”}, 两个集合的交集为{“a”, “aba”},其中最长的元素为 “aba”,长度为3,则 “ababa” 在PMT表中对应的值就是3。
如何使用PMT表加速字符串的查找:
- 主字符串为:“ababababca” -> main_string[0:len_m]
- 模式字符串为:“abababca” -> pattern_string[0:len_p]
在主字符串 main_string
中查找模式字符串 pattern_string
。当 main_string[i] != pattern_string[j]
时,我们知道,main_string[i-j:i-1]
和 pattern_string[0:j-1]
是完全一致的。
具体到下图:i=j=6
时,两个字符不相等,main_string[0:5] = pattern_string[0:5] = “ababab”
那么此时我们应该如何操作:从PMT表中,我们能够看到 PMT[5]
的值( pattern_string[0:5]
中的前缀集合与后缀集合的交集中最长元素的长度), PMT[5]=4
,也就是说,该字符串中,前缀和后缀相同的最长长度为4,也就是说,pattern_string[0:5]
前四位和后四位是相同的。因为 main_string[0:5] = pattern_string[0:5]
,也就是说,pattern_string[0:5]
的前四位和 main_string[0:5]
的后四位是相同的,那么我们就可以直接进行移动了,保持 i
指针不动,然后将 j
指针指向模式字符串的 PMT[5]
位即可。
简言之,以图中的例子来说,在 i
处失配,那么主字符串和模式字符串的前边6位就是相同的。又因为模式字符串的前6位,它的前4位前缀和后4位后缀是相同的,所以我们推知主字符串 i
之前的4位和模式字符串开头的4位是相同的。就是图中的灰色部分。那这部分就不用再比较了。
有了上面的思路,我们就可以使用PMT加速字符串的查找了。我们看到如果是在 j
位失配,那么影响 j
指针回溯的位置的其实是第 j −1
位的 PMT 值,所以为了编程的方便, 我们不直接使用PMT数组,而是将PMT数组向后偏移一位。我们把新得到的这个数组称为next数组。下面给出根据next数组进行字符串匹配加速的字符串匹配程序。其中要注意的一个技巧是,在把PMT进行向右偏移时,第0位的值,我们将其设成了-1,这只是为了编程的方便,并没有其他的意义。在本节的例子中,next数组如下表所示。
1 | int KMP(String text, String pattern) { |
如何快速求next数组
其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。
具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。
p串错开一位匹配,0起始的p串与1起始的p串,前者是瞄准了p的前缀,后者抛弃了p[0]所以是瞄准p的后缀,双方的公共部分即是公共前后缀。
1 | void getNext(String pattern, int[] next) { |
- 匹配失败时,总是能够让 pattern 回退到某个位置,使 text 不用回退。
- 在字符串比较时,pattern 提供的信息越多,计算复杂度越低。(有兴趣的可以了解一下 Trie 树,这是 text 提供的信息越多,计算复杂度越低的一个例子。)
参考链接:https://www.zhihu.com/question/21923021
参考链接:https://blog.csdn.net/qq_37969433/article/details/82947411