KMP算法

368 阅读7分钟

KMP介绍

在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置。此算法通过运用对这个词在不匹配时本身就包含足够的信息来确定下一个匹配将在哪里开始的发现,从而避免重新检查先前匹配的字符。这个算法是由高德纳和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终由三人于1977年联合发表。

KMP算法解读

KMP解析

我们定义主串S串,子串T串,i为S串中匹配的位置,j为T串中当前做比较的位置

如下:

i: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
T: ABCDABD
j: 0123456

我们从S串(主串)与T串(子串)的开头比较,我们对比S[3] = ' ',T[3] = 'D',子串和主串不符,接着并不是从S[1],继续比较,我们知道S[1]--S[3] 和 T[0]不相等,因此,可以略过这些字符,让i=4 以及 i= 0.

i: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
T:     ABCDABD
j:     0123456

如上所示,我们检验了T串”ABCDABD“这个字符串。然而,这与目标扔有差异,我们可以注意到,”AB“在字符串头尾处出现了两次,这意味着尾端“AB”可以作为下次比较的起始点,因此可以令i = 8,j = 2,如下:

i: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
T:         ABCDABD
j:         0123456

在i  = 10的位置,又出现了不相符的情况,类似的。令m = 11,i= 0继续比较:

i: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
T:            ABCDABD
j:            0123456

这时,S[17] = 'C' 和T[6]不相同,但是出现了两次“AB“,我们采取一贯的做法,令m = 15和i= 2,继续搜索。

i: 01234567890123456789012
S: ABC ABCDAB ABCDABCDABDE
T:                ABCDABD
j:                0123456

我们完全找到了匹配的字符串,其起始位置位域S[15]的位置

通过以上我们可以了解到主串S的匹配位置i是不断++,而子串T中,每次匹配失败,会判断T中头尾是否有相同字符,从而确定子串T中j的回溯位置

子串回溯位置推导

我们设定主串和子串是从i = 1位置开始比较的

next数组为我们储存回溯位置的数组

我们设定子串 T = ”ABCDE“ 那么下标和next数组如下图所示


我们归整一下next数组可能的4中情况:(j为子串T与主串S匹配过程的子串字符位置,m 为子串T 首位匹配中位置)

1 默认next[1] = 0;

2 当m = 0时,表示当前的比应该从头开始,则m++,j++,next[j] = m;

3 当T[m] == T[j]表示两个字符相等,则m++,j++,同时 next[j] = m;

4 当T[m] != T[j] 表示不相等,则需要将m退回到合理的位置。则 m = next[m];


如果我们有了next回溯数组,那么当我们子串T和主串S在匹配到j失败时,子串T的回溯位置就是next[j]

KMP算法->1

KMP算法之next数组查找

//----KMP 模式匹配算法---
//1.通过计算返回子串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];
        }
    }
}

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);
    //注意: 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;
    }
    }




KMP算法优化

KMP算法也是有缺陷的,比如:如果我们的主串S = "aaaabcde",子串T = "aaaax";那么next数组就是{0,1,2,3,4,5}

乍一看没毛边,那么我们看看实际当中的效果






看下这个匹配过程,其中2345步骤中的回溯比较都是多余的的判断

由于子串T串的第二三四五位置的字符都是与首位”a“相等,那么可以用首位next[1]的值去取代与他相等的字符后续的next[j]值,那么我们来对next数据进行改良

next数组 = {0,1,2,3,4,5}

改良后的nextVal数组 = {0,0,0,0,5}

KMP 算法->2

KMP_Next 数组逻辑

求解nextval数组的五种情况:

1默认 nextval[1] = 0;

2 T[i] = T[j] 且i++;j++后 T[i] 依旧等于T[j] 则nextval[i] = nextval[j];

3 i = 0,表示从开头开始i++,j++后,且T[i] != T[j],则nextval[i] = j;

4 T[i] == T[j],且 i++,j++,后T[i] != T[j],则nextval[i] = j;

5 当T[i] != T[j] 表示不相等,则需要将i退回到合理的位置则i = next[i];

代码实现KMP_nextVal数组

//求模式串T的next函数值修正值并存入nextval数组中;
void get_nextVal(String T,int *nextVal){
    int i,j;
    j = 1;
    i = 0;
    nextVal[1] = 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];
        }
    }
}




KMP匹配算法(2)

//返回子串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;
    }
    
}