先上需求
给定一个 str 字符串和一个 value 字符串,在 str 字符串中找出 value 字符串出现的第一个位置 (从0开始)。其实就是C语言的 strstr() 以及 Java的 indexOf() 实现原理。
输入案例:str:"hello" value=“ll”
输出案例:2
暴力解法
用一个长指针i跟短指针j分别指向主串跟模式串,从左往右,一个一个匹配,如果匹配,如果出现不匹配,那就把长指针移回第 i - j + 1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:
…
…
…
…
直到匹配成功,返回出去就好了
于是上代码
/**
* 暴力破解法
* @param str 主串
* @param value 模式串
* @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
*/
int match(string str, string value) {
int i = 0;
int j = 0;
while (i < str.length() && j < value.length()) {
if (str[i] == value[j]) {
// 当两个字符相同,就比较下一个
i++;
j++;
} else {
// i后退,j重头开始
i = i - j + 1;
j = 0;
}
}
if (j == str.length()) {
return i - j;
} else {
return -1;
}
}
这样的方法也可以得到结果,但是时间复杂度来到了(n*m),n为主串的长度,m为模式串的长度,一般这种暴力匹配的方法就会被老板暴力地辞退。
这时候Donald Knuth、Vaughan Pratt、James H. Morris三大高叟就想到了能不能不让i指针不回退,只移动短指针j阿,于是就有了牛逼(头痛)的KMP算法
KMP算法
由于动画效果不好制作,贴上知乎的大佬讲解,里面有动画的呈现
知乎KMP
于是有了我自己对KMP算法的理解
其实KMP算法的基本操作流程如下:
- 假设现在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置 如果 j = -1,或者当前字符匹配成功(即 S[i] ==
P[j] ),都令 i++,j++,继续匹配下一个字符; - 如果 j != -1,且当前字符匹配失败(即 S[i] != P[j] ),则令 i 不变,j =
next[j]。此举意味着失配时,模式串 P相对于文本串 S 向右移动了 j - next [j] 位 - 换言之,将模式串 P 匹配位置的 next 数组的值对应的模式串 P 的索引位置移动到未重复出现位置
例如这样的小串,如果用暴力解法 主串就得从A后的BC重新开始,此时i指针指向主串的B,j指向主串的A
这种情况还不算特别慢,如果是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。其实用人类的思维,其实只要让i不动j回到AB第一次重复后的下一个位置就可以
此时i还是原来的指向,只是短指针j指向了C
所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪?
据说这是有公式的,翻了很久资料(搜索引擎),终于找到了关于KMP的公式:
Value[0 ~ k-1] == Value[j-k ~ j-1]
公式的推导:
当str[i] != Value[j]时
有str[i-j ~ i-1] == Value[0 ~ j-1]
由Value[0 ~ k-1] == Value[j-k ~ j-1]
必然:str[i-k ~ i-1] == Value[0 ~ k-1]
这时候我们就列个表,看看这几个公式在作甚
模式串value子串对应的各个前缀后缀的公共元素的 最大长度表 下图。
公共元素的最长重复元素就得到是2了
这时候我们只需要的到当前匹配不等的元素在模型串的位置,只需要回溯到上次不等的位置,并且将索引一起移动
将该步骤翻译成计算机步骤如下
1)找出前缀pre,设为pre[0~m];
2)找出后缀post,设为post[0~n];
3)从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:
如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;
如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。
如此下去,直到找到重复子串,或者k没找到。
这时候就引进了一个next数组来解决最大长度值的问题,并且要给初始0位置赋上-1(其实这一步是为了next数组记录下一个字串位置,并且确保匹配不成功,短指针能够下移,这一步得看到最后再回来理解)
通过next数组我们就能够记录模型串的最大长度值
比如模式串的D 与文本串的 C 失配了,找出失配处模式串的 next数组 里面对应的值,这里为 0,然后将索引为 0 的位置移动到失配处。
贴个代码,继续讲
int* getNext(string value) {
int *next = new int[value.length()];
next[0] = -1;
int j = 0;
int k = -1;
while (j < value.length() - 1) {
if (k == -1 || value[j] == value[k]) {
next[++j] = ++k;
} else {
k = next[k];
}
}
return next;
}
k其实扮演了很重要的作用,通过k,我们回到重复串上一次结束重复的位置
根据别的博主博客,总结出这样的规律:
当value[k] == value[j]时,
有next[j+1] == next[j] + 1
其实这个是可以证明的:
因为在P[j]之前已经有value[0 ~ k-1] == value[j-k ~ j-1]。(next[j] == k)
这时候现有value[k] == value[j],我们是不是可以得到value[0 ~ k-1] + value[k] == value[j-k ~ j-1] + value[j]。
即:value[0 ~ k] == value[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
其实说了这么多,next数组的主要构建思路就是,
- 用k去临时记录重复串的个数,如果value[j]与value[k]相等,此时让next[j]位置保存之前出现该值的位置,利于回溯
- 如果不等,将k值回到上一次不等的位置,继续1操作,最后把next里对应模型串的所有位置保存应该回溯的位置。
当理解了next数组其实剩下的就很容易了
int KMP(string str, string value) {
int i = 0;
int j = 0;
int* next = getNext(value);
while (i < str.length() && j < value.length()) {
if (j == -1 || str[i] == value[j]) {
// 当j为-1时,要移动的是i,当然j也要归0
i++;
j++;
} else {
// j回到指定位置
j = next[j];
}
}
if (j == str.length()) {
return i - j;
} else {
return -1;
}
}
所以整个KMP的算法其实就是在next数组,确保模串中相同子串回溯的位置相等
该算法有小小瑕疵,这里有更优的算法
如果两个字串已经相等,其实回溯是没有意义的
int* getNext(string value) {
int *next = new int[value.length()];
next[0] = -1;
int j = 0;
int k = -1;
while (j < value.length() - 1) {
if (k == -1 || value[j] == value[k]) {
if (value[++j] == value[++k]) {
// 当两个字符相等时要跳过
next[j] = next[k];
} else {
next[j] = k;
}
} else {
k = next[k];
}
}
return next;
}
下面贴上java的代码,这是leecode上strStr()的用KMP解法
public int strStr(String haystack, String needle) {
if(needle.length() == 0) {
return 0;
}
if(needle.length() > haystack.length()){
return -1;
}
int i = 0;
int j = 0;
int[] next = getNext(needle);
char[] t = haystack.toCharArray();
char[] p = needle.toCharArray();
while(i < t.length && j <p.length) {
//1、j回到起点的时候或者值相等都应该下移
if(j == -1 || t[i] == p[j]) {
j++;
i++;
}else {
j = next[j];
}
}
if(j == p.length) {
return i-j;
}
return -1;
}
public int[] getNext(String needle) {
char[] p = needle.toCharArray();
int[] next = new int[p.length];
int j = 0;
int k = -1;
next[0] = -1;
while(j < p.length - 1) {
//两者相等或者k处于next的初始位置
if(k == -1 || p[j] == p[k]) {
j++;
k++;
if(p[j] == p[k]) {
next[j] = next[k];
}else {
next[j] = k;
}
}else {
k = next[k];
}
}
return next;
}
如果有不正确的地方,希望大家能够指出来,第一次看KMP算法,以前都是直接那indexOf套