KMP算法
KMP算法的作用
可以在O(m+n)
的时间复杂度内,在文本串S
中找到模式串P
是否存在。
KMP算法的思想
首先要想理解KMP
算法,就必须先理解了解暴力解法的思想
假设有文本串
A B B B A A B B A
模式串
A B B A
如果是暴力解法,那么会有两个指针,一个指针i
指向文本串。一个指针j
指向模式串。
当j
未走完的中间发现不匹配都需要将i
调整为i+1
而j
要变为0
.
for(int i=0;i<S.size();i++) {
for(int j=i;j<S.size();j++) { // 简化写法,并不考虑S不够的情况
if(S[i+j]!=P[j]) {
break;
}
}
}
A B A B A A B A A
A B A A
发现不匹配
A B A B A A B A A
A B A A
A B B B A A B B B
A B A A
…
A B A B A A B A A
A B A A
匹配成功
算法4中的KMP算法
KMP算法思想
算法4中的KMP
算法的表象就是i
会一直的往前走,遇到不匹配的情况就只需要调整j
也就是下一次匹配的地方
其实KMP
算法就是充分利用了匹配失败时候的已知的信息。
如果匹配失败
A B A B A A B A A
A B A A
其实不难发现,我们知道匹配失败时候i
指向的字符是B
也知道,前面匹配成功的字符就是前j-1
个字符,也就是A B B
这时候如果要让i
往前走j
调整到合适的匹配位置。
也就是这时候i
变成i+1
,而i
之前的与P
匹配的最多越好。
其实也就是在匹配失败时候i到匹配失败的位置之间,重新找到那个能和P匹配上的点
A B A B
这个问题就转变为找这个字符串的最长相同的前后缀长度了。
- 前缀, 是包含第一个字符,不包含最后一个字符的的子串
- 后缀,包含最后一个字符,但是不包含第一个字符的子串
A B A B
的前缀有 A
,A B
,A B A
后缀有 B
,A B
, B A B
可以看出最长的公共前后缀就是A B
这时候就变成了
A B A B A A B A A 黄色为当前i的位置
A B A B 黄色为j当前调整后的位置,需要与当前i元素比较
这样的会i
只管往前跑,只需要调整j
的位置
这就是KMP
算法的核心思想。
KMP算法的表示
- 如何取去示,当遇到字符
S[i]
的时候时候,j
跳转的位置
这时候设置二维数组dfa[S[i]][j]
代表的含义就是,当遇到S[i]
字符j
需要跳转的位置
由上面可以知道:
跳转的位置其实就是,最长公共前后缀的长度
所以这个含义也是:
P[0-j-1]与S[i]组合而成的字符串(也就睡以S[i]结尾长度为j+1的字符串)的最长公共前后缀长度
怎么求解dfa数组
其实就只有两种情况
-
情况一: 当
S[i]
与P[j]
相等匹配的时候如果匹配其实就是
dfa[s[i]][j]=j+1
-
情况二: 当
S[i]
与P[j]
不相等不匹配的时候这个时候,
j
的跳转位置,是已经匹配字符串与匹配失败字符结合字符串的最长公共前后缀长度。其实也就是
dfa[S[i]][k]
,其中k
代表的是
这个问题可以转化为:已知一个字符串S,它的前缀字符串和后缀字符串的最长共有元素的长度为k,现将一个字符拼接在S的尾部组成新的字符串S',求S'的前缀字符串和后缀字符串的最长共有元素的长度k'。
首先能够确定的是,k'的大小不会超过k+1,也就是说k'只可能<= k+1。分两种情况讨论:
当拼接的字符等于S[k]时,k'的大小为k+1;
当拼接的字符不等于S[k]时,k'等于S[0]、S[1]、S[2]、...、S[k-1]与该字符组成的字符串的前缀字符串和后缀字符串的最长共有元素的长度;
实际上这正是dfa[t[i]][k]的含义
所以dfa
的构造就是
dfa[pat.charAt(0)][0] = 1;
for (int k = 0, j = 1; j < pat.length(); j++) {
// 计算dfa[][j]
for (int c = 0; c < 256; c++) {
dfa[c][j] = dfa[c][k]; // 匹配失败的情况
}
dfa[pat.charAt(j)][j] = j + 1; // 匹配成功的情况
k = dfa[pat.charAt(j)][k]; // 这时候的S就是之前匹配上的最长公共前后缀
}
如果将j
的位置当做一种状态,这就是一个自动状态机了。
所以
public static int search(String pat, String txt) {
int j, M = pat.length();
int i, N = txt.length();
for (i = 0, j = 0; i < N && j < M; i++)
j = dfa[txt.charAt(i)][j]; // 状态的转移
if (j == M)
return i - M;
else
return N;
}
这时候我们知道i
是一直往前走的,所以必须要S
中匹配失败的一个字符和T
中已经匹配成功字符的信息。
// 这时候就想,能不能不要和S
牵扯上关系,只需要和P
有关
这时候,如果匹配成功i=i+1
.如果匹配失败了,这时候i
不变,直接调整j
,这时候S[i]
就是要匹配的下一个字符,这时候要转移多少其实就是已经匹配字符串的最长公共前后缀的长度。
A B A B A A B A A
A B A A
这时候下一个匹配的还是B
前缀: A,AB
后缀A,BA
所以
A B A B A A B A A
A B A A
因为B的前一个是A,所以一定是以A为结尾,而且必须重头开始匹配,所以以A开头,这也是为什么用前后缀 这是理解KMP算法的一个关键点,为什么是前后缀
这样做的好处是,去除了一维,只剩下一维了
怎么去求这一维的值就是重点了,已经S[0-i]的公共前后缀长度,求S[0-i+1]公共前后缀的长度
这也就是next数组的前身
用数组pre_next[]
来表示,所以pre_next[j]=k
代表的含义是:模式串中前j个字符子串的最长公共前后缀的长度为k
求解pre_next[]
那么问题是怎么去求模式串各个子串的最长公共前后缀的长度
A B A A
来说
这就是next数组的来历
例题
NC149 kmp算法
知识点字符串
描述
给你一个文本串 T ,一个非空模板串 S ,问 S 在 T 中出现了多少
示例1
输入:
"ababab","abababab"
返回值:
2
示例2
输入:
"abab","abacabab"
返回值:
1
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算模板串S在文本串T中出现了多少次
* @param S string字符串 模板串
* @param T string字符串 文本串
* @return int整型
*/
int kmp(string S, string T) {
// write code here
vector<int> next(S.size());
int ans = 0;
GetNext(next, S);
int j = 0;
int i = 0;
while(i<T.size()) {
if(j==-1 || T[i]==S[j]) { // 匹配成功 j==-1的含义是第一个匹配失败,重新开始匹配
if(j==S.size()-1) { // 匹配成功
ans++;
// 下一个要匹配的位置就是
j=next[j]; // 假设当前失败后,这个位置要去哪里匹配
} else {
i++;
j++;
}
} else { // 匹配失败
j=next[j]; // 跳转位置
}
}
return ans;
}
void GetNext(vector<int>& next,string& Pattern) {
// 初始化
next[0]=-1;
int k=-1,j=0;
while(j<Pattern.size()-1) { // 这时候的next相当于pre_next的往前推了一位。所以最后一位不考虑
if(k==-1 || Pattern[j]==Pattern[k]) {
j++;
k++;
next[j]=k;
} else { // 不相等
k=next[k];
}
}
}
};