【手把手带你学会KMP算法】

简介: 【手把手带你学会KMP算法】

相信大家在遇到字符串匹配问题时,无论是听老师上课讲还是在网上查询资料时几乎都会用到KMP算法,本篇博客借鉴于大博哥对于KMP算法的分析以及自身对于KMP算法的看法,相信认真看完了后会对你有一些帮助。

1 BF 算法

了解KMP算法之前,我们先来回忆一下BF算法(暴力求解),基本思想就是主串中元素与子串中元素一一比较,匹配失败就让子串返回到0下标,主串回溯到开始匹配的下一位。

假设主串的长度位M  子串的长度为N  BF算法的时间复杂度为 O(M*N)

动图详解(此图借鉴于其他博主):

62d9a05ace22404e8ea4edb1454e4e48.gif

具体代码:

int BF(char* mainStr, char* subStr,int pos)
{
  assert(mainStr && subStr);
  if (!*subStr)
  {
    return 0;
  }
  if (!*mainStr)
  {
    return -1;
  }
  int lenMain = strlen(mainStr);
  int lenSub = strlen(subStr);
  int i = pos; //遍历主串
  int j = 0; //遍历子串
  while (i < lenMain && j < lenSub)
  {
    if (mainStr[i] == subStr[j])
    {
      i++;
      j++;
    }
    else
    {
      i = i - j + 1;
      j = 0;
    }
  }
  if (j == lenSub)
  {
    return i-j;
  }
  else
  {
    return -1;
  }
}

2 KMP 算法

KMP算法的诞生:KMP算法是三位大牛:Knuth、Morris和Pratt同时发现的,于是取了他们名字的首字母然后组合起来,就成了该算法的命名。(来自百度)

kmp算法的主要思想是:在主串中查找子串时,利用主串和子串中已经匹配的部分,跳过一些无用的比较,使主串的标记指针不会回溯,子串的标记指针移动。其实主要是子串中如果有部分内容和主串中一致,我们需要研究这些已知的匹配内容,这相当于我们需要研究子串,发现子串中存在的规律。KMP算法的时间复杂度为 O(M+N)

听起来是不是有点懵,没关系,我们来举个栗子:

主串:ababcabcdabcde

子串:abcd

由上述主串与子串数据可知目前在索引为2匹配失败,就算让主串回退到1也是没有必要的,因为主串中1位置字符b与子串0位置的a也不一样,所以我们的目的是:主串是不回退的,让子串回退到特定的位置。

那么子串应该回退到什么位置呢? 再来举个栗子:

主串:abcababcabc

子串:abcabc

这里主串与子串在索引为5的位置不匹配,由于主串不回退,回退的是子串,通过肉眼观察我们发现子串回退到索引为2是比较合理的(我们要尽量在主串中找到和子串匹配的一部分串),由于主串中索引为5的字符a!=子串中索引为2的字符c,所以还要继续回退,直到回退到与字符a相等为止。那如果字符数量多了用肉眼观察肯定是不切合实际的,所以我们引出了next数组,用来保存子串中某个位置匹配失败后回退的位置。

KMP的精髓就是next数组,那么应该怎样来求next数组呢?假定next[j]=k;

求k的规则:

找到匹配成功部分的两个相等的真子串(不包含本身)一个以下标0开始,另一个以j-1下标结束。

总有next[0]=-1,next[1]=0。

有了上面的规则我们就可以来求next数组了:

栗子1:“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]

通过上面的栗子我们可以得出总结:找的时候可以找重复的,但是必须是以0下标开始,找的前一位下标结束;而且数组中元素增大只能一个一个的加,减小可以跳跃减小。

到这里我相信大家对求next数组应该没有什么问题了。但现在的问题是我们如何用公式来求next数组,总不能next数组让我们人为来算,这显然是不现实的。

我们假设已知:next[i]=k;

                    k         i
给定一个字符串" a b c a b a b c a b c "
             0 1 2 3 4 5 6 7 8 9 10
next数组:    [-1,0,0,0,1,2,1,2,3,4,5]

我们假设此时i==8

则我们可以知道:p[0]……p[k-1] == p[i-k]……p[i-1]

所以由:next[i]=k  <--->   p[0]……p[k-1] == p[i-k]……p[i-1]

如果现在 p[k]==p[i]  将其分别加在上式右边的两边得:

p[0]……p[k] == p[i-k]……p[i]

所以不难推出:next[i+1]=k+1;这也很好得解释了为什么增加只能一个一个的加。

那么问题又来了,如果p[k] != p[i]呢?

假设索引为8的元素由a改为b,那么k就要回退到next[k]的位置,也就是k=0,但还是不满足p[k]==p[i],就继续回退k=-1,此时已经找不到了,就直接将0给予给下一位,也就是:

next[i+1]=0;

好了,next数组的求解概念已经掌握,现在我们如何用代码来将其表示出来呢?

代码的具体实现:

int KMP(char* mainStr, char* subStr, int pos)
{
  assert(mainStr && subStr);
  if (!*subStr)
  {
    return 0;
  }
  if (!*mainStr)
  {
    return -1;
  }
  int lenMain = strlen(mainStr);
  int lenSub = strlen(subStr);
  //动态申请next数组
  int* next = (int*)malloc(sizeof(int) * lenSub);
  if (NULL == next)
  {
    exit(-1);
  }
  GetNext(next,subStr);
  int i = pos; //遍历主串
  int j = 0; //遍历子串
  while (i < lenMain && j < lenSub)
  {
    if (mainStr[i] == subStr[j] || -1==j)
    {
      i++;
      j++;
    }
    else
    {
      j = next[j];
    }
  }
  if (j == lenSub)
  {
    free(next);
    return i - j;
  }
  else
  {
    free(next);
    return -1;
  }
}

这里面有一个容易出错的点,当j==-1时也不要忘记了处理,当j==-1时说明回到了索引为0的位置,并且还没有找到与其匹配的项,就让j从索引为0开始与i一一比对。

得到next数组的具体代码:

void GetNext(int* next, int* subStr)
{
  assert(next && subStr);
  int lenSub = strlen(subStr);
  next[0] = -1;
  next[1] = 0;
  int i = 2;
  int k = 0;
  while (i < lenSub)
  {
    if ((-1 == k) || subStr[i - 1] == subStr[k])
    {
      next[i] = k + 1;
      i++;
      k++;
    }
    else
    {
      k = next[k];
    }
  }
}

由于这里的next[i]是未知的,而next[i-1]才是已知的,所以上面才是subStr[i - 1] == subStr[k]

next[i] = k + 1; 同理当k==-1时说明到索引为0都还没有匹配的项就把其置为0.

3 KMP算法的优化

假定给一个这样的字符串: " a a a a a a a a a a g "

它的next数组为:               [-1,0,1,2,3,4,5,6,7,8,9]      

假设在索引为9的位置匹配失败,则j会不断回退直到j==-1,这样效率就比较低了,有什么办法能够一步到位回退呢?

我们不妨优化一下next数组:为了区别,不妨将将优化后的数组取名为nextval

nextval数组的规则如下:

1 如果回退到的位置和当前字符一样,就写回退那个位置的nextval值;

2 如果回退到的位置和当前字符不一样,就写当前原来的nextval值;

举个栗子:给" a  b  c  a  a  b  b  c  a  b  c  a  a  b  d  a  b "

next:               [-1, 0, 0, 0, 1, 1, 2, 0, 0, 1, 2, 3, 4, 5, 6, 0,1]

nextval:          [-1, 0, 0,-1, 1, 0, 2, 0,-1, 0, 0,-1, 1, 0, 6,-1,0]

具体代码:

void GetNextval(int* nextval, int* subStr)
{
  assert(nextval && subStr);
  int lenSub = strlen(subStr);
  nextval[0] = -1;
  nextval[1] = 0;
  int i = 2;
  int k = 0;
  while (i < lenSub)
  {
    if ((-1 == k) || subStr[i - 1] == subStr[k])
    {
      nextval[i] = k + 1;
      i++;
      k++;
    }
    else
    {
      k = nextval[k];
    }
  }
  i = 2;
  while (i < lenSub)
  {
    if (subStr[i] == subStr[nextval[i]])
    {
      nextval[i] = nextval[nextval[i]];
    }
    i++;
  }
}

好了,KMP算法就到这里了,如果有什么不对的地方欢迎各位佬在评论区指正。

目录
相关文章
|
6月前
|
算法
数据结构中的KMP算法及其改进算法
KMP算法通过引入部分匹配表,有效避免了重复计算,从而将字符串匹配的时间复杂度降低到O(m+n)。通过进一步优化next数组,KMP算法的效率得到了进一步提升。对于大规模字符串匹配问题,KMP算法及其改进算法提供了高效的解决方案,是计算机科学领域的经典算法之一。
104 3
|
2月前
|
算法
第四章 KMP算法理论基础
第四章 KMP算法理论基础
22 0
|
2月前
|
算法
KMP算法
KMP算法
39 0
|
4月前
|
算法 C++
A : DS串应用–KMP算法
这篇文章提供了KMP算法的C++实现,包括计算模式串的next数组和在主串中查找模式串位置的函数,用于演示KMP算法的基本应用。
|
5月前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
140 1
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
4月前
|
算法
KMP算法
KMP算法
37 0
|
5月前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
6月前
|
人工智能 算法 BI
一篇文章讲明白KMP算法(俗称看毛片算法)
一篇文章讲明白KMP算法(俗称看毛片算法)
65 0
|
6月前
|
算法 Java
Java数据结构与算法:字符串匹配算法之KMP算法
Java数据结构与算法:字符串匹配算法之KMP算法
|
7月前
|
算法