zoukankan      html  css  js  c++  java
  • 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 实现的答案代码请戳这里

    其他参考资料:

  • 相关阅读:
    DateTime的精度小问题
    使用For XML PATH 会影响Cross Apply 返回
    一个update的小故事
    行大小计算测试
    Sql Server 2008R2 遇到了BCP导入各种中文乱码的问题
    php-fpm 启动不了 libiconv.so.2找不到
    Git使用教程
    支付宝接口使用文档说明 支付宝异步通知
    Linux(CentOs6.4)安装Git
    NGINX防御CC攻击教程
  • 原文地址:https://www.cnblogs.com/biaobiaoqi/p/3288772.html
Copyright © 2011-2022 走看看