1. BF算法
BF「BruteForce」算法,是普通的字符串匹配模式算法
主串和子串一一比较
- 相等:比较下一个
- 不相等:主串回退到上次比较起始下标的下一位;子串回退到0下标
返回第一个匹配相等的下标
package KMP;
public class Main {
public static int BF(String str, String sub) {
if (str == null || sub == null) {
return -1;
}
int strLen = str.length();
int subLen = sub.length();
int i = 0, j = 0;
while (i < strLen && j < subLen) {
if (str.charAt(i) == sub.charAt(j)) {
++i;
++j;
} else {
i = i - j + 1;
j = 0;
}
}
if (j >= subLen) {
return i - j;
}
return -1;
}
public static void main(String[] args) {
System.out.println(BF("ababcabcdabcde", "abcd"));
System.out.println(BF("ababcabcdabcde", "abcde"));
System.out.println(BF("ababcabcdabcde", "abcdef"));
}
}
5
9
-1
2. KMP算法
KMP是一种字符串匹配模式的改进算法
算法核心是:利用匹配失败后的信息,尽量减少主串与子串的匹配次数已达到快速匹配的目的。具体就是通过一个 next[]数组 的实现。
**区别:**主串 i 并不回退,子串 j 也不回退到 0 下标
2.2. 主串为什么不回退
目前在 2下标匹配失败 str[2] != sub[2]
因此按照 BF 算法,主串str 就算回退到 1下标,也是没有必要的
为此大佬们发明出了主串不回退,子串回退的解决方案,那么子串该如何回退呢?
2.3. 子串回退的位置
目前在 5下标匹配失败 str[5] != sub[5]
举个例子,此时匹配失败了。我们不回退 主串str,因为走到这一步说明 主串的i下标之前和子串的j下标之前前面是有一部分相同的 不然下标不可能走到 5
下面是子串回退的最理想位置,子串 j 直接一步回退到 2下标。
问题又来了——该如何回退到 2下标呢?为此大佬们提供了一个 next数组
2.4. 引出 next 数组
KMP的精髓就在于 next数组: 用 next[i] = k 不同的 i 对应一个 K 值,这个 K 就是将来要移动 i 的位置
求K值方法:
规则:找到匹配成功部分的两个真子串「不包含本身」,一个以下标 0 开始,一个以下标 i-1 字符结尾
不管什么数据,next[0] = -1,next[1] = 0 ,因为 0 下标之前没有匹配的字符所以规定是-1,1 下标之前也只有一个字符,但不包含本身所以是 0
求下面的 next 数组
next数组1
省略掉后边的匹配步骤,最终 next数组结果
next数组2这里直接写出结果
看懂这两幅图的关键就是死扣概念:0下标起始,i-1下标结束「就是说0下标起始的字符串终止要和i-1下标字符串相等才行,千万不要异想天开的越过0下标去匹配i-1下标」
能看懂这里,说明已经对 next 数组怎么来的问题不大了。接下来的问题就是 一直next[i]=k,如何求 next[i+1]
如果我们能通过 next[i]经过一些列转换得到next[i+1],那我们就能实现这一部分。
具体该如何做呢?
刚才两个 next 数组感觉学会的朋友可以继续来看这个数组「主要用验证next[i]=k」来检测自己的 next 数组是否掌握会了
next[8] = k = 3 ,为何 next[9] = k = 4 呢?「假设 next[i]=k 是成立的。」
那么 i的回退就是k=3,回到 3 下标,此时来到了 a
我们发现 字符串S[0, 3-1] 和 字符串S[5, 8-1]
也就是:S[0, k-1] 和 S[x, i-1] 相等「下图中红色圈圈部分」
根据字符串长度相等可得:k-1-0 = i-1-x --> x = i-k
换算后就是:S[0, k-1] 和 S[i-k, i-1] 相等
由之前的next[8] 我们再看 next[9]
上图中绿色部分 S[k] = S[i]
根据上边换算后的公式推导即可扩展为
S[0, k-1] == S[i-k, i-1]
由于 S[k]==S[i]
所以可得:S[0,k] == S[i-k,i]
一切的成立都是源于 next[i]=k
结合上述分析,我们再来看最后一步的推导步骤
就有个推论:next[i+1]=k+1
思考: 如果 S[k] != S[i] 那么 next[i+1]= ?
回退规律:根据 next数组,一直回退到 S[k] == S[i]
看懂了上述的解析,就已经了解 60% 的KMP了「后续还有一个代码的实现和 next 数组的优化等着你来学习」
Java代码实现
public static int KMP(String str, String sub, int pos) {
if (str == null || sub == null) return -1;
int lenStr = str.length();
int lenSub = sub.length();
if (lenStr == 0 || lenSub == 0) return -1;
if (pos < 0 || pos >= lenSub) return -1;
int next[] = new int[lenSub];
getNext(sub, next);
int i = pos;
int j = 0;
while (i < lenStr && j < lenSub) {
if (str.charAt(i) == sub.charAt(j)) {
++i;
++j;
} else {
j = next[j];
}
}
if (j >= lenSub) {
return i - j;
}
return -1;
}
private static void getNext(String sub, int[] next) {
next[0] = -1;
next[1] = 0;
int i = 2;
int k = 0;
for (; i < sub.length(); i++) {
if (k == -1 || sub.charAt(i - 1) == sub.charAt(k)) {
next[i] = k + 1;
++k;
++i;
} else {
k = next[k];
}
}
}
public static void main(String[] args) {
System.out.println(KMP("ababcabcdabcde", "abcd", 0));
System.out.println(KMP("ababcabcdabcde", "abcde", 0));
System.out.println(KMP("ababcabcdabcde", "abcdef", 0));
}
5
9
-1
C语言代码实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
void getNext(const char *sub, int *next){
next[0] = -1;
next[1] = 0;
int i = 2;
int k = 0;
int lenSub = strlen(sub);
for(; i < lenSub; ++i){
if(sub[i-1] == sub[k]){
next[i] = k+1;
++i;
++k;
}else{
k = next[k];
}
}
}
int KMP(const char *str, const char *sub, int post){
assert(str != NULL && sub != NULL);
int lenStr = strlen(str);
int lenSub = strlen(sub);
int i = post;
int j = 0;
int *next = (int *)malloc(sizeof(int) * lenSub);
assert(next != NULL);
getNext(sub, next);
while(i < lenStr && j < lenSub){
if(str[i] == sub[j]){
++i;
++j;
}else{
j = next[j];
}
}
if(j >= lenSub){
return i-j;
}
return -1;
}
int main(){
printf("%d\n", KMP("ababcabcdabcde", "abcd", 0));
printf("%d\n", KMP("ababcabcdabcde", "abcde", 0));
printf("%d\n", KMP("ababcabcdabcde", "abcdef", 0));
return 0;
}
代码写到这里,细心的朋友可能会发现套路:这不就是BF算法基础上加了一个 getNext(subStr, next[]) 蛮!
确实是有个小套路!
代码套路:
KMP(str, sub, pos){
int lenStr=str.length();
int lenSub=sub.length();
int i=pos;
int j=0;
int[] next;
getNext(sub, next);
while(i < lenStr && j< lenSub){
if(str[i] == sub[j]){
++i;
++j;
}else{
j = next[j];
}
}
}
getNext(sub, next){
int i=2;
int k=0;
for(; i<sub.length(); ++i){
if(sub[i-1] = sub[k]){
next[i] = k+1;
++k;
}else{
k = next[k];
}
}
}
2.5 next数组的优化
原有的 next数组 回退会发现不能一步到位,回退之后还需要进一步的回退,所以就对next数组做了修改。
细心的朋友可能会专研:为什么会一直倒退呢?
假设在 7下标 遇到了匹配失败,则它需要一步步不回退到
7回到 6,6回5,5回4。。。回到0
因此:
- 如果回退到的位置和当前字符一样,就写则回退到位置的 nextVal 值
- 如果回退到的位置和当前字符不一样,就写当前字符原来的值
练习 nextVal
模式串 abcaabbcabcaabdab ,该模式串的 next 数组的值为( D ) , nextval 数组的值为 (F) 。
A. 0 1 1 1 2 2 1 1 1 2 3 4 5 6 7 1 2
B. 0 1 1 1 2 1 2 1 1 2 3 4 5 6 1 1 2
C. 0 1 1 1 0 0 1 3 1 0 1 1 0 0 7 0 1
D. 0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2
E. 0 1 1 0 0 1 1 1 0 1 1 0 0 1 7 0 1
F. 0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1
|