关于串匹配的简单理解

来源:互联网 发布:2017广电禁止网络电视 编辑:程序博客网 时间:2024/05/27 01:05

想起串匹配问题还是3G的二面,当时正好第三题就是字符串匹配

实例输入:

第一组:

abcdefg123456

efg1234

第二组

abcdefg123456

2222

示例输出

第一组

5

第二组

-1

当时二面上机,时间也紧张,没怎么多想,就直接用了暴力匹配

#include<stdio.h>#include<string.h>int main(void) {char s1[20] = {0};char s2[20] = {0};int len1;int len2;int i;int j;int k;int index;scanf("%s",s1);scanf("%s",s2);len1 = strlen(s1);len2 = strlen(s2);index = -2;for(i = 0;i < len1;i++) {if(s1[i] == s2[0]) {index = i;for(j = 1;j < len2;j++) {k = index;if(s1[k] == s2[j]) {k++;} else {break;}}if(j == len2) {break;} else {continue;}}}printf("%d",index + 1);return 0;}

当时也比较紧张,思维可能也比较混乱,不过后来出答案的时候知道暴力匹配比较浪费时间,有一种更好的算法,叫做KMP算法,专门用于串匹配。

kmp算法说实话一开始看的也是云里雾里,啥都不知道的。慢慢的才开始理解一点意思。


如果通过暴力的算法,时间复杂度上o(m*n)

 从图中可以看出,暴力匹配存在着大量的控制源字符串的下标的往回移动,比如源字符串比较到下标为6的时候,由于在下标为7的位置失配,则,不得不将下标从7移动到2继续进行下一次比较,在对一个长度很长的字符串进行遍历的时候,这样的不必要的移动大量存在。

我们在让子串跟源串的一部分进行比较时,当遇到不匹配的字符了,自然说明源串的这一段字符串跟我们要找的目标串不匹配,我们自然要移动目标串,使它跟下一段比较,这时,暴力匹配的做法是同时改变控制源串和子串的下标进行下一次比较,而KMP算法则保持源串的当前下标不变,只移动子串到相应的位置进行下一次的比较。那么,究竟移动多少,通过什么得到应该移动多少都是KMP算法的一部分,我们来观察这样一组字符串:


源串:   abababaaaabbbabab

目标串:abababb


        我们观察到,该目标串在下标为6的位置失配,那么则说明前六个位置的字符都是匹配的,我们想直接在适配的位置进行下一次比较。这是,该串截止到失配的位置都已经比较过了,而失配字符的后面还没有比较过,而此时本次比较已经结束了。而此时,我们就可以得到截至失配字符,前面的字符都是匹配的结论。即:“ababab”;这时如果将目标串向后移动一个单位,两者的对应关系就为:


源串:    abababa ... ...

目标串:  ababab ... ...


        此时,我们明显地看出前面的字符串都没有匹配,因此,后面的比较都是没有意义的。而我们向后移动两个单位的时候对应关系为:


源串:    abababa ... ...

目标串:    ababa ... ...


因此,我们得到如果在目标串下标为7的位置失配,则应该将目标串向后移动两个位置进行下一次的比较的结论。以此类推,每一个位置都应该有一个失配后应该移动多少个单位的值。如果得到了这样的一组值,我们就能使每一次失配后的移动量达到最小。下面我们的工作就是得到每一个位置如果失配应该向后移动多少个单位的这样一组数据。        KMP算法的实现方案是首先给出一个数组,我们先叫它“next”,元素个数为目标字符串的长度。每一个元素的值“n”代表当以该元素下标为目标字符串的下标的字符失配时,应该让目标字符串的以“n”为下标的元素继续与源字符串失配的位置进行比较。

总感觉next数组的最大前后缀的推理实在太繁琐了,推理也不容易。中间推理就抄大佬们的了偷笑,能看懂就行 原文:点击打开链接

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7.

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

16.

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。


在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1 = next [k' ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0

所以该数组的实现过程如下:

首先,如果在第一个元素就失配了,那么肯定要用第一个元素继续与源字符串失配的位置进行比较。因此,这个数组的第一个元素一定是“0”,这一点是容易想到的。之后的做法是:将目标字符串当前元素与next前一个元素的值作为目标字符串下标的元素进行比较,若相等,则将当前next的元素为上一个元素的值+1。若不相等,又分为两种情况:

1、如果next数组当前元素的前一个元素的值为零,那么直接将当前元素赋值为0;

2、如果next数组当前元素的前一个元素不为零,那么将当前位置的字符与目标字符串的数组中上一个位置的元素的值为下标的元素作比较,继续执行上述判断操作。


void getNext(char *res, int *nextl) {      int index = 2;      int value = 0;        if (strlen(res) <= 2)         return;        while (res[index]) {          if (res[index-1] == res[value]) {              next[index++] = ++value;          }          else if (value == 0) {              next[index++] = 0;          }          else {              value = next[value];          }      }  }  


通过一个变量value记录当前需要与目标字符串当前的字符进行比较的字符的下标,这样,我们最后一步看似繁琐的步骤只要更改一下value的值却不增加index的值就可以实现继续用当前的字符与前面的字符作比较了。


int KMPStringMarth(char *resource, char *target) {      int resLenth = strlen(resource);      int tarLenth = strlen(target);      int *next;      int resIndex = 0;      int tarIndex = 0;        next = (int *)calloc(sizeof(int), tarLenth);      getNext(target, next);        while (resource[resIndex]) {          while (target[tarIndex] && target[tarIndex] == resource[resIndex]) {              resIndex++;              tarIndex++;          }          if (target[tarIndex] == 0) {              free(next);                return resIndex - tarIndex;          }          else{              tarIndex = next[tarIndex];              if (tarIndex == 0) {                  resIndex++;              }          }      }        free(next);      return -1;  }  

1、我们根据target字符串的长度给出了一个数组,用我们之前编写的getNext函数得到相应字符失配后偏移量的信息;

2、给出两个变量resIndex、tarIndex分别用于跟踪源字符串和目标字符串;

3、用一个循环控制源字符串的遍历,逐一进行比较,字符匹配则两个字符串的下标均加一,若不匹配则保持跟踪源字符串的下标不变,让跟踪目标字符串的下标等于next中该失配字符所对应的值,并继续进行比较。

 4、如果某一次比较时,目标字符串一直到零结束标志都没有失配,则说明找到了目标字符串,返回当前的源字符串中目标字符串的开始位置所对应的

字符的下标;若直到源字符串找到了零结束标志都没有返回,则返回-1。