算法介绍
Knuth-Morris-Pratt 算法,简称KMP 算法,由 Donald Knuth、ames H. Morris 和Vaughan Pratt 三人于 1977 年联合发表。
主要用于解决字符串匹配问题。
“借助前缀表,可以在匹配失败时,充分利用已匹配的部分的信息,避免对模式串从头重新匹配”
例如,给定文本串“aabaabaaf”以及模式串“aabaaf”
012345678
aabaabaaf
aabaaf
第一次匹配:匹配到位置5时出现冲突,这个时候已经匹配的文本串中其实有一部分可以直接使用
即,
012345678
aabaabaaf
aabaaf
这个时候只需要继续对文本串中的5位置与模式串中的3位置的“b”进行匹配即可。
- KMP算法可以将暴力解法的时间复杂度由O(m*n)降为O(m+n)。
- 空间复杂度为O(n)
前缀表
前缀&后缀
前缀:包含首字母不包含尾字母的所有子串
后缀:包含尾字母不包含首字母的所有子串
最长相等前后缀:
前缀表:
上述前缀子串的最大长度构成前缀表。
a | a | b | a | a | a | b |
---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 2 | 2 | 3 |
前缀表的作用:
匹配字符串的过程中,如果产生了冲突,则根据冲突位置前面一位的前缀表的数值,寻找下一步要匹配的模式串中的元素下标。
前缀表求解代码实现
在不同的代码实现中,前缀表一般使用next数组表示,对于next数组的具体实现还存在多种不同的方式:
- next数组等于前缀表
- next数组为前缀表对每个元素-1
- next数组为前缀表整体右移一位,0位置赋值为-1
代码实现关键点:
- 初始化:
- i指针为后缀表末尾(i走的更快)
- j指针为前缀表末尾
- 当i,j指向的元素相同时如何移动 j 指针
- 当i,j指向的元素不同时如何移动 j 指针
- next[i] 如何赋值
next数组等于前缀表的求解方法:
前缀表存储的是原字符串中当前位置长度子串的最长相等前后缀的长度。
- 初始化:
- i指针为后缀表末尾(i走的更快):i=1
- j指针为前缀表末尾 :j=0
- 当i,j指向的元素相同时如何移动 j 指针:j++(因为j对应的数值是下标是从0开始的,但是对于前缀表中存储的值应该是长度,所以需要j++;并且,如果i,j指向的元素相同,那么对于下一个子串的判断,前缀表末尾应该后移一位)
- 当i,j指向的元素不同时如何移动 j 指针:将j不断根据前移(前移是根据其前一位的next值移动,不需要一位一位地移动),直到j移动到0位置;或者j对应元素等于i对应元素。【在此处求解next数组的过程中其实就用到了KMP的思想,匹配失败时不需要对j从头开始重新匹配,而是按照next数组进行移动】
- next[i] 如何赋值:next[i]=j
代码实现:
void getNext(int next[],string s){
int j=0;
next[0]=0;
for(int i=1;i<s.size();i++){
while(j>0 && s[i]!=s[j]){
j=next[j-1];
}
if(s[i]==s[j]){
j++;
}
next[i]=j;
}
}
题目汇总
28. 实现 strStr()
题目介绍:
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
解题思想:
1、暴力算法:
对文本串从第一个元素开始与模式串进行匹配,如果匹配失败,文本串指针向后移动一个单位,然后再对模式串进行从头到尾匹配。
时间复杂度为O(m*n)
2、KMP算法:
“可以在匹配失败时,知道前面一部分的信息,避免从头重新匹配”
时间复杂度:O(m+n)
空间复杂度:O(n)
class Solution {
public int strStr(String haystack, String needle) {
//使用KMP算法进行字符串的匹配,时间复杂度O(m+n)
//1、首先计算haystack的前缀表
int[] next=calNext(needle);
//2、然后进行遍历匹配
int i=0;
int j=0;
for(i=0;i<haystack.length();i++){
while(j>0 && needle.charAt(j)!=haystack.charAt(i)){
j=next[j-1];
}
if(haystack.charAt(i)==needle.charAt(j)){
j++;
}
if(j==needle.length()){
return i-needle.length()+1;
}
}
return -1;
}
public int[] calNext(String s){
int[] next=new int[s.length()];
next[0]=0;
int j=0;
for(int i=1;i<s.length();i++){
while(j>0 && s.charAt(j)!=s.charAt(i)){
j=next[j-1];
}
if(s.charAt(i)==s.charAt(j)){
j++;
}
next[i]=j;
}
return next;
}
}
459. 重复的子字符串
题目介绍:
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
解题思想:
利用KMP中的前缀表的性质,
如果字符串是一个重复字符串,那么其最后一个元素对应的前缀表中的(最长相同前后缀的长度)将达到最大,
并且,len(str)-next[-1]==一个周期的长度(此处以next实现为前缀表为例)
因此,只需要判断len(str)%一个周期的长度==0?即可
注意:一个周期的长度len(str)的情况!!!
class Solution {
public boolean repeatedSubstringPattern(String s) {
//使用前缀表解决
//如果一个字符串s可以由其子串重复多次构成,则s一定有相等的前后缀
//1、首先计算一个next表
int[] next=calNext(s);
int len=s.length();
if(next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0){
//此处注意next[len-1]==0的情况!会导分母为0
return true;
}
return false;
}
public int[] calNext(String s){
int[] next=new int[s.length()];
next[0]=0;
int j=0;
for(int i=1;i<s.length();i++){
while(j>0 && s.charAt(i)!=s.charAt(j)){
j=next[j-1];
}
if( s.charAt(i)==s.charAt(j)){
j++;
}
next[i]=j;
}
return next;
}
}