数据结构与算法之KMP算法

802 阅读7分钟

KMP算法引入

我们先看上篇博客提到的字符串匹配问题,假设现在有一个主串S = “abcdefgab” ; 模式串 T = “abcdex” ; 如果使用暴风算法的话,前面5个字母完全相等,直到第6个字母.'f' 和 'x' 不相等; 如下图

按照暴风算法计算时我们需要②③④过程都执行

只有当执行①的时候S[i] = T[j],此时 i= 2,j = 1,然后继续循环。但是明显的从j = 1到j = 5,T

[j]均不相同,S[i]中i = 1到i = 5也不同,并且S[i] = T[j],这就浪费了②③④过程,因此我们引出KMP算法

KMP算法的原理

KMP的想法是利用这个已知的信息,不要把“搜索位置”前移到已经比对过的位置,继续把他往后移,这样就提高了效率。

因此我们需要先算出当S[i] != T[j]时模式串T中j下一个移动位置,要计算j的下一个移动位置,我们先看下面的例子

第一种情况

  • 如果我们知道T串中的首字母a与T串中后面的字符均不相等(这是前提,具体计算后面讲解)
  • 并且T串第二个字符b跟S串的第二个字符b相等,这样我们就知道T串首字符a与S串的第二个字符不用判断,因为他们根本不可能相等,因此②步骤可以省略
  • 同样的道理在T串首字符a与其后面的字符均不相同的前提下,a与S串的c、d、e也不相同,因此③④⑤就没有必要了,我们直接保留①以及⑥即可,如图

    第二种情况:如果T串保留首字母字符时'a'时,假设主串S = ‘abcababca’,模式串T = 'abcabx'

  • 对于刚开始的判断前5个字符完全相同,第6个字符不同
  • 根据我们第一种情况的推断,T串的首字符a与T串第二个b以及第三个字符c均不相同,所以以下两个步骤就是多余的


  • 由于T串首字符以及第二位字符跟第四位以及第五位字符分别相同,并且跟主串S的第四位、第五位相同,所有我们可以推断出下面的步骤也是多余的


  • 因此我们可以得出对于子串中有首字符相同的字符,也是可以省略一部分不必要的步

从上面两个例子我们得出KMP算法就是为了减少不必要的步骤的,但是我们知道i是无法进行回溯的,那么我们只能对j进行回溯,我们通过刚才的分析可以得出j值得回溯跟主串S没有关系,j更多的是与模式串T的结构有关

模式串T回溯求解next()

  • 我们先看以下例子

如上图所示,模式串T中没有相同的字符,所以j由6变成了1

  • next[j]数组公式

next数组值推导

  • 模式串T中没有相同的字符

    在上图例子中,模式串T没有重复的字符,符合next公式中的第一和第三种情况,所以next =  {0,1,1,1,1,1};

  • 当模式串T中有重复的字符时,如T = "abcabx",


如上两张图所展示的,符合next推导公式的三种情况,所以next = {0,1,1,1,2,3};

  • 思考,当T = “ababaaaba”时,next = {0,1,1,2,3,4,2,2,3},当T = "aaaaaaaab"时,next = {0,1,2,3,4,5,6,7,8}
  • 总结next数组求值的四种情况:这里i表示需要回溯的位置,j是遍历模式串的下标
  1.   默认next[1] = 0;
  2. 当i == 0时,表示当前的比较应该从头开始,所以j++,i++,next[j] = i
  3. 当T[i] == T[j],表示两个字符相等,此时i++,j++,同时next[j] = i;
  4. 当T[i] != T[j],表示两个字符不相等,需要将i进行回溯到合理位置,i = next[i];

next数组求值代码实现

  • 实现next数组求解

#pragma mark - 获取next数组
int *GetNext(String T){
    if (!T) {
        return NULL;
    }
    //注意T[0]存储的是字符串的长度
    int *next = (int *)malloc(sizeof(int) * (T[0] + 1));
    int i = 0, j = 1;
    next[1] = 0;
    while (j<T[0]) {
        if (i == 0 || T[i] == T[j]) {//第一个字符或者相邻字符相同
            i++;
            j++;
            next[j] = i;
        }else{//相邻字符不相同,i值回溯
                        i = next[i];
        }
    }
    return next;
}

KMP算法next数组优化

  • 优化next数组的原因

KMP 算法也是有缺陷的,比如当主串S = "aaaabcde",模式串 T= "aaaaax",此时next = {0,1,2,3,4,5},


  • 当我们匹配时,j = 5, i = 5,此时 ‘b’ 与 'a'不符合,如①所示则 j = next[i],即 j = next[5] = 4,此时如②还是不正确一次类推,直到j = 1
  • 这样无形中多计算了j = 4、3、2、1,因此需要对计算next数组进行优化
  • 由于T串的首字符与第二、第三、第四、第五位的字符相同,因此可以使用next[1]的值去替代跟它相等的字符后续的next[j]值,比如例子中next = {0,1,2,3,4,5}变为next = {0,0,0,0,0,5}

nextVal数组优化规律

 初始化i = 0,j = 1

  • 默认nextVal[1] = 0;
  • 当T[i] == T[j],且i++,j++后T[i]依旧等于T[j],则nextVal[j] = nextVal[i]
  • 当T[i] == T[j],且i++,j++后T[i]不等于T[j],则nextVal[j] = i
  • 当i = 0,表示从头开始,i++,j++后,T[i] != T[j],则nextVal[j] = i,否则nextVal[j] = nextVal[i]
  • 当T[i] != T[j]表示不相等,则需要i回溯,i = nextVal[i]

nextVal举例讲解

我们以模式串T = "ababaaaba"为例子,T没有优化前的next = {0,1,1,2,3,4,2,2,3},然后我们计算优化后的nextVal,下面中i是nextVal的下标,j是遍历T串的下标,temp = T[j],T[0]表示模式串T的长度,所以真实字符从下标1开始,初始化i= 0,j = 1

  • j = 1时,temp = 'a',i = 0,nextVal[1] = 0(i++了)
  • j = 2,temp = 'b',next[1] = 1,此时 b != (T[1] = a),所以nextVal[2] = 1,i++,j++
  • j = 3,temp = ‘a’,next[2] = 1,此时 a == (T[1] = a),所以nextVal[3] = nextVal[1] = 0,i++,j++
  • j = 4,temp = 'b',next[3] = 2,此时 b ==  (T[2] = b),所以nextVal[4] = nextVal[2] = 1,i++,j++
  • j = 5,temp = 'a',next[4] = 3,此时 a == (T[3] = a),所以nextVal[5] = nextVal[3] = 0,i++,j++
  • j = 6,temp = 'a',next[5] = 4,此时 a != (T[4] = b),所以nextVal[6] = 4,i++,j++
  • j = 7,temp = 'a',next[6] = 2,此时 a != (T[2] = b),所以nextVal[7] = 2,i++,j++
  • j = 8,temp = 'b',next[7] = 2,此时 b == (T[2] = b),所以nextVal[8] = nextVal[2] = 1,i++,j++
  • j = 9,temp = 'a',next[8] = 3,此时 a == (T[3] = a),所以nextVal[9] = nextVal[3] = 0,i++,j++
  • 因此推得nextVal = {0,1,0,1,0,4,2,1,0}

代码实现nextVal优化

#pragma mark - 优化nextVal数组
int *getNextVal(String T){
    if (!T) {
        return NULL;
    }
    //此处开辟T[0] + 1空间是因为nextVal的有效值从下标1开始,即nextVal[1] = 0
    int *nextVal = (int *)malloc(sizeof(int)*(T[0] + 1));
    nextVal[1] = 0;
    int i = 0, j = 1;
    while (j<T[0]) {
        if (i == 0 || T[i] == T[j]) {//i == 0
            i++;
            j++;
            if (T[i] == T[j]) {//++后依然相等则nextVal[j]跟前面的nextVal[i]相同
                nextVal[j] = nextVal[i];
            }else{//++后不相等,则nextVal[j]等于i
                nextVal[j] = i;
            }
        }else{//不相等则i进行回溯
            i = nextVal[i];
        }
    }
    return nextVal;
}

KMP算法匹配字符

#pragma mark - KMP算法实现
int GetIndexKMP(String S, String T){
    int sLength = S[0],tLength = T[0];
    if (tLength>sLength) {
        return -1;
    }
    //未优化next
    int *next = GetNext(T);
    //优化后的next
    next = getNextVal(T);
    int i = 1,j = 1;
    while (i <= sLength && j<= tLength) {
        if (S[i] == T[j] || j == 0) {//j == 0是因为j == 1时,此时不同,  j = next[j],此时next[1] = 0,所以存在j == 0的情况
            j++;
            i++;
        }else{//j回溯到next[j]
            j = next[j];
        }
    }
    if (j>tLength) {//找到了
        return i - tLength;
    }else{
        return -1;
    }
}

总结

KMP算法难在理解计算i回溯的数组next或nextVal,理解了i回溯的计算,代码层面就比较简单了