朴素查找子串算法和KMP算法

 如何在一个字符串查找子串呢?按照我们以前学过的查找方法无非是这样的。
         假如就给定字符串A为“ababcabcd.....”,设要从A查找的子串a为"abc",我们就同时遍历A串,和a串,分别用i和j记录遍历的下标,如果A串的i和a串的j 各自对应的字符一样,我们就让i,j同时往后+,然而大部分情况是i和j还没到它们各自的终点就出现匹配失败了。匹配失败子串的i要置0,j退回到j这轮开始的下一个位置。
       



然而这种查找速度太慢,时间复杂度过高,有太多不必要的遍历。
何为“不必要”?
举个栗子
就拿"ababcabcd.....”中找"abc"来说吧,第一次从i=0开始找子串,i到第三位就失配了腰折了,按照朴素查找的思想,我们要把i再返还到i=0的下一个i=1;可是我们用眼睛看看,i=1的开头是'b',和子串a j置为0的'a',开头就不一样了!那我们还有必要把i退到i=1这个不必要的位置吗?显然没有必要。

        可是如果i不退的话,可能又会错过匹配成功的地方。也就是说一轮和子串匹配下来 各种++后的i  和 各种++前的原始i之间会有一处地方k可能会匹配成功,并且那个k才是我们真正想要的退回的地方。
然而我们再想想还要什么可以再优化的,既然是A串和a串匹配直到i的位置失配了,是不是代表i之前  A串和a串匹配成功的内容全部一样的,我们刚说了让i退到某个地方k,那是不是转换成 i不退,子串a的j退到k。
      换句话说,如果假如真的i退到某个k地方,那么j置为0,i和j肯定又能匹配成功同步++直到上一轮的i位置,最后的结果是i回到上轮的原位置,而j到了k的位置,这都是我们预料到的100%会发生的事情,那么何必让计算机再重复这些操作增加复杂度呢?
    别乱,千万别乱,请想清楚这步再往下看,否则再往下也是天书!
    再简化说,我们先不管A串什么样子,我们就光看a串,因为之前匹配,所以就可以通过a串知道A串是什么样子!然后我们只要对a串做一些牛逼的想法就行了,什么牛逼的想法?我们直到我们要极力避免的情况就是进行不必要的匹配判断, 我们要找到a串匹配失败元素前中重复的段落,a串匹配失败前的段位是和A串的我们正用的一部分是完全一样的。所以a串中找到重复的东西,就相当于映射了A串中的重复。
比如“ababcsdasdsa”中找"ababe"   我们直到i和j都到第5位的时候,'c'和'e'匹配会失败,而两串之前的"abab"的部分是一样的吧!然而按照朴素思想,我们要把i回到第一个'b'位置,可是我们明知'b'开头是不会有结果的!
而后面第二组的“ab”和第一组“ab”一样,那我们为什么不从第二组的“ab”开始?然而与其让j置0,i从第二组“ab”开始,为什么不让i不动,j从第二组"ab"开始?就目前来看两个串的前四位似乎没有什么不同吧?那么下一次匹配的时候,i并没有动!而j也没有退到0那么远的位置,而是只退了2步,到了第二组“ab”的开头。



   不管上述原理你有没有听懂,你也不必纠结,请记住我们只对a串即要查找的小串进行处理,对于被查找的大串A串是不用管的,其中自然有对应关系!
    而我们要找到的东西就是子串失配前的那段中是否存在两个相同的 最大真子串,如果有,这两个最大真子串有多大。我们要靠它来确定j的回退的地方!。
   而约束这两个最大真子串的条件也是有的, 一条真子串要以0开头,另一条要以失配位的前一位为结尾
 还有一个问题,为什么要是最大真子串?你想想如果任意要任意的子串,是不是违背了我们的初衷,会有不彻底的重复段落的筛选,简单来说,一条真子串就是一个字母在0位,另一个真子串也是相同的字母 位置在失配前一位,中间有大量类似“abababa”这样的,你看看你这样要重复判断多少次!
  然而如何求串中每个位置出现失配情况下的它之前的两个最大真子串长度呢。(如果没有就是0)
  
  我们随便举个例子看看
  “ abababcdab”
    -1
     a失配了,可它前面并没有任何串了,比较特殊,记为-1;
 
 “a bababcdab”
    -10
     第二位b失配了,它前面只有一个“a”串,真子串是不能和它的父串完全等同的,所以没有,记为0
  
  “ab ababcdab”
    -100
    第三位a失配,它前面的串是“ab”   很显然,它只有“a”和“b”两个真子串。按规则来,也是没有相同的真子串的,记为0
  “
      aba babcdab”
    -1001
   第四位b失配,它前面的串是“aba”,“a”和“a”两个真子串符合规则,记为1

      abab abcdab”
     -10012
    第五位a失配,它前面的串是“abab”,“ab”和“ab”,记为2

      ababa bcdab”
     -100123
    第6位b失配,它前面的串是“ababa”,“aba”和“aba”,记为3

       ababab cdab”
      -1001234
    第7位c失配,它前面的串是“ababab”,“abab”和“abab”,记为4
     

      ababab c dab”
      -10012340
    第8位d失配,它前面的串是“abababd”,然而并没有0号位开始的'd'结尾的真子串,记为0
     
......

 我们把从这个串里找到这些数字称之为next数组,只要找到了这些数组,我们就知道了每次匹配失败后,j究竟该回到哪个位置!
  那么它如何求呢?
  除了next[0]和next[1]固定为-1和0,我们可以发现规律,就是next数组的增长最多为1,降低不确定。增加的触发条件是什么?

我把next增长成功的一部分截了下来,不难发现,每次新增的 失配位的前一位 如果和 上次记录第一个最大真子串的 下一个字符  一样的话,就可以增长。
而且增长规律也很简单,因为每次最大比上次多出一位,所以next元素也最多加1.
    
可是如果,每次新 增的失配位的前一位 如果和上次记录的第一个最大真子串的下一个字符  不一样的话,那么规律又是什么呢?明显没有增长那么简单。

正当我们毫无头绪的时候,我们看看下面这张增长失败的图。
蓝色表示新增的不一样。


眼熟不!!!
是不是我们最开始找子串匹配失败的情况!
我们要做什么???是不是让下标往回退!退到next[它自己位置]的地方!如果退的地方还是出现
失配位的前一位 和 退到的位置的 下一个字符  不一样的话怎么办?
那就继续退,直到退到-1.无路可退位置!
这里就凸显了第一位设为-1的智慧,如果设为0的话,next数组中间也有0存在,会出现误判!
到此next数组的求法就出来,next数组求出之后,我么只要把朴素查找稍作修改,就可以变成变态的KMP算法!
怎么修改。朴素算法的匹配成功代码块不动,只要把失配时   i=i-j+1; j=0;   修改为j=next[j];
把判断匹配成功的判断条件改为   if(j==-1 || (str1[i]==str2[j]))   为什么要加入j==-1? 因为这就考虑到上面说的,如果一直退到-1的情况。

如此以来,时间复杂度从原先0(n*m)变成了0(m+n)

代码如下


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是C语言数据结构BF算法KMP算法的介绍: 1. BF算法(暴力匹配算法) BF算法是一种朴素的字符串匹配算法,也称为暴力匹配算法。它的基本思想是从主串的第一个字符开始,依次与模式串的每个字符进行比较,如果匹配成功,则继续比较下一个字符,否则主串指针后移一位,重新开始匹配。这种算法的时间复杂度为O(m*n),其中m和n分别为主串和模式串的长度。 以下是BF算法的C语言实现: ```c #include <stdio.h> #include <string.h> int BF(char *s, char *p) { int i = 0, j = 0; int slen = strlen(s); int plen = strlen(p); while (i < slen && j < plen) { if (s[i] == p[j]) { i++; j++; } else { i = i - j + 1; j = 0; } } if (j == plen) { return i - j; } else { return -1; } } int main() { char s[] = "hello world"; char p[] = "world"; int pos = BF(s, p); if (pos != -1) { printf("匹配成功,位置为:%d\n", pos); } else { printf("匹配失败\n"); } return 0; } ``` 2. KMP算法(Knuth-Morris-Pratt算法KMP算法是一种改进的字符串匹配算法,它的基本思想是利用已知信息来避免无效的比较。具体来说,它通过预处理模式串,得到一个next数组,用于指导匹配过程中的跳转。在匹配过程中,如果当前字符匹配失败,则根据next数组的值进行跳转,而不是直接从主串的下一个字符开始匹配。这种算法的时间复杂度为O(m+n),其中m和n分别为主串和模式串的长度。 以下是KMP算法的C语言实现: ```c #include <stdio.h> #include <string.h> void getNext(char *p, int *next) { int i = 0, j = -1; int plen = strlen(p); next[0] = -1; while (i < plen - 1) { if (j == -1 || p[i] == p[j]) { i++; j++; next[i] = j; } else { j = next[j]; } } } int KMP(char *s, char *p, int *next) { int i = 0, j = 0; int slen = strlen(s); int plen = strlen(p); while (i < slen && j < plen) { if (j == -1 || s[i] == p[j]) { i++; j++; } else { j = next[j]; } } if (j == plen) { return i - j; } else { return -1; } } int main() { char s[] = "hello world"; char p[] = "world"; int next[strlen(p)]; getNext(p, next); int pos = KMP(s, p, next); if (pos != -1) { printf("匹配成功,位置为:%d\n", pos); } else { printf("匹配失败\n"); } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值