数据结构与算法(8)- KMP算法与字符串匹配

359 阅读9分钟

KMP算法

一. 梗概

主要是通过利用模式串中的重复字母,进行跳跃匹配减少匹配次数:

  1. abcaefgab 主串S

  2. abcax 模式串T

假设当前匹配到第5位,ex不相同

  • 使用暴风算法,模式串就要回溯到第1个位置(a)和主串的第2个位置(b)匹配

  • 使用KMP算法,因为已知主串中的第1个位置a和第5个位置a相同,模式串可以直接回溯到第2个位置b和主串中的第6个位置e去匹配;以此来减少匹配的次数;

那么问题来了,根据什么回溯?

二. next数组推导

2.1 原理

把模式串T中各个位置j值的变化定义为一个next数组,那么next数组的长度就是模式串T的长度,有下面的函数定义:

  • j = 1时,next[j] = 0
  • Max {k | 1<k<j , 且’p1...pk-1’ = ‘ pj-k+1 ... pj-1’} 当此集合不为空时, next[j] = k
  • 其他情况,即前面没有重复字母,next[j] = 1

2.2 练习推导如下:

2.2.1 模式串T = abcd

  • j = 1时,next[1] = 0

  • j = 2时,j1j-1范围内只有字符a,属于其他情况 next[2] = 1

  • j = 3时,j1j-1范围内只有字符ab,显然a不等于b,属于其他情况 next[3] = 1

  • j = 4时,j1j-1范围内只有字符"abc",显然abc不存在相等的情况,属于其他情况 next[4] = 1

因此,next = [0,1,1,1]

2.2.2 模式串T = abcabd

  • j = 1时,next[1] = 0
  • j = 2时,j1j-1范围内只有字符a,属于其他情况 next[2] = 1
  • j = 3时,j1j-1范围内只有字符"ab",显然a不等于b,属于其他情况 next[3] = 1
  • j = 4时,j1j-1范围内只有字符"abc",显然abc不存在相等的情况,属于其他情况 next[4] = 1
  • j = 5时,j1j-1范围内只有字符"abca",显然abca存在相等的字符"a",因此,可以得到next[5] = 2,表示当第五个位置匹配失败时,直接从模式串的第二个位置与匹配失败的那个字符进行相比。
  • j = 6时,j1j-1范围内只有字符"abcab",显然abcab存在相等的字符"ab",因此,可以得到next[6] = 3,表示当第五个位置匹配失败时,直接从模式串的第三个位置与匹配失败的那个字符进行相比。

因此,next = [0,1,1,1,2,3]

2.2.3 模式串T = aaaabcj = 1时,next[1] = 0j = 2时,j1j-1范围内只有字符"a",属于其他情况 next[2] = 1j = 3时,j1j-1范围内只有字符"aa",显然存在aa next[3] = 2j = 4时,j1j-1范围内只有字符"aaa",显然前缀aa等于后缀aanext[4] = 3j = 5时,j1j-1范围内只有字符"aaaa",前缀aaa与后缀aaa相等,因此,可以得到next[5] = 4j = 6时,j1j-1范围内只有字符"aaaab",前缀与后缀不想等,因此,next[6]= 1

因此,next = [0,1,2,3,4,1]

2.3 代码实现

  1. next求解
//注意字符串T[0]中是存储的字符串长度; 真正的字符内容从T[1]开始;
void get_next(String T,int *next){
    int i,j;
    j = 1;
    i = 0;
    next[1] = 0;
    //遍历T模式串, 此时T[0]为模式串T的长度;
    //printf("length = %d\n",T[0]);
    while (j < T[0]) {
        if(i ==0 || T[i] == T[j]){
            //T[i] 表示后缀的单个字符;
            //T[j] 表示前缀的单个字符;
            ++i;
            ++j;
            next[j] = i;
        }else{
            //如果字符不相同,则i值回溯;
            i = next[i];
        }
    }
}
  1. 使用KMP
//KMP 匹配算法(1)
//返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0;
int Index_KMP(String S,String T,int pos){
    //i 是主串当前位置的下标准,j是模式串当前位置的下标准
    int i = pos;
    int j = 1;
    int count = 0;
    //定义一个空的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;
    }
    
}

int main(int argc, const char * argv[]) {
    int i,*p,*t;
    String s1,s2;
    int Status;
    //KMP算法调用
    StrAssign(s1,"abcababca");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"abcdex");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP(s1,s2,1);
    printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
    
    StrAssign(s1,"abccabcceabc");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"abcce");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP(s1,s2,1);
    printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
    
    StrAssign(s1,"aaaabcde");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"aaaaax");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP(s1,s2,1);
    printf("主串和子串在第%d个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] \n",Status);
    return 0;
}
  1. 输出
主串为: abcababca
子串为: abcdex
主串和子串在第-1个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] 
主串为: abccabcceabc
子串为: abcce
主串和子串在第5个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] 
主串为: aaaabcde
子串为: aaaaax
主串和子串在第-1个字符处首次匹配(KMP算法)[返回位置为负数表示没有匹配] 

next优化

3.1 在求解nextVal数组的5种情况:

  • 默认nextval[1] = 0;
  • T[i] == T[j]++i,++jT[i] 依旧等于 T[j]nextval[i] = nextval[j]
  • i = 0, 表示从头开始i++,j++后,且T[i] != T[j]nextVal = j;
  • T[i] == T[j]++i,++jT[i] != T[j] ,则nextVal = j;
  • T[i] != T[j] 表示不相等,则需要将i 退回到合理的位置. 则 i = next[i];

3.2 练习:

当模式串T = 中有多个相同的字符时,会进行多次重复比较,如模式串为ababaaaba,next 数组为{0,1,1,2,3,4,2,2,3}

  • j = 1,nextVal = 0; 继续保持next[1]的逻辑;
  • j = 2时,也就是当j = 2发生匹配错误, 那么由于第二个字符'b'的 next 值是1, 而且第一个字符是'a' .两者不相等.所以nextval[2] = next[2] = 1;
  • j = 3 时, 此时因为第3个字符'a' 的 next 值是1, 所以得知第1位的'a' 与第3位的'a' 是相等,则此时nextVal[3] = nextVal[1] = 0;
  • j = 4时, 因为第 4 个字符 "b" 的next = 2; 所以可以得知 它与第2位字符 "b" 是相等,则nextVal[4] = nextVal[2] = 1;
  • j = 5 时,next 值为3 , 第5个字符'a' 与第3个字符'a' 相等,则nextVal[5] = nextVal[3] = 0;
  • j = 6 时,next 值为4 , 第6个字符'a' 与第4个字符'b' 不相等,则nextVal[6] = 4;
  • j = 7 时,next 值为2 , 第7个字符'a' 与第2个字符'b' 不相等,则nextVal[7] = 2;
  • j = 8 时,next 值为2 , 第8个字符'b' 与第2个字符'b' 相等,则nextVal[6] = nextVal[2] = 1;
  • j = 9 时, next 值为3, 第9个字符'a' 与第3个字符'a' 相等,则nextVal[9] = nextVal[3] = 0;

3.3 代码实现

  1. nextval 求解
//求模式串T的next函数值修正值并存入nextval数组中;
void get_nextVal(String T,int *nextVal){
    int i,j;
    j = 1;
    i = 0;
    nextVal[1] = 0;
    //字符串"0" 位置,存储了字符的长度
    while (j < T[0]) {
        if (i == 0 || T[i] == T[j]) {
            ++j;
            ++i;
            //如果当前字符与前缀不同,则当前的j为nextVal 在i的位置的值
            if(T[i] != T[j])
                nextVal[j] = i;
            else
            //如果当前字符与前缀相同,则将前缀的nextVal 值赋值给nextVal 在i的位置
                nextVal[j] = nextVal[i];
        }else{
            i = nextVal[i];
        }
    }
}
  1. 调用
//返回子串T在主串S中第pos个字符之后的位置, 如不存在则返回0;
int Index_KMP2(String S,String T,int pos){
    
    //i 是主串当前位置的下标准,j是模式串当前位置的下标准
    int i = pos;
    int j = 1;
    
    //定义一个空的next数组;
    int next[MAXSIZE];
    
    //对T串进行分析,得到next数组;
    get_nextVal(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;
    }
    
}

int main(int argc, const char * argv[]) {
    int i,*p,*t;
    String s1,s2;
    int Status;
    //KMP算法调用
    StrAssign(s1,"abcababca");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"abcdex");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP2(s1, s2, 1);
    printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
    
    StrAssign(s1,"abccabcceabc");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"abcce");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP2(s1, s2, 1);
    printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
    
    StrAssign(s1,"aaaabcde");
    printf("主串为: ");
    StrPrint(s1);
    StrAssign(s2,"aaaaax");
    printf("子串为: ");
    StrPrint(s2);
    Status = Index_KMP2(s1, s2, 1);
    printf("主串和子串在第%d个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] \n\n",Status);
    return 0;
}
  1. 输出
主串为: abcababca
子串为: abcdex
主串和子串在第-1个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] 

主串为: abccabcceabc
子串为: abcce
主串和子串在第5个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配] 

主串为: aaaabcde
子串为: aaaaax
主串和子串在第-1个字符处首次匹配(KMP_2算法)[返回位置为负数表示没有匹配]