字符串匹配 - KMP算法

·  阅读 858

BF算法分析

假设主串为“abcdefgab”,模式串为“abcdex”(模式串的每一位都不一样),那么按照BF算法,比较到主串的“f”时,匹配失败,模式串回退到开始位置“a”,主串回退到字符“b”,匹配失败,主串下标到“c”,以此类推...

其实这里面,因为模式串的每一位都不一样,所以第一次匹配失败时,已经匹配成功的部分“a”到“e”,首字符"a"已经匹配成功,所以主串的"b"到“e”跟模式串的首字符“a”肯定是无法匹配的,因此我们可以省略掉这一部分的循环。 如下图:

KMP模式匹配算法原理探索

后面我们用i来表示对主串的遍历(i从1开始),j来表示对模式串的遍历(j从1开始)。

KMP算法的精髓就是:利用已知信息(模式串),根据其重复值来修改回朔值(主串并不会进行回朔),得到一个最恰当的位置来跟主串进行下一轮的比较:

这种情况的前提是模式串每一位都并不一样,我们再来看一个案例(模式串有重复): 假设主串为“abcababca”模式串为“abcabx”,那么应该怎样来比较呢? 按照上面的经验,第一轮比较 i=6&&j=6时,匹配失败,因为前三位abc互不相同,所以 i=2&&j=1 i=3&&j=1是无意义的比较,如下图:

那么,下面我们来思考一下,第四步是不是有必要的? 根据模式串信息,前两位“ab”与第四五位"ab"是相同的,因为第一轮比较已经到了第六位,所以前两位肯定跟主串里的第四五位也是相等的,所以这一步也可以优化(直接从 i=6 j=3比较):

KMP模式匹配算法——next数组推导

我们把模式串各个位置j值变化定义为一个next数组,那么next的长度就是模式串的长度,next下标也是从1开始,默认第一位是0,即next[1] = 0。KMP中i是不往回走的,i要么是不动,要么是跟j一起往后移。

模式串为“abcedx”时next的推导:

  • j=1时匹配冲突,next[1] = 0
  • j=2时匹配冲突,j由1到j-1范围内只有字符“a”,next[2]=1
  • j=3时匹配冲突,j由1到j-1范围内只有字符“ab”,不存在相等情况,next[3]=1
  • j=4时匹配冲突,j由1到j-1范围内只有字符“abc”,不存在相等情况,next[4]=1
  • j=5时匹配冲突,j由1到j-1范围内只有字符“abcd”,不存在相等情况,next[5]=1
  • j=6时匹配冲突,j由1到j-1范围内只有字符“abcde”,不存在相等情况,next[6]=1

模式串为“abcabx”时next的推导:

  • j=1时匹配冲突,next[1] = 0
  • j=2时匹配冲突,j由1到j-1范围内只有字符“a”,next[2]=1
  • j=3时匹配冲突,j由1到j-1范围内只有字符“ab”,不存在相等情况,next[3]=1
  • j=4时匹配冲突,j由1到j-1范围内只有字符“abc”,不存在相等情况,next[4]=1
  • j=5时匹配冲突,j由1到j-1范围内有字符“abca”,出现了重复,我们来找到重复的前置字符和后置字符,得到前置字符“a”与后置字符“a”,此时j回朔到a就没有必要了,因为a肯定是有的,直接回朔到b,即next[5]=2
  • j=6时匹配冲突,j由1到j-1范围内有字符“abcab”,出现了重复,得到前置字符“ab”与后置字符“ab”,此时j回朔到a和b都没有必要了,直接回朔到c得到next[6]=3

tips :如果前后置一个字符相等,那么next对应位置的值就是2,如果两个字符相等,那么next对应位置的值就是3,如果n个字符相等,那么next对应位置的值就是n+1

按照上面得到的方法,来计算模式串“ababaaaba”的next数组

  • j<4时,跟上面一样,next[1]=0,next[2]=1,next[3]=1;
  • j=4,前后置相同的字符分别为“a”和“a”,next[4]=2;
  • j=5,前后置相同的字符分别为“ab”和“ab”,next[5]=3;
  • j=6,注意,此时的前后置相同的字符分别为“aba”和“aba”(前后不完全相等时最长的相等字符),中间的a即是前置也是后置,next[6]=4;
  • j=7,前后置相同的字符分别为“a”和“a”,next[7]=2;
  • j=8,前后置相同的字符分别为“a”和“a”,next[8]=2;
  • j=9,前后置相同的字符分别为“ab”和“ab”,next[9]=3;

再来试试“aaaaaaaab”:

  • j=1,next[1] = 0
  • j=2,只有一个“a”,next[2]=1;
  • j=3,前后置相同的字符分别为“a”和“a”,next[3]=2;
  • j=4,前后置相同的字符分别为“aa”和“aa”,next[4]=3;
  • j=5,前后置相同的字符分别为“aaa”和“aaa”,next[5]=4;
  • j=6,前后置相同的字符分别为“aaaa”和“aaaa”,next[6]=5;
  • j=7,前后置相同的字符分别为“aaaaa”和“aaaaa”,next[7]=6;
  • j=8,前后置相同的字符分别为“aaaaaa”和“aaaaaa”,next[8]=7;
  • j=9,前后置相同的字符分别为“aaaaaaa”和“aaaaaaa”,next[9]=8;

用代码来求得next数组

假设主串为“abcababca”,模式串为“abcdex”,此时next数组为{0,1,1,1,1,1}。当遍历到匹配失败时,将j回朔到next[j]的位置

我们来模拟一下执行过程:

  • 第一轮比较,i=4,j=4时匹配失败,将j回朔到next[4]的位置,即j=next[4]=1;

  • 继续遍历,i=6,j=3时匹配失败,回朔到j=next[3]=1的位置继续比较

  • 依次类推......

那用代码如何计算next数组呢?

假设,主串S='abcababca',模式串T='abcdex'

  1. 默认next[1] = 0;

2, i=0,j=1开始遍历,当j<T.length时,从1-length遍历字符串

3,如果i=0表示[0,j]这个范围内没有找到相同的字符串,所以i要回朔到1的位置,表示next[j]=1

4,如果T[i]=T[j],表示找到了与其相同字符的位置,所以next[j]=i;

5,当以上两个条件都不满足,则将i回朔到前面记录的next[i]的位置

模拟下计算步骤 (开始i=0,j=1):

1,i=0,j=1,T[i]!=T[j],表示0-1范围内没有重复字符,则i++,j++,next[2]=1;此时i=1,j=2

2, T[i]!=T[j] && i!=0,此时需要将i回朔到next[i]的位置,即i=0,此时i=0,j=2

3, T[i]!=T[j] && i=0,表示i-j范围内没有重复字符,i++,j++,next[3]=1,此时i=1,j=3

4, T[i]!=T[j] && i!=0,此时需要将i回朔到next[i]的位置,即i=0,此时i=0,j=3

5, T[i]!=T[j] && i=0,表示i-j范围内没有重复字符,i++,j++,next[4]=1,此时i=1,j=4

6, T[i]!=T[j] && i!=0,此时需要将i回朔到next[i]的位置,即i=0,此时i=0,j=4

7, T[i]!=T[j] && i=0,表示i-j范围内没有重复字符,i++,j++,next[5]=1,此时i=1,j=5

8, T[i]!=T[j] && i!=0,此时需要将i回朔到next[i]的位置,即i=0,此时i=0,j=5

9, T[i]!=T[j] && i=0,表示i-j范围内没有重复字符,i++,j++,next[6]=1,此时i=1,j=6

10,跳出循环

最终求得next数组为 [0,1,1,1,1,1]

求解next数组时有4种情况:

1,默认next[1]=0;

2,当 i=0 时,表示从头开始比较,则i++,j++,next[j]=1;

3,当T[i]==T[j],表示找到了相同的字符,准备开始下一次循环 i++,j++,同时next[j]=i;(因为next数组是从1开始的,而遍历是从0开始的,所以先做++,再赋值)

4,当T[i]!=T[j],表示字符不相同,此时要扩大范围,将i退回到合理的位置,i=next[i]

下面用代码来计算:

//通过计算返回子串T的next数组;
//注意字符串T[0]中是存储的字符串长度; 真正的字符内容从T[1]开始;
void get_next(String T,int *next){
    int i,j;
    j = 1;
    i = 0;
    next[1] = 0;
    //abcdex
    //遍历T模式串, 此时T[0]为模式串T的长度;
    //printf("length = %d\n",T[0]);
    while (j < T[0]) {
        //printf("i = %d j = %d\n",i,j);
        if(i ==0 || T[i] == T[j]){
            //T[i] 表示后缀的单个字符;
            //T[j] 表示前缀的单个字符;
            ++i;
            ++j;
            next[j] = i;
            //printf("next[%d]=%d\n",j,next[j]);
        }else
        {
            //如果字符不相同,则i值回朔;
            i = next[i];
        }
    }
}
复制代码

next数组的使用

KMP思路:

1,遍历主串S,i是用来标记主串的索引,遍历模式串T,j是用来标记模式串的索引;

2,当 i>S.length || j>T.length; i>S.length,j<T.length,表示主串遍历完也没找到匹配;只有一种可能,就是当j>T.length,表示找到了匹配字符串

3,当j = 0,表示需要将模式串从 1这个位置与主串i+1这个位置比较

4,当S[i] == T[j],表示模式串j和主串i的位置字符相等,i++,j++;

5,当j>0且S[i]!=T[j],表示此事需要移动模式串的j,那么我们让j=next[j],以达到减少比较次数的目的

//KMP 匹配算法
//返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0;
int Index_KMP(String S,String T,int pos){
    
    //i 是主串当前位置的下标准,j是模式串当前位置的下标准
    int i = pos;
    int j = 1;
    
    //定义一个空的next数组;
    int next[MAXSIZE];
    
    //对T串进行分析,得到next数组;
    get_next(T, next);
    count = 0;
    //注意: T[0] 和 S[0] 存储的是字符串T与字符串S的长度;
    //若i小于S长度并且j小于T的长度是循环继续;
    while (i <= S[0] && j <= T[0]) {
        
        //如果两字母相等则继续,并且j++,i++
        if(j == 0 || S[i] == T[j]){
            i++;
            j++;
        }else{
            //如果不匹配时,j回退到合适的位置,i值不变;
            j = next[j];
        }
    }
    
    if (j > T[0]) {
        return i-T[0];
    }else{
        return -1;
    }
}


复制代码
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改