C/C++/Java代码 模式匹配算法 KMP算法 数据结构【经典算法思想】详解

什么是模式匹配常见模式匹配算法C/C++/Java代码 详见:https://blog.csdn.net/kjcxmx/article/details/82348917

KMP算法是什么?

先看看某度的解释。。

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。

KMP算法的核心,也就是为什么可以如此的高效?关键就在于它Next数组的存在,有了Next数组就好比有一个滑动窗口,这样就避免了在匹配过程的主串中元素下标 i 不会发生回溯,也就是比较过程中的 i 始终是增加或不变的,这样就使得时间复杂度降低了,效率大大提高。

如下例子:主串T为“BABABCAB”,模式串S为“ABCA”。

A、见下图KMP.1开始进行Next数组的计算,首先把模式串S进行标号。(下面的例子中下标是从0开始的)

B、见下图KMP.2中,分别写出每个字串的对应前缀,(当然也可以不写出,熟悉之后直接计算即可),最后一行也就是5号“A”对应的前缀为整个模式串。

C、见下图KMP.3分别对各个子串的前后缀比较,算出最长字串的长度,写在对应左侧。如下文字分析

第一行,只有一个字符“A”,没有前缀和后缀(前缀和后缀不能为串本身),所以为0

第二行,串为“A B”,前缀为“A”,后缀为“B”,不匹配(一致),所以为0

第三行,串为“A B A”,前缀为“A”和“A B”,后缀为“A”和“B A”,最大匹配长度为“A”=="A",所以为1

第四行,串为“A B A B”,前缀为“A”和“A B”和“ABA”,后缀为“B”和“A B”和“BAB”,最大匹配长度为“A B”=="AB",所以为2

第五行,串为“A B ABC”,前缀为“A”和“A B”和“ABA”和“ABAB”,后缀为“C”和“B ABC”和“ABC”和“BC”均不匹配,所以为0

第六行,串为“A B ABCA”,前缀为“A”和“A B”和“ABA”和“ABAB”和“ABABC”,后缀为“A”和“CA”和“BCA”和“ABCA”和“BABCA”,最大匹配长度为“A”=="A",所以为1

得出最大前后缀长度
子串 AB ABA ABAB ABABC ABABCA
前缀   A、B、AB A、AB、ABA A、AB、ABA、ABAB A、AB、ABA、ABAB、ABABC
后缀   B、A、BA B、AB、BAB C、BC、ABC、BABC A、CA、BCA、ABCA、BABCA
最大     A、B AB  
最大长度

D、见下图KMP.4我们得到了一个数列“001201”,并不是我们的Next数组,把这个数列不妨叫做MLength,对应写在子串下面。

E、见下图KMP.5我们将MLength中最后一个元素即“1”删除,在开头增加一个元素“-1”,然后将整个MLength加一,即得到Next数组“011231”。到此我们便得到了Next数组,下面给出代码

Java代码:

    /**
     * 获取next数组的值
     * @param ps 模式串(匹配串)
     * @return 
     */
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
           if (k == -1 || p[j] == p[k]) { //判断是否匹配
               next[++j] = ++k;
           } else {
               k = next[k];
           }
        }
        return next;
    }

C/C++代码:

void GetNext(char* p,int next[]){  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1) {  
        if (k == -1 || p[j] == p[k]) {  //p[k]表示前缀,p[j]表示后缀  
            ++k;  
            ++j;  
            next[j] = k;  
        } else{  
            k = next[k];  
        }  
    }  
}  

优化KMP算法:

其实上面的方法求Next数组,有一定的缺陷,匹配不成功有时候需要将模式串的j回溯。所以对于已经匹配过的了,就不必再重新回到开头重新匹配了。

A、见下图KMP.6我们将在图KMP.5中修改,新增一行,分为两步给出NextVal数组。首先将第一个元素填为0

  1. 如果MLength[j]!=Next[j],则对应的填入Next中的数值
  2. 如果MLength[j]==Next[j],则对应的填入j-1对应的序号中的Next数值(这句话比较绕,多想想)这里的j是大于1的,即为j>1.这就解释了为什么首元素置零了。图中标的挺清楚,对应的符号一块看。

给出下面代码:

Java代码:

    /**
     * 优化后的获取next数组的值
     * @param ps 模式串(匹配串)
     * @return 
     */
    public static int[] getNextVal(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
           if (k == -1 || p[j] == p[k]) {
               if (p[++j] == p[++k]) { //增加了一层判断,当两个字符相等时要跳过,否则赋值为k
                  next[j] = next[k];
               } else {
                  next[j] = k;
               }
           } else {
               k = next[k];
           }
        }
        return next;
    }

C/C++代码:

void GetNextval(char* p, int next[]) {  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1) {  
        if (k == -1 || p[j] == p[k]) { //p[k]表示前缀,p[j]表示后缀  
           ++j;  
           ++k;  
      if (p[j] != p[k]) //只需要改动在下面4行,添加一步判断  
                next[j] = k;  
           else
              next[j] = next[k];  
        } else {  
            k = next[k];  
        }  
    }  
}

KMP算法:

Java代码:

    /**
     * 经典KMP算法
     * @param ts 主串(目标串)
     * @param ps 模式串(匹配串)
     * @return 
     */
    public static int KMPSearch(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);
        while (i < t.length && j < p.length) { //主要是这个循环
           if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,j也要归0
               i++;
               j++;
           } else {
               // i不需要回溯 i = i - j + 1;
               j = next[j]; // j回到指定位置
           }
        }
        if (j == p.length) {
           return i - j;
        } else {
           return -1;
        }
    }

C/C++代码:

int KmpSearch(char* s, char* p) {  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen) {     
        if (j == -1 || s[i] == p[j]){//j = -1,或当前字符匹配成功(即S[i]==P[j])则后移
            i++;  
            j++;  
        } else {        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}

结语:

这样就完成了著名的KMP算法,算法中重要的是算法所蕴含的思想,理解了具体的算理也就能迁移到其他方面了。

猜你喜欢

转载自blog.csdn.net/kjcxmx/article/details/82587924