菜鸟都能理解的KMP算法

KMP算法其实是一个O(n)的字符串匹配算法

A = "ababacbacab"

B = "baca"

假设位置从1开始

这样可以说B是A的一个子串,首先我们想到的办法是枚举A的位置,比如

1.首先枚举位置A[1],即字符'a',然后从A[1]开始比较"abab"是否等于"baca",显然不等

2.接着枚举位置A[2],即字符'b',然后从A[2]开始比较"baba"是否等于"baca",显然,又不匹配

......

经过不断得尝试,我们终于枚举到一个位置A[7]开始的子串"baca"与B相等,my god,真不容易

我们来计算一下时间复杂度,我们枚举A的位置,最多有O(A.length())个位置,并且每个位置最多要匹配O(B.length()),所以,算法复杂度当然是O(A.length()*B.length())的啦


下面我们来见识一下神奇的看毛片算法

看毛片算法的思想是用两个指针i和j来指示A和B中的一个位置

1) 用i来表示当前匹配到A中的哪个位置啦

2) 用j来表示当前匹配到B中的哪个位置啦

3) 并且要满足B[1...j]要和A[i+j-1...i]相等

哦?很难理解啊,下面的图(图1)应该使你能够一拍脑袋,“哦,我太聪明了,这么简单!”


图1

灰色部分相等的啦^ ^


算法开始前,我们先考虑下面一个东东

    指示数组p满足p[j]表示使得B[1...p[j]] = B[j-p[j]+1...j](图3)

    

  图3(图片太大,新窗口打开吧亲)

灰色的两个部分相等

简单推理一下,你猜猜看三个红色的部分是不是也相等(请务必搞懂,非常重要)?答案是肯定的


算法开始咯我们要考虑这个问题,i和j应该怎么增长

①  如果A[i+1] = B[j+1]

     那么,我们只需要将两个灰色框子往后面拖动一格就行了哇(图2),并且,如果j变成B.length(),那么我们的匹配过程就结束咯,聪明的你一定理解吧?


图2

灰色部分相等的啦^ ^


② 如果A[i+1] != B[j+1]呢?

回顾一下i和j的定义先(看图1即可),我们可以将j改为p[j],显然更改后的p[j]任然满足图1哦,改完后,图1接下来扩展成介个样子哩(图4)


图4(对照图1和图3看哦)


调整了一下j的位置,我们又得到了一个新的j啦(j = p[j]),聪明的你如果看不懂图4只能说明。。。傻傻的我讲述得还不够清楚,欢迎留言开骂~

接下来呢,我们得看看B[p[j]+1] == A[i+1]成立否,这里,我们不知不觉又递归到“算法开始咯”,请你去递归着玩玩吧。。。


很开心地告诉你,看到这里,你已经几乎明白了KMP算法的整个流程,可以去上个厕所先,回来之后我们继续剩下的步骤,喝口茶,慢慢欣赏吧








欢迎回来,呃,继续....

罗里吧嗦一大堆,终于可以上代码咯,你会发现这个过程如此简单,亮瞎你的钛合金神眼,这就是编程之美,上!


  1.   for (j = 0, i = 0; i < n; ++i)  
  2.   {  
  3.     while (j > 0 && B[j+1] != A[i+1]) j = p[j];//看图4啦  
  4.     if (B[j+1] == A[i+1]) ++j;  
  5.     if (j == m)  
  6.     {  
  7.       //如果B串都结束了,那么显然成功匹配咯  
  8.       cout << "匹配成功: " << i - m + 1 << endl;  
  9.       j = p[j]; //使得可以继续匹配下去,这个你自己画个图就明白为啥要这么做咯  
  10.     }  
  11.   }  
  for (j = 0, i = 0; i < n; ++i)
  {
    while (j > 0 && B[j+1] != A[i+1]) j = p[j];//看图4啦
    if (B[j+1] == A[i+1]) ++j;
    if (j == m)
    {
      //如果B串都结束了,那么显然成功匹配咯
      cout << "匹配成功: " << i - m + 1 << endl;
      j = p[j]; //使得可以继续匹配下去,这个你自己画个图就明白为啥要这么做咯
    }
  }



复杂度分析:我们首先观察i,由于我们是依次扫描A数组中i的位置,所以你有木有发现i一直在增加,并且i只能增加A.length()次?而++i,++j是靠在一起的语句,所以呢j最多也只能增加A.length()次,并且j = p[j]语句说明j一直在减少,由于j只能增加A.length()次,所以j顶多也只能减少A.length()次,否则,j不是成了负数了嘛,而我们观察while(1)里面的内容发现,j每次不是增加就是减少,所以增加和减少(最外层的if elseif)两个语句加起来顶多执行2*A.length()次,所以呢,这个时间按复杂度当然是O(A.length())的啦


聪明的你一定还发现我们还有一个问题木有解决,就是,指示数组P!,我会告诉你P通过O(B.length())就能搞定的么?

假定我们已经知道p[1], ...p[j]是多少了,我们如何来构造p[j+1]呢,诶呀,我都口干舌燥了,喝口水慢慢来,上个图先(图5)


           图5(最好保存下来看,图太大了,坑爹的csdn没有滚动条!!!)

相信你仔细观察上图之后我不用多做解释就能知道p[j+1]的构造过程了吧,你只需要搞清楚p[j]的定义,一切问题都是很轻松的KO掉的

p构造过程源码如下


  1.   p[1] = 0;  
  2.   for (j = 1; j <= m - 1; ++j)  
  3.   {  
  4.     k = j;    
  5.     while (k > 0 && B[j+1] != B[p[k]+1]) k = p[k];  
  6.     p[j+1] = (k!=0 ? p[k]+1 : 0);  
  7.   }  
  p[1] = 0;
  for (j = 1; j <= m - 1; ++j)
  {
    k = j;  
    while (k > 0 && B[j+1] != B[p[k]+1]) k = p[k];
    p[j+1] = (k!=0 ? p[k]+1 : 0);
  }



最后,放出完整源码

  1. #include <iostream>  
  2. #include <string>  
  3. using namespace std;  
  4.   
  5. int main() {  
  6.   string A = " ababababafcbaababafcc";  
  7.   //string A = " ababafcc";  
  8.   string B = " ababafcb";  
  9.   int p[20];  
  10.   int n = A.size() - 1;  
  11.   int m = B.size() - 1;  
  12.   int i, j, k;  
  13.   
  14.   p[1] = 0;  
  15.   for (j = 1; j <= m - 1; ++j)  
  16.   {  
  17.     k = j;  
  18.     while (k > 0 && B[j+1] != B[p[k]+1]) k = p[k];  
  19.     p[j+1] = (k!=0 ? p[k]+1 : 0);  
  20.   }  
  21.   
  22.   
  23.   //进行匹配  
  24.   for (j = 0, i = 0; i < n; ++i)  
  25.   {  
  26.     while (j > 0 && B[j+1] != A[i+1]) j = p[j];  
  27.     if (B[j+1] == A[i+1]) ++j;  
  28.     if (j == m)  
  29.     {  
  30.       cout << "匹配成功: " << i - m + 1 << endl;  
  31.       j = p[j];  
  32.     }  
  33.   }  
  34.   return 0;  
  35. }  
#include <iostream>
#include <string>
using namespace std;

int main() {
  string A = " ababababafcbaababafcc";
  //string A = " ababafcc";
  string B = " ababafcb";
  int p[20];
  int n = A.size() - 1;
  int m = B.size() - 1;
  int i, j, k;

  p[1] = 0;
  for (j = 1; j <= m - 1; ++j)
  {
    k = j;
    while (k > 0 && B[j+1] != B[p[k]+1]) k = p[k];
    p[j+1] = (k!=0 ? p[k]+1 : 0);
  }


  //进行匹配
  for (j = 0, i = 0; i < n; ++i)
  {
    while (j > 0 && B[j+1] != A[i+1]) j = p[j];
    if (B[j+1] == A[i+1]) ++j;
    if (j == m)
    {
      cout << "匹配成功: " << i - m + 1 << endl;
      j = p[j];
    }
  }
  return 0;
}


感谢matrix67大神的文章http://www.matrix67.com/blog/archives/115

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于计算机专业的学生而言,参加各类比赛能够带来多方面的益处,具体包括但不限于以下几点: 技能提升: 参与比赛促使学生深入学习和掌握计算机领域的专业知识与技能,如编程语言、算法设计、软件工程、网络安全等。 比赛通常涉及实际问题的解决,有助于将理论知识应用于实践中,增强问题解决能力。 实践经验: 大多数比赛都要求参赛者设计并实现解决方案,这提供了宝贵的动手操作机会,有助于积累项目经验。 实践经验对于计算机专业的学生尤为重要,因为雇主往往更青睐有实际项目背景的候选人。 团队合作: 许多比赛鼓励团队协作,这有助于培养学生的团队精神、沟通技巧和领导能力。 团队合作还能促进学生之间的知识共享和思维碰撞,有助于形成更全面的解决方案。 职业发展: 获奖经历可以显著增强简历的吸引力,为求职或继续深造提供有力支持。 某些比赛可能直接与企业合作,提供实习、工作机会或奖学金,为学生的职业生涯打开更多门路。 网络拓展: 比赛是结识同行业人才的好机会,可以帮助学生建立行业联系,这对于未来的职业发展非常重要。 奖金与荣誉: 许多比赛提供奖金或奖品,这不仅能给予学生经济上的奖励,还能增强其成就感和自信心。 荣誉证书或奖状可以证明学生的成就,对个人品牌建设有积极作用。 创新与研究: 参加比赛可以激发学生的创新思维,推动科研项目的开展,有时甚至能促成学术论文的发表。 个人成长: 在准备和参加比赛的过程中,学生将面临压力与挑战,这有助于培养良好的心理素质和抗压能力。 自我挑战和克服困难的经历对个人成长有着深远的影响。 综上所述,参加计算机领域的比赛对于学生来说是一个全面发展的平台,不仅可以提升专业技能,还能增强团队协作、沟通、解决问题的能力,并为未来的职业生涯奠定坚实的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值