KMP算法实现

本文描述了单模式的字符串匹配的经典算法 KMP 算法的实现。首先对字符串匹配算法做简单的介绍,然后是 KMP 算法的实现描述,最后推荐两道简单的 ACM 模板题做练手用。

字符串匹配算法

字符串匹配(String Matchiing)也称字符串搜索(String Searching)是字符串算法中重要的一种,是指从一个大字符串或文本中找到模式串出现的位置。一个基本的字符串匹配算法分类如下:

  • 单模式匹配:即每次算法执行只需匹配出一个模式串。
  • 有限集合的多模式匹配:即算法需要同时找出多个模式串的匹配结果,而这个模式串集合是有限的。
  • 无限集合的多模式匹配:如正则表达式的匹配。

单模式匹配最容易理解,构造也非常简单。一个最朴素的思路就是从文本的第一个字符顺次比较模式串,不匹配则重新从下一个字符开始匹配,直到文本末尾。Java 实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
 public static boolean bruteforce(String str1, String str2) {  for (int i = 0, j = 0; i!= str1.length(); ) {  if (str1.charAt(i) == str2.charAt(j)) {  j ++;  i ++;  if (j == str2.length()) return true;  }else {  i = i - j + 1;  j = 0;  }  }  return false;  } 

但是这种算法,有明显的效率黑洞。因为每次匹配失败后,都会回到原来的匹配起点的下一个字符开始匹配,这些步骤很多情况下,并不是必要的。

实际上这些字符很有可能已经被读入了一次。理论上,如果我们能对所有被读入过的字符有足够的了解,那就能判定是否能避免再次读入一遍做匹配运算了。经典的 KMP 算法正是基于这点思考,对原有的蛮力算法做出了优化。

KMP 算法

网络上关于 KMP 算法的描述很多,其中个人觉得阮一峰老师的《字符串匹配的 KMP 算法》对 KMP 的描述最为简明和清晰。图例展示的算法流程更容易让人接受和理解。这里仅记录我所认为重点的知识点。

算法的思想

相比蛮力算法,KMP 算法预先计算出了一个哈希表,用来指导在匹配过程中匹配失败后尝试下次匹配的起始位置,以此避免重复的读入和匹配过程。这个哈希表被叫做“部分匹配值表(Particial match table)”,它的设计是算法精妙之处。

部分匹配值表

要理解部分匹配值表,就得先了解字符串的前缀(prefix)和后缀(postfix)。

  • 前缀:除字符串最后一个字符以外的所有头部串的组合。
  • 后缀:除字符串第一个字符以外的所有尾部串的组合。
  • 部分匹配值:一个字符串的前缀和后缀中最长共有元素的长度。

举例说明:字符串ABCAB

  • 前缀:{A, AB, ABC, ABCA}
  • 后缀:{BCAB, CAB, AB, B}
  • 部分匹配值:2 (AB)

而所谓的部分匹配值表,则为模式串的所有前缀以及其本身的部分匹配值。

举例如下:还是针对字符串ABCAB,它的部分匹配值表为:

1
2
A B C A B 0 0 0 1 2 

这代表着:字符串A B C A B 中,子串A B C的部分匹配值为 0,而子串A B C A的部分匹配值为 1,诸如此理。

算法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static int[] next;  public static boolean kmp(String str, String dest) {  // i stands for index of str string, j stands for index in dest string.  // At the beginning of each loop process, j is the new position of dest  // taht should be compared.  for (int i = 0, j = 0; i < str.length(); i++) {  while (j > 0 && str.charAt(i) != dest.charAt(j))  // This loop is to get a matching character recursively. Another  // stop condition is when particial match value meets end.  j = next[j - 1];// As i in str and j in dest is comparing,  // recomputing of j should be in the former  // character substring, which is next[j-1]   if (str.charAt(i) == dest.charAt(j))  j++;   if (j == dest.length())  return true;  }   return false; }  public static int[] kmpNext(String str) {  int[] next = new int[str.length()];  next[0] = 0;  // i stands for index of string, j is temporary for particail match  // values computing, at the beginning of each loop process, j is the  // particial match value of former character .  for (int i = 1, j = 0; i < str.length(); ++i) {  while (j > 0 && str.charAt(i) != str.charAt(j))  // This loop is to get a matching character recursively. Another  // stop condition is when particial match value meets end.  j = next[j - 1];// j will be recomputed in the recursion. Take  // care that next[j-1] is the particial match  // value of the first j characters substirng.   if (str.charAt(i) == str.charAt(j)) // If not in this case, j must  // meets end, equals to zero.  ++j;   next[i] = j;  }  return next; } 

理解算法实现时,有几点特别需要注意:

  • 在生成部分匹配值数组的 kmpNext()方法中,第一层循环内,i是字符串的索引,而j则在每次循环开始时代表了i所指定字符之前的子串的部分匹配值。
  • kmpNext()方法的内层 while()循环,是为了迭代得到让i指定字符匹配到的情况。有另外一种实现方案:不有用这一层循环,而是直接使用一层循环,在大循环内部做 j 值变更的判定即可。
  • kmpNext()方法的 while()循环中,需要特别注意是next[j -1],部分匹配值 j 对应到的是字符串中的第j-1个字符。
  • kmp()的循环代码和 kmpNext()部分匹配值表生成的循环代码很类似。两者使用了相同方式,在字符匹配失败后迭代获取新的可匹配情况,且都是利用了 next 数组。

其他

KMP 算法虽然能达到 O(M+N)的算法复杂度,但在实际使用中,KMP 算法的性能并不如 BM 算法强。

模板题

基础模板题

HDOJ 的 2203 题是一个能检验算法正确性的模板题。Java 实现的答案代码请戳这里

延伸模板题

POJ 的 2406 题,对考察点做了巧妙的变形,对更深入的理解 KMP 中的部分匹配表(即 next 数组)很有帮助。Java 实现的答案代码请戳这里

HDOJ 的 1867 题也属于 kmp 的变形。要求对 kmp 利用 next 数组进行比较的过程有清晰的认识。Java 实现的答案代码请戳这里

其他参考资料:

你可能感兴趣的:(KMP)