前言

    前面博文分别介绍了字符串匹配算法《​​朴素算法​​​》、《​​Rabin-Karp​​​​算法​​​》和《​​有限自动机算法​​​》;本节介绍Knuth-Morris-Pratt字符串匹配算法(简称KMP算法)。该算法最主要是构造出模式串pat的前缀和后缀的最大相同字符串长度数组next,和前面介绍的《​​朴素字符串匹配算法​​》不同,朴素算法是当遇到不匹配字符时,向后移动一位继续匹配,而KMP算法是当遇到不匹配字符时,不是简单的向后移一位字符,而是根据前面已匹配的字符数和模式串前缀和后缀的最大相同字符串长度数组next的元素来确定向后移动的位数,所以KMP算法的时间复杂度比朴素算法的要少,并且是线性时间复杂度,即预处理时间复杂度是O(m),匹配时间复杂度是O(n)。

Java中的indexof()方法用的蛮力法,不过有优化

KMP字符串匹配算法实现

KMP算法预处理过程

    首先介绍下前缀和后缀的基本概念:

    前缀:字符串中除了最后一个字符,前面剩余的其他字符连续构成的字符或字符子串称为该字符串的前缀;

    后缀:字符串中除了首个字符,后面剩余的其他字符连续构成的字符或字符子串称为该字符串的后缀;

注意:空字符是任何字符串的前缀,同时也是后缀;

    例如:字符串“Pattern”的前缀是:“P”“Pa”“Pat”“Patt”“Patte”“Patter”;

          后缀是:“attern”“ttern”“tern”“ern”“rn”“n”;

    在进行KMP字符串匹配时,首先要求出模式串的前缀和后缀的最大相同字符串长度数组next;下面先看下例子模式串pat=abababca的数组next:其中value值即为next数组内的元素值,index是数组下标标号;注意:next[i]是pat[0..i]的最长前缀和后缀相同的字符串,包括当前位置i的字符。之所以是这样,是因为这里讲解的KMP算法是最基本的,没有经过优化的,若要进行优化,则必须优化next数组,下面会介绍优化数组。 


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. char:  | a | b | a | b | a | b | c | a |
  2. index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
  3. value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |




" a "的前缀和后缀都为空集,最大相同字符子串的长度为 0 ;

-"

ab "的前缀为[ a ],后缀为[ b ],不存在最大相同字符子串,则长度为 0 ;

-"

aba "的前缀为[ a, ab ],后缀为[ ba, a ],最大相同字符子串[a]的长度为1;

-"

abab "的前缀为[a, ab, aba],后缀为[bab, ab, b],最大相同字符子串[ab]的长度为 2 ;

-"

ababa "的前缀为[ a, ab, aba, abab ],后缀为[ baba, aba, ba, a ],最大相同字符子串[ aba ]的长度为 3 ;

-"

ababab "的前缀为[ a, ab, aba, abab, ababa ],后缀为[ babab, abab, bab, ab, b ],最大相同字符子串[ abab ]的长度为 4 ;

-"

abababc "的前缀为[a, ab, aba, abab, ababa,ababab],后缀为[bababc, ababc, babc, abc, bc, c],不存在最大相同字符子串,则长度为0。

-"abababca"的前缀为[a, ab, aba, abab, ababa,ababab,abababc],后缀为[bababca, ababca, babca, abca, bca, ca,a],最大相同字符子串[a]的长度为1。


模式串的前缀和后缀的最大相同字符串长度数组next的递推求解

已知next[0..i-1],求出next[i]:

  1. 若P[i]=P[len],则next[i]=++len;i++继续查找下一个字符的next元素值;
  2. 若P[i]!=P[len],则分为两步:

  • 若len!=0,递归查找,即比较next前一个元素值所在位置的字符P[next[len-1]]与P[i],因此i不变,而len=next[len-1];
  • 若len=0,则当前字符的next元素值为0,即next[i]=0;此时len不变,i++查找下一个位置字符的next元素值;

   下面给出求解模式串 next 数组的代码:



[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. void computeNextArray(const string &pat, int M, int *next)
  2. {
  3. int len = 0;  // lenght of the previous longest prefix suffix
  4. int i = 1;
  5. // next[0] is always 0

  6. // the loop calculates next[i] for i = 1 to M-1
  7. while(i < M)
  8. {
  9. if(pat[i] == pat[len])
  10. {
  11. len++;
  12. next[i] = len;
  13. i++;
  14. }
  15. else // (pat[i] != pat[len])
  16. {
  17. if( len != 0 )
  18. // This is tricky. Consider the example AAACAAAA and i = 7.
  19. len = next[len-1];
  20. // Also, note that we do not increment i here
  21. }
  22. else // if (len == 0)
  23. {
  24. next[i] = 0;
  25. i++;
  26. }
  27. }
  28. }
  29. }



KMP算法字符串匹配过程

  1. 若当前对应字符匹配成功即pat[j] = txt[i],则i++,j++,继续匹配下一个字符;
  2. 若当前对应字符匹配失败即pat[j] != txt[i],则分为两步:

  • 若模式串当前字符的位置j!=0时,此时,模式串相对于文本字符串向后移动j - next[j-1]位,文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1],继续匹配字符;
  • 若模式串当前字符的位置j=0时,此时只需更新文本字符串的当前位置i++,其他不变,继续匹配下一个字符;

    源码实现如下:



[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. void KMPSearch(const string &pat, const string &txt)
  2. {
  3. int M = pat.length();
  4. int N = txt.length();

  5. // create next[] that will hold the longest prefix suffix values for pattern
  6. int *next = (int *)malloc(sizeof(int)*M);
  7. int j  = 0;  // index for pat[]

  8. // Preprocess the pattern (calculate next[] array)
  9. computeNextArray(pat, M, next);

  10. int i = 0;  // index for txt[]
  11. while(i < N)
  12. {
  13. if(pat[j] == txt[i])
  14. {
  15. j++;
  16. i++;
  17. }

  18. if (j == M)
  19. {
  20. "Found pattern at index:"<< i-j<<endl;
  21. j = next[j-1];
  22. }

  23. // mismatch after j matches
  24. else if(pat[j] != txt[i])
  25. {
  26. // Do not match next[0..next[j-1]] characters,
  27. // they will match anyway
  28. if(j != 0)
  29. j = next[j-1];
  30. else
  31. i = i+1;
  32. }
  33. }
  34. // to avoid memory leak
  35. }


下面举例,模式串 p at = “ abababca ” , 输入文本字符串  text = “ bacbababaabcbab ”。

    由上面可知next表元素值如下


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. char:  | a | b | a | b | a | b | c | a |
  2. index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
  3. value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |


      下面是匹配过程

    第一次匹配成功的字符为相对应字符a,由于模式串下一个字符b与文本字符c不匹配,且j=1、已匹配字符数为j=1,next[j-1]=0;所以下一次向后移动的位数为j-next[j-1]=1-0=1;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=0;


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. bacbababaabcbab
  2. |
  3. abababca


           第二次匹配成功的是字符ababa;由于模式串下一个字符b与文本字符a不匹配,且j=5、已匹配字符数j=5、next[j-1]=3;所以下一次向后移动的位数为j-next[j-1]=5-3=2;即忽略两位文本字符;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=3;


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. bacbababaabcbab
  2. |||||
  3. abababca


经过上一步向后移动后的字符匹配为下面所示; 由于模式串下一个字符 b 与文本字符 a 不匹配,且 j=3 、已匹配字符数 j=3 、 next[j-1]=1 ;则下一次匹配是向后移动位数为j-next[j-1]=3-1=2;即忽略两位文本字符;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=1;


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. // x denotes a skip

  2. bacbababaabcbab
  3. xx|||
  4. abababca


经过前一步的移动后得到下面的匹配; 由于模式串下一个字符 b 与文本字符 a 不匹配,且 j=1 、已匹配字符数 j=1 、 next[j-1]=0 ; 则下一次匹配是向后移动位数为j-next[j-1]=1-0=1;但是此时,模式串的字符长度大于待匹配的文本字符长度,所以,模式串匹配失败,即在文本字符串中不存在与模式串相同的字符串;


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. // x denotes a skip

  2. bacbababaabcbab
  3. xx|
  4. abababca


完整程序:


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. #include<iostream>
  2. #include<string>
  3. #include<stdlib.h>

  4. using namespace std;

  5. void computeNextArray(const string &pat, int M, int *next);

  6. void KMPSearch(const string &pat, const string &txt)
  7. {
  8. int M = pat.length();
  9. int N = txt.length();

  10. // create next[] that will hold the longest prefix suffix values for pattern
  11. int *next = (int *)malloc(sizeof(int)*M);
  12. int j  = 0;  // index for pat[]

  13. // Preprocess the pattern (calculate next[] array)
  14. computeNextArray(pat, M, next);

  15. int i = 0;  // index for txt[]
  16. while(i < N)
  17. {
  18. if(pat[j] == txt[i])
  19. {
  20. j++;
  21. i++;
  22. }

  23. if (j == M)
  24. {
  25. "Found pattern at index:"<< i-j<<endl;
  26. j = next[j-1];
  27. }

  28. // mismatch after j matches
  29. else if(pat[j] != txt[i])
  30. {
  31. // Do not match next[0..next[j-1]] characters,
  32. // they will match anyway
  33. if(j != 0)
  34. j = next[j-1];
  35. else
  36. i = i+1;
  37. }
  38. }
  39. // to avoid memory leak
  40. }

  41. void computeNextArray(const string &pat, int M, int *next)
  42. {
  43. int len = 0;  // lenght of the previous longest prefix suffix
  44. int i = 1;
  45. // next[0] is always 0

  46. // the loop calculates next[i] for i = 1 to M-1
  47. while(i < M)
  48. {
  49. if(pat[i] == pat[len])
  50. {
  51. len++;
  52. next[i] = len;
  53. i++;
  54. }
  55. else // (pat[i] != pat[len])
  56. {
  57. if( len != 0 )
  58. // This is tricky. Consider the example AAACAAAA and i = 7.
  59. len = next[len-1];
  60. // Also, note that we do not increment i here
  61. }
  62. else // if (len == 0)
  63. {
  64. next[i] = 0;
  65. i++;
  66. }
  67. }
  68. }
  69. }

  70. int main()
  71. {
  72. "ABABDABACDABABCABAB";
  73. "ABABCABAB";
  74. KMPSearch(pat, txt);
  75. "pause");
  76. return 0;
  77. }


数组next的优化


    优化求出模式串的前缀和后缀的最大相同字符串长度数组next;下面先看下例子模式串pat=abab的优化数组next:index是数组下标标号,shift标志value值向右移一位之后,并把第一个值初始化为-1的值,next数组内的元素值是对shift值进一步优化;注意:next[i]是pat[0..i]的最长前缀和后缀相同的字符串,不包括当前位置i的字符,所以这里是优化之后的next数组。 


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. char:  | a  | b | a  | b |
  2. index: | 0  | 1 | 2  | 3 |
  3. value: | 0  | 0 | 1  | 2 |
  4. shift:| -1 | 0 | 0  | 1 |
  5. next: | -1 | 0 | -1 | 0 |


下面通过例子讲解优化的过程,假设输入文本字符串和模式串分别为 txt = "abacababc",pat = "abab";

    第一次匹配成功如下,若根据没有优化的数组进行匹配时,优化之前的数组为shift,则当前模式串字符b与文本字符c不匹配,当前匹配失败的字符位置是j=3;则模式串右移j-shift[j] = 3-1=2位,



[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. abacababc
  2. |||
  3. abab


经过上一步骤后,模式串字符b还是与文本字符c失配。而且失配对应的字符和上一步骤完全一样。事实上,因为在上一步的匹配中,已经得知pat[3] = b,与txt[3] = c失配,而右移两位之后,让pat[shift[3]] = pat[1] = b再跟txt[3]匹配时,必然失配。


[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. //x denotes a skip
  2. abacababc
  3. xx|
  4. abab





问题是因为出现 pat[shift [j]]=pat[j];因为当pat[j] != txt[i]时,下次匹配必然是pat[shift[j]]跟txt[i]匹配,如果pat[shift[j]]=pat[j],必然导致后一步匹配失败,所以不能允许pat[shift[j]]=pat[j]。如果出现了pat[shift[j]]=pat[j],则需要再次递归,即令shift[j]=shift[shift[j]]。则优化后的数组shift就是数组next;


我们重新看下模式串pat=abab的优化数组next;下面是优化数组next的操作过程:



[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. ___________________________________________________________________________________
  2. |char:    | a             | b                 | a               | b               |
  3. |_________|_______________|___________________|_________________|_________________|
  4. |index:   | 0             | 1                 | 2               | 3               |
  5. |_________|_______________|___________________|_________________|_________________|
  6. |value:   | 0             | 0                 | 1               | 2               |
  7. |_________|_______________|___________________|_________________|_________________|
  8. |shift:  | -1            | 0                 | 0               | 1               |
  9. |_________|_______________|___________________|_________________|_________________|
  10. |reason:  | The initial   | p[1]!=p[shift[1]] | p[2]=p[shift[2]]| p[3]=p[shift[3]]|
  11. |         |value unchanged|                   |                 |                 |
  12. |_________|_______________|___________________|_________________|_________________|
  13. |operator:|do nothing     |do nothing         | shift[2]=       | shift[3]=       |
  14. |         |               |                   | shift[shift[2]] | shift[shift[3]] |
  15. |_________|_______________|___________________|_________________|_________________|
  16. |next:   | -1            | 0                 | -1              | 0               |
  17. |_________|_______________|___________________|_________________|_________________|




下面给出优化后的程序:



[cpp]  ​​view plain​​​ ​​​copy​​​ ​​​​​​ ​​​​



  1. #include <iostream>
  2. #include <string>
  3. #include<stdlib.h>
  4. using namespace std;


  5. void computeNextArray(const string &pat, int M, int *next)
  6. {
  7. int j=0,k=-1;
  8. //优化next,初始值为-1
  9. while(j<M-1)
  10. {
  11. if(k==-1 || pat[j]==pat[k])
  12. {
  13. ++j;
  14. ++k;
  15. if(pat[j]!=pat[k])next[j]=k;
  16. //因为不能出现pat[j] = pat[ next[j ]],所以当出现时需要继续递归
  17. else next[j]=next[k];
  18. }
  19. else k=next[k];
  20. }
  21. }


  22. void kmpSearch(const string&txt,const string&pat)
  23. {
  24. int i=0,j=0;
  25. int N = txt.length();
  26. int M = pat.length();
  27. int *next = (int *)malloc(sizeof(int)*M);
  28. computeNextArray(pat, M, next);
  29. "The value of next are:";
  30. for ( i = 0; i < M; i++)
  31. {
  32. " ";
  33. }
  34. cout<<endl;
  35. //注意:i的值必须为0,因为从第一个字符开始比较
  36. while(i<N && j<M)
  37. {
  38. if(j==-1 || txt[i]==pat[j])
  39. {
  40. i++;
  41. j++;
  42. }
  43. else j=next[j];
  44. }
  45. if(j==M)cout<<"Found pattern at index:"<< i-j<<endl;
  46. free(next);
  47. }


  48. int main()
  49. {
  50. "aacababc";
  51. "abab";
  52. kmpSearch(txt,pat);
  53. "pause");
  54. return 0;
  55. }




参考资料:

《算法导论》

​http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/​

​http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/​

​http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html​

​http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm​

​http://dsqiu.iteye.com/blog/1700312​