本文借助一道经典的字符串匹配的题目来复习KMP匹配算法。对于两个长度为n和m的字符串(文本串和模式串)的匹配,使用暴力解法的时间复杂度为O(m*n)而使用KMP算法的时间复杂度变为O(m+n)。
例题:28(实现 strStr())
1.KMP概述
KMP算法的主要思想是在进行字符串的匹配出现不匹配的时候利用前面已经匹配过的一部分字符串来避免模式串的重头开始匹配。
文本串:用于进行被匹配的串(一般是长串)
模式串:用于匹配的串(一般是短串)
2.前缀表(next数组存储)
前缀表是记录模式串前缀后缀的最大相同长度的一个表,使用next数组来进行存储。为什么要记录前缀后缀的最大相同长度呢:在匹配的时候可以利用next数组进行回退操作,而next数组记录了模式串应该从哪里开始重新匹配。回退操作也是KMP算法的精髓所在。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串
前缀后缀的最大相同长度(也成为最长公共前后缀长度)
前缀后缀最大相同长度指前缀和后缀中的最大的相同长度(字面理解即可),上例图:
只有一个字符a,前缀后缀最大相同长度为0
两个字符aa,前缀后缀最大相同长度为1
三个字符,此时前缀和后缀显然不可能相同,所以前缀后缀最大长度为。
利用next数组记录前缀表,前缀表的长度和模式串的长度是相同的且对应位置上前缀表记录的是的模式串子串当前位置(往前)的前缀后缀最大相同长。
next数组的代码实现非常重要。理解next数组代码的关键在于两点:
(1)j有两重含义:a) 指针的含义。b) j还代表最大前缀后缀的相同长度
(2)求next数组的回退过程和文本串和模式串匹配时的回退过程类似(都是找当前位置的前一个位置的next数组值)
//getnext函数,其中模式串s和next数组的长度是相同的,它们直接具有对应关系
public void getNext(int[] next, String s){
//初始化j=0,表示指针从第一个元素开始,注意j既有指针的含义也有最大前缀后缀相同长度的含义
int j = 0;
//初始化next[0],因为只有一个元素时其最大前缀后缀相同长度肯定为0
next[0] = j;
//第二个指针i对模式串进行遍历求出next数组对应模式串的值
for (int i = 1; i<s.length(); i++){
//判断两个指针所指元素是否相同,若不相同指针j进行回退操作,这个回退操作和文本串模式串匹配时期的回退操作类似
while(j>0 && s.charAt(i) != s.charAt(j)){
j=next[j-1];
}
//若两个指针所指元素相同则j++(指针前进、最大前缀后缀长度加一)
if(s.charAt(i)==s.charAt(j)){
j++;
}
//将得到的j(最大前缀后缀长度)赋值给next数组对应索引
next[i] = j;
}
}
3.匹配过程
匹配过程其实就是遍历文本串匹配的过程(和暴力解法有点相同,不同之处在于模式串的匹配不会在每次匹配不成功的时候重头开始匹配而是利用next数组合理进行回退操作)
public int strStr(String haystack, String needle) {
if(needle.length()==0){
return 0;
}
int[] next = new int[needle.length()];
//通过getNext函数获取next数组
getNext(next, needle);
int j = 0;
//循环遍历进行匹配
for(int i = 0; i<haystack.length();i++){
/*当文本串某个字符和模式串某个字符不相同时,模式串指针回退,回退的位置由其前一个位置的next数组
值决定(即next[j-1])(因为next[j-1]已经知道了前面有多少元素重复匹配过了不需要再重新匹配)*/
while(j>0 && haystack.charAt(i) != needle.charAt(j)){
j = next[j-1];
}
if(haystack.charAt(i)==needle.charAt(j)){
j++;
}
if(j==needle.length()){
return (i-needle.length()+1);
}
}
return -1;
}
相关题目:459
KMP算法有些折腾,因为网上不同人写next数组的方法不一样,有些人写的next数组是进行了减一操作,而有的人则是进行了右移一位的操作,这些都是为了实现代码的方便,个人觉得自己写得是最好理解的。