目录
暴力匹配(BF)算法
基本概念
分析BF算法
代码实现
牛刀小试
BF算法的时间复杂度
KMP算法
基本概念
分析KMP算法
引出next数组
代码实现
关键代码讲解
牛刀小试
KMP算法的时间复杂度
BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。
光看定义晦涩难懂,接下来我将举例子与大家一起学习:
假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.
对于这个问题我们很容易想到:从左到右依次匹配,如果字符相等,都向后移一位,如不相等, 子串从0下标重新开始,主串向右移动一位(假设原来从0下标开始,下次从1下标开始)
我们可以这样初始化:
根据我们的想法,之后我们就需要比较i指针和j指针指向的数字是否一致,如果一致都向后移动,如果不一致,如下图:
b和d不相等,那就把i指针回退到刚刚指针的下一个位置(刚刚指针是从0下标开始的),j指针退回到0下标重新开始。
根据以上的分析,下面我们开始写代码:
C代码:
#include
#include
#include
int BF(char* str1, char* str2)
{
assert(str1 != NULL && str2 != NULL);
int len1 = strlen(str1);//主串的长度
int len2 = strlen(str2);//子串的长度
int i = 0;//主串的起始位置
int j = 0;//子串的起始位置
while (i < len1 && j < len2)
{
if (str1[i] == str2[j])
{
i++;//相等i和j都向后移动一位
j++;
}
else {//不相等
i = i - j + 1;//i回退
j = 0;//j回到0位置
}
}
if (j >= len2) {//子串遍历玩了说明已经找到与其匹配的子串
return i - j;
}
else {
return -1;
}
}
int main()
{
printf("%d\n", BF("ababcabcdabcde", "abcd"));//测试,为了验证代码是否正确尽量多举几个例子
printf("%d\n", BF("ababcabcdabcde", "abcde"));
return 0;
}
java代码:
public class Test {
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;
int j = 0;
while (i < strLen && j < subLen) {
if(str.charAt(i) == sub.charAt(j)) {
i++;
j++;
}else {
i = i-j+1;
j = 0;
}
} i
f(j >= subLen) {
return i-j;
} r
eturn -1;
}
public static void main(String[] args) {
System.out.println(BF("ababcabcdabcde","abcd"));
System.out.println(BF("ababcabcdabcde","abcde"));
}
}
通过上面的学习,对BF算法有了初步的认识,为了更深刻的了解和运用,下面和大家一起完成以下试题:
试题在这里>>实现strStr()
感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;
最好的情况就是从第一次开始就匹配成功时间复杂度为O(1);
最坏的情况就是每次都匹配到最后一个才发现与主串不相同,比如"aaaaab",子串”aab"
看上图,除了最后一次,其余的都是每次匹配到最后,才发现,啊,我们不一样。
这种情况下,上图中,模式串在前 3 次,每次都要匹配 3 次,并且不匹配,直到第 4 次,全部匹配,不需要继续移动,所以匹配的次数为(6 - 3 + 1)* 3 = 12 次。
由此可知,对于主串长度为 n,模式串长度为 m ,最坏情况下的时间复杂度为 O((n - m + 1) * m) = O(n * m)。
相信善于思考的小伙伴会发现,如果是为了寻找的话,根本不需要将i移动到1的位置,因为前面几个字符都是匹配的,再将i移动到1的位置,j移动到0位置,位置错开了,显然也不会匹配,那么我们能不能丢掉以上没必要的步骤,减少指针回溯进行算法简化囊,有一个想法,i位置不动,只需要移动j位置,由此引出我们今天的主人公kmp算法.
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
区别:KMP 和 BF 唯一不一样的地方在,我主串的 i 并不会回退,并且 j 也不会移动到 0 号位置
假定我们给出字符串"ababcabcdabcde"做为主串,然后给出子串"abcd",现在我们需要查找子串是否在主串中出现,出现返回主串中第一个匹配的下标,失败返回-1.
1.首先举例说明,为什么主串不回退
2.j的回退的位置
那么j是怎么回退到下标2的位置的囊?下面我们引出next数组
KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,不同的 j 来对应一个 K 值,这个 K 就是你将来要移动的 j要移动的位置。而 K 的值是这样求的:
求next数组的练习:
练习一:举例对于”ababcabcdabcde”, 求其的 next 数组?
-1 0 0 1 2 0 1 2 0 0 1 2 0 0
练习 2: 再对”abcabcabcabcdabcde”,求其的 next 数组? "
-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
核心的东西来了:
到这里大家对如何求next数组应该问题不大了,接下来的问题就是,已知next[i] = k;怎么求next[i+1] = ?
如果我们能够通过 next[i]的值,通过一系列转换得到 next[i+1]得值,那么我们就能够实现这部分。
那该怎么做呢?
首先假设: next[i] = k 成立,那么,就有这个式子成立:P0...Pk-1 = Px...Pi-1,得到: P0...Pk-1 = Pi-k..Pi-1;如下图分析:
然后我们假设如果 Pk = Pi;我们可以得到 P0...Pk = Pi-k..Pi;那这个就是 next[i+1] = k+1;
C代码:
#include
#include
#include
void GetNext(int* next, char* sub, int len2)
{
next[0] = -1;//规定第一个为-1,第二个为0,则直接这样定义就好了;
next[1] = 0;
int k =0;//前一项的k
int j = 2;//下一项
while (j < len2)
{
if (k==-1||sub[j-1] == sub[k])
{
next[j] = k + 1;
j++;
k++;
}
else
{
k = next[k];
}
}
}
int KMP(char* str, char* sub, int pos)
{
assert(str != NULL && sub != NULL);
int len1 = strlen(str);
int len2 = strlen(sub);
assert(pos >= 0 && pos < len1);
int i = pos;//i从指定下标开始遍历
int j = 0;
int* next = (int*)malloc(sizeof(int) * len2);//动态开辟next和子串一样长
assert(next != NULL);
GetNext(next, sub, len2);
while (i < len1 && j < len2)
{
if (j == -1||str[i] == sub[j])//j==-1是防止next[k]回退到-1的情况
{
i++;
j++;
}
else {
j = next[j];//如果不相等,则用next数组找到j的下个位置
}
}
if (j >= len2)
{
return i - j;
}
else {
return -1;
}
}
int main()
{
char* str = "ababcabcdabcde";
char* sub = "abcd";
printf("%d\n", KMP(str, sub, 0));
return 0;
}
java代码:
public static void getNext(int[] next, String sub){
next[0] = -1;
next[1] = 0;
int i = 2;//下一项
int k = 0;//前一项的K
while(i < sub.length()){//next数组还没有遍历完
if((k == -1) || sub.charAt(k) == sub.charAt(i-1)) {
next[i] = k+1;
i++;
k++;
}else{
k = next[k];
}
}
}
public static int KMP(String s,String sub,int pos) {
int i = pos;
int j = 0;
int lens = s.length();
int lensub = sub.length();
int[] next= new int[sub.length()];
getNext(next,sub);
while(i < lens && j < lensub){
if((j == -1) || (s.charAt(i) == sub.charAt(j))){
i++;
j++;
}else{
j = next[j];
}
}
if(j >= lensub) {
return i-j;
}else {
return -1;
}
}
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));
}
else{
j=next[j]
}
if (j == -1||str[i] == sub[j])
{
i++;
j++;
}
问题:为啥还有个j==-1?
如下图所示:当第一个字符就不匹配,i,j此时都是0,j=next[j] >> j=next[0] >> j=-1; 此时j是-1,如果不添加j==-1这种情况,那么这个程序将结束返回没有匹配,但你仔细观察下图,P[5]~P[8]与子串相匹配,故答案显然错误.所以我们应该加上j==-1这种情况,让其从头再遍历;
next[0] = -1;
next[1] = 0;
int k =0;//前一项的k
int j = 2;//下一项
根据我们的规定next数组第一个和第二个数为-1和0,故没啥问题。k=0是前一项k的值,j=2是下一项.
if (k==-1||sub[j-1] == sub[k])
{
next[j] = k + 1;
j++;
k++;
}
根据上面的内容我们可知,p[j]==p[k],next[i]=k;则能推出next[i+1]=k+1;如下图所示,不过这里i是j-1,大家要注意这一点, p[j]==p[k]>>sub[j-1]==sub[k];next[i+1]=k+1>>next[j]=k+1;
else
{
k = next[k];
}
这个知识点上面讲过,当p[j]!=p[k]时候,k回退,一直找到p[j]==p[k]然后用这个next[i+1]=k+1;
题目在这里>>重复的子字符串
感兴趣的伙伴可以去做试试,下一章我们进行共同探讨;
假设在M字符串中找N字符串的起始位置,长度分别为m和n,使用KMP算法,一般认为时间复杂度是O(m+n),也就是计算next数组的时间复杂度是O(n),而匹配的时候是O(m).
以上是KMP算法的讲解,有不足的地方或者对代码有更好的见解,欢迎评论区留言共同商讨,共同进步!!