前言
本人是一个刚刚上路的IT新兵!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!
提示:本篇文章,文字较多,还请各位细细咀嚼
一、KMP算法是什么?
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
🤑🤑🤑那么就通过举例来讲解一下。
KMP算法是高效的字符串匹配算法
在主串中寻找这个字串的第一个下标
所以KMP算法的时间效率更高。
为啥KMP会将 j
移动到 3 位置呢🧐🧐
原因就是:
此时, i
走到了 7 位置, j
走到了 7 位置,就代表
主串中的[0,6]位置字符
与
子串中的[0,6]位置字符
一定相同。
[x,y]
下标 表示x,x+1,x+2,...y
这些下标
子串[0,2]
下标的字符与子串[4,6]
下标的字符相同,
主串[4,6]
下标的字符与子串[4,6]
下标的字符相同,
所以子串[0,2]
下标字符与主串[4,6]
下标字符相同。
所以 i
不需要动位置,把 j
挪动到3
下标
此时子串[0,2]
下标字符与主串[4,6]
下标字符相同,无需重复进行比较。可以直接从3
下标开始比较。
👀👀下面的推导是重点😇😇
推导至一般化
- 无论主串中
i
在什么位置,
如果子串中的[ 0 , x - 1 ]
位置的字符串 (必须以0下标开头)
与
子串中的[ j - x , j - 1]
位置的字符串 (必须以j-1下标结尾)
相同;
那么在j
位置匹配失败时,就将j
移动到x位置。(x为两个相同字符串的长度)
KMP算法的核心思想:通过上述推导的思想,不让主串下标 i
再移动位置了,当子串下标 j
位置匹配失败时,重新将 j
移动到合适的位置,再继续匹配
二、子串下标的移动
- 移动方法:在下标
[ 0 , j - 1]
范围中寻找一个以0
下标为开头,一个以j - 1
下标为结尾的,两个相等的字符串,他们的长度就是在j
下标匹配失败后,子串下标j
要移动的位置。
举例:
A.
背景:在主串的 X
下标处与子串中的 Y
下标处字符匹配失败,且 X
与 Y
均合法, X
>= Y
。
假设在 7
下标的字符匹配失败。
所以要在子串中找到两个相同的以 0
下标 a
字符开头,以 7 - 1
下标 c
为结尾的两个相同的子串。(相同,但不能是同一个子串)
如图,下标[ 0 , 2 ]
与下标[ 4 , 5 ]
两个字符串相同。
所以当在 7
下标位置匹配失败时,应 主串下标 i
不变,子串下标 j
移至 3
位置然后继续匹配。
B.
假设在 5
下标的字符匹配失败。
所以要在子串中找到两个相同的以 0
下标 a
字符开头,以 5 - 1
下标 a
为结尾的两个相同的子串。(相同,但不能是同一个子串)
如图,下标[ 0 , 0 ]
与下标[ 4 , 4 ]
两个字符串相同。
所以当在 5
下标位置匹配失败时,应 主串下标 i
不变,子串下标 j
移至 1
位置然后继续匹配。
C.
假设在 4
下标的字符匹配失败。
所以要在子串中找到两个相同的以 0
下标 a
字符开头,以 4 - 1
下标 a
为结尾的两个相同的子串。(相同,但不能是同一个子串)
如图,下标[ 0 , 2 ]
与下标[ 1 , 3 ]
两个字符串相同。
所以当在 4
下标位置匹配失败时,应 主串下标 i
不变,子串下标 j
移至 3
位置然后继续匹配。
在这里我们引入两个变量;
int k;//代表子串中以0下标开头的字符串的结尾
int j;//代表子串中以j-1下标结尾的字符串的结尾
🤪🤪🤪重点重点重点!!!!
无论A,B,C哪种情况,
在k != -1
时,k 与 j 都有一个恒定不变的关系
下标[ 0 , k ]
的字符串与下标[j - 1 - k , j - 1 ]
的字符串相同
在k == -1
,那么就说明不存在两个这样的字符串,此时,就需要i++,去匹配后面的字符
所以我们可以牺牲空间复杂度,来换取时间效率。
即创建一个next数组,里面存储子串在 j
下标匹配失败后,应该移动到什么位置,
当子串匹配失败后,主串下标 i
不变,子串下标挪动到合适的位置。
三、创建next数组
1.理论
- 如果在子串的
0
下标处就匹配失败了,那么下一次匹配应该还是在0
下标处匹配,那这样就会变成死循环, - 如果在子串的
0
下标处就匹配失败,那么i就应该往下走一个,才有可能找到匹配的字符串。所以令next[0] = -1,通过让程序识别-1来操控主串下标i
。 - 如果在子串的
1
下标处匹配失败,1
下标前就只有一个字符,不可能有两个相同字符串的。所以next[1] = 0;
那么后面的next数组就要咱们自己写算法了。
其实这个算法也不难写,在写算法之前,
我们先将《字串下标移动》中得出的一个重点重点重点的结论与next数组进行关联🤗🤗
🤪🤪🤪超级重点重点重点!!!!
假设 next[
j
] = x
在x != -1
时,x 与 j 都有一个恒定不变的关系
下标[ 0 , x - 1 ]
的字符串与下标[j - x , j - 1 ]
的字符串相同
在x == -1
,那么就说明不存在两个这样的字符串,此时,就需要i++,去匹配后面的字符
如果有小伙伴对这个结论有些疑惑,先带着疑惑继续往下看,下面会为大家揭秘疑惑的!!
int k = 0;//代表必须以0下标开头的字符串的结尾
int j = 2;//代表必须以j-1下标结尾的字符串的结尾
此时第一组《子串下标移动》的B情况,
即next[2] = 1
;即
next[j] = k+1;
k++;
j++;
即此时的 0
, 1
下标处的字符一定相同,
第二组,符合上述,字符下标的移动中的第一种情况。
即next[3] = 2
;
即
next[j] = k+1;
k++;
j++;
即此时的 0
, 1
, 2
下标处的字符一定相同,
此时大家应该很好奇,为什么 j
与 k
都是字符串的结尾呢??
难道开头和中间的字符不需要比较吗??
如果有这个疑惑的小伙伴,请移步《子串下标的移动》中得出的重点重点重点结论,同时这个结论也可以解释为什么 next[ j ] = k + 1;
至于为什么要+1,是因为下标[ 0 , k ]
的字符串与下标[j - 1 - k , j - 1 ]
的字符串相同
所以要比较的是下标 k + 1
与 下标 j
处字符。
那么当大家明白了为什么 next[ j ] = k + 1;
也就解答了超级重点重点重点的结论中的,
当next[ j
] = x时,
为什么是下标[ 0 , x - 1 ]
的字符串与下标[j - x , j - 1 ]
的字符串相同。
到了这里我们就需要明白,无论是《子串下标移动》中 A,B,C中的哪一种情况,
当 k
下标与 j - 1
下标相等时,才能确定 next[ j ]
的值,等于 k + 1;
当 k
下标与 j - 1
下标不相等时,就利用超级重点重点重点结论
去让k = next[ k ];
从而去寻找A,B,C三种情况中的一种,直到当 k == -1
时,说明A,B,C三种情况都不存在,那么他就只能从0下标重新开始匹配,并且主串下标也要+1。
🤩🤩切记next数组的意义
在一般情况下,利用k = next[ k ],进行跳跃,可以帮我们直接跳过中间不可能形成符合条件的字符串的字符,从而更加节省时间。
2.图解
就模拟到这里,当 k
下标与 j - 1
下标处字符不相等,让k = next[ k ] 可以让我们跳过很多不符合条件的字符,从而节省更多时间。
四、KMP完整代码
public static int KMP(String str,String sub,int pos) {
//判断两个串都不为空,与pos的合法性
if(str == null || sub == null || pos > str.length() - sub.length()) {
return -1;
}
//next数组,代表在子串的第j个下标匹配失败时,j应该回退到的位置。
int[] next = new int[sub.length()];
getNext(sub,next);
int i = pos;//主串下标,从pos位置开始
int j = 0;//子串下标
while (i < str.length() && j < sub.length()) {
//j == -1,代表字串的第一个字符就与i位置的字符不同,所以i++,去i+1的位置继续去匹配字串
//j++ 使从子串0下标重新开始匹配。
//如果相同,那么j与i都往后走,继续匹配后面的字符
if(j == -1 || str.charAt(i) == sub.charAt(j)) {
j++;
i++;
} else {
//证明不是第一个字符不相同,所以按next数组中的值去回退位置
j = next[j];
}
}
//返回子串开始出现在主串的第一个下标
if(j == sub.length()) {
return i - j;
}
//走到这一步说明,子串在主串中不存在,返回-1
return -1;
}
private static void getNext(String ary, int[] next) {
next[0] = -1;
//判断数组长度
if(next.length <= 1) {
return;
}
next[1] = 0;
int j = 2;
int k = 0;
while (j < ary.length()) {
if(k == -1 || ary.charAt(j - 1) == ary.charAt(k)) {
next[j] = k + 1;
j++;
k++;
} else {
k = next[k];
}
}
}
总结
以上就是今天要分享的KMP算法了,本文仅是个人对KMP算法的想法,文章中的重点重点重点理论
与超级重点重点重点理论
一定要理解,🤗🤗🤗才能更好的明白如何创建next数组。如有错误,还请各位在评论区讨论指正。
路漫漫,不止修身也养性。