算法:KMP 算法实现及详解

1、求下标 K 所对应的 next 数组对应值的具体方法:

  1. 观察下标 K-1 对应 字符串 的值,令 X 为下标 K -1 对应的 next 数组的值(X = next [ K-1 ])
  2. X 为 -1,则下标 K 对应的 next 数组值为 0,否则进入步骤3
  3. 观察下标为 X 的 字符串 的值是否与下标 K-1 对应 字符串 的值相等,若相等则下标 K 对应的 next 数组值为 X+1,否则进入步骤4
  4. X = next[X] ,返回步骤3

对字符串 “abaababaa” 共 9 个字符求 next 数组,下表为结果:

字符数组 a b a a b a b a a
下标 0 1 2 3 4 5 6 7 8
next数组 -1 0 0 1 1 2 3 2 3

分解求解步骤

1、初始化,令next[0] = -1;

字符         |  a  |  b  |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |

2、求解下标为1对应的next数组,观察 next[0] 的值 X = -1,则 next[1] = 0;

字符         |  a  |  b  |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next          -1 |  0  |

3、求解下标为2对应的next数组,观察 next[1] 的值 X = 0,观察下标 1 对应的字符 b ,

下标为 X(0) 对应的字符为 a ,两者不一致,则令 X=next [0](X=0)即 X = -1,即 next[2] = 0;

字符         |  a  | |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         -1 |  0  | 

4、求解下标为3对应的next数组,观察 next[2] 的值 X = 0,观察下标 2 对应的字符 a ,

下标为 X(0) 对应的字符为 a ,两者一致,则 next [3] = next[2] + 1,即 next[3] = 1;

字符         | |  b  |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  | |  1  | 

5、求解下标为4对应的next数组,观察 next[3] 的值 X = 1,观察下标 3 对应的字符 a ,

下标为 X(1) 对应的字符为 b ,两者不一致,则令 X=next [X] (X=1)即 X = 0,

下标为 X(0) 对应的字符为 a,两者一致,则 next [4] = next[1] + 1,即 next[4] = 1;

字符         |  b  |  a  | |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  

6、求解下标为5对应的next数组,观察 next[4] 的值 X = 1,观察下标 4 对应的字符 b ,

下标为 X(1) 对应的字符为 b ,两者一致,则 next [5] = next[4] + 1,即 next[5] = 2;

字符         |  a  | |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  2  |  

7、求解下标为6对应的next数组,观察 next[5] 的值 X = 2,观察下标 5 对应的字符 a ,

下标为 X(2) 对应的字符为 a ,两者一致,则 next [6] = next[5] + 1,即 next[6] = 3;

字符         |  a  |  b  |  |  a  |  b  |  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  |  3  | 

8、求解下标为7对应的next数组,观察 next[6] 的值 X = 3,观察下标 6 对应的字符 b ,

下标为 X 对应的字符为 a ,两者不一致,则令 X=next [X] (X=3)即 X = 1,

下标为 X(1) 对应的字符为 b,两者一致,则 next [7] = next[3] + 1,即 next[7] = 2;

字符         |  a  |  |  a  |  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  2  |  |  2  | 

9、求解下标为8对应的next数组,观察 next[7] 的值 X = 2,观察下标 7 对应的字符 a ,

下标为 X 对应的字符为 a ,两者一致,则 next [8] = next[7] + 1,即 next[8] = 3;

字符         |  a  |  b  |  a  |  a  |  b  |  a  |  b  |  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  2  |  3  |  |  3  |


2、观察法求出 next 数组:

  • 求下标 K 的 next 数组的值,即从字符串首往后到下标 K - 1 观察得出前缀,从下标 K -1 往前到串首观察得出后缀
  • 观察前缀和后缀一致时的长度最大值,该值即为下标 K对应的next 数组的值

前缀:整个字符串"abaababaa"的前缀为 a | ab | aba | abaa | abaab | abaaba | abaabab | abaababa

后缀:整个字符串"abaababaa"的前缀为 a | aa | baa | abaa | babaa | ababaa | aababaa | baababaa

例子1:求解下标为 4 的 next 数组的值,即观察 abaa 的前缀 [ a , ab , aba ],后缀 [ a , aa , baa ],

前缀后缀一致的只有 a ,长度为 1,则 next [ 4 ] = 1

例子2:求解下标为 6 的 next 数组的值,即观察 abaaba 的前缀 [ a,ab,aba,abaa,abaab ],

后缀 [ a,ba,aba,aaba,baaba],前后缀一致的有 a,aba,长度分别为 1 和 3,则取最大值 next [ 6 ] = 3

字符         |  a  |  b  |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  2  |  3  |  2  |  3  |


3、求解next数组,C++代码实现:

vector GetNext(string s)
{
    vector next(s.size(), 0); // 初始化一个 s 长度的 next 数组
    next[0] = -1;                  // 初始化下标 0 next 数组的值
    int k = 0;                     // 存储字符串从开始到目前下标前一位的前后缀的对称程度
    for (int i = 2; i < s.size(); ++i)       
    {
        while (k > 0 && s[i - 1] != s[k])
            k = next[k];
        if (s[i - 1] == s[k])
            k++;
        next[i] = k;
    }
    return next;                          
}

发生不匹配时的操作步骤详解:

当匹配字符串为“abaababaa”时,有下表:

字符         |  a  |  b  |  a  |  a  |  b  |  a  |  b  |  a  |  a  |

下标         |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |

next         |  -1 |  0  |  0  |  1  |  1  |  2  |  3  |  2  |  3  |

例子3:当下标 3 不匹配时,返回到对称的前缀的下一位查看,如 abaa 中,有前缀和后缀 a 相同,当发生不匹配时,可直接从 abaa 返回到 abaa ,然后继续进行匹配;

例子4:当下标 6 不匹配时,观察 abaabab ,有前缀和后缀 aba 相同,当发生不匹配时,可直接从 abaabab 返回到 abaabab ,下标返回到 3 ,然后继续进行匹配;

例子5:当下标 8 不匹配时,观察 abaababaa ,有前缀和后缀 aba 相同,当发生不匹配时,可直接从 abaababaa 返回到 abaababaa,下标返回到 3 ,然后继续进行匹配;

通过例子3-5可知:前后缀相同时,它们的长度代表已匹配成功的字符串长度,因此只需回到前缀的下一位继续进行匹配即可,因为通过后缀匹配成功可证明前缀必匹配;


4、KMP算法,C++代码实现:

int KMP(string S, string str_match)
{
    vector next = getNext(str_match);
    int i = 0;    // 被匹配字符串的下标
    int j = 0;    // 匹配字符串的下标
    while (S[i] != '\0' && str_match[j] != '\0')
    {
        if (j == -1||S[i] == str_match[j])
        {
            ++i;
            ++j;
        }
        else
        {
            j = next[j];
        }
    }
    if (str_match[j] == '\0')
        return i - j;
    else
        return -1;
}

KMP详解:

逻辑1:当 S[ i ] 与 str_match [ 0 ] 不匹配时(j = 0),将 i 往后移动一位,j 设置为 0;

逻辑2:当 S[ i ] 与 str_match [ j ] 匹配时(j >= 0),将 i ,j 同时往后移动一位;

逻辑3:当 S[ i ] 与 str_match [ j ] 不匹配时(j >= 0),将找到与后缀相同的前缀的下一位,即 j = next [ j ];

思考(如何将逻辑1和逻辑3统一):

  • 当 j = 0时,若S[ i ] 与 str_match [ j ] 不匹配,由逻辑1和3知, 需要将 i 往后移动一位且保持 j = 0,且令j = next [ 0 ] 且;
  • 即 (i++,j 为0)和(j = next [ 0 ] );
  • 观察逻辑2的操作可知,逻辑2也需要将 i 往后移动一位,但同时将 j 往后移动一位;
  • 即( i++,j++);
  • 若先进行(j = next [ 0 ] )的操作,并在下一步进行 i++ 操作且保证 j = 0 ,思考是否可行;
  • 若令 next [ 0 ] = -1,通过 j = next [ 0 ] 得到 j = -1,之后进行 i++ 操作,j此时需要保证 j = 0,则可进行 j++ 操作,产生逻辑4这也是为何要将next [ 0 ] 初始化为 -1 的原因);

逻辑4:当 j 为 -1 时,将 i ,j 同时往后移动一位;


字符串匹配的其他算法:

BM算法、Horspool算法、Sunday算法、RK算法

你可能感兴趣的:(算法)