【搞定算法】KMP 算法

迈不过友情╰ 2021-12-14 07:57 419阅读 0赞

目 录:

1、问题描述

2、next 数组

3、代码实现

4、KMP 的应用

4.1、子树问题

4.2、加最短字符问题


前面讲过字符串匹配的其他几种算法:字符串匹配算法之 BF、RK、BM。

本文用来讲解 KMP 算法及其应用,KMP 算法时间复杂度为:O(N + M),空间复杂度为:O(M)。

1、问题描述

给定两个字符串 O 和 f,长度分别为 n 和 m,判断 f 是否在 O 中出现,如果出现则返回出现的位置。常规暴力方法是遍历 o 的每一个位置,然后从该位置开始和 f 进行匹配,但是这种方法的复杂度是 O(N x M)。KMP 算法通过一个 O(M) 的预处理(next 数组)可以加速匹配速度,使匹配的复杂度降为 O(N + M)。

68747470733a2f2f696d672d626c6f672e6373646e2e6e65742f32303137303332393136313135363438393f77617465726d61726b2f322f746578742f6148523063446f764c324a736232637559334e6b626935755a5851766257467558334e706232343d2f666f6e742f3561364c354c32542f666f6e7473697a652f3430302f66696c6c2f49304a42516b46434d413d3d2f646973736f6c76652f37302f677261766974792f43656e746572

2、next 数组

注意:next 数组是针对标准串而言的(上图中 f 是标准串、O 是母串)。

其实字符串匹配算法理解起来并不难,非常直观,结果就要求要匹配的字符在两个串中一一对应。但是为了提高暴力解法的效率,就必须提高字符串的匹配速度,其实就是解决每次匹配失败如何往前多移几位的问题,不要每次都是匹配失败,移动一位再重头开始。next 数组就是来做这件事情的,每次匹配失败,查找匹配串匹配失败位置处对应的 next 值,标准串向前移动 next 值那么多的长度,然后再继续和母串匹配。

那下面就讲一下 next 数组究竟是什么:

如上图所示:next 数组存放的是字符串 f 的 i 位置前面字符串的最长前缀和最长后缀的匹配长度【前缀不能扩到最后一个字符,后缀也不能扩到第一个字符】。

  • A 段字符串是 f 在 i 位置的最长前缀子串;
  • B 段字符串是 f 在 i 位置的最长后缀子串;
  • A 段字符串和 B 段字符串相等。

分析:在字符串 O 中寻找 f,当匹配到位置 i 时两个字符串不相等,这时我们需要将字符串 f 向后移动。常规方法是每次向后移动 1 位,但是它没有考虑前 i - 1 位已经比较过这个事实,所以效率不高。KMP就是要加速这个过程:

  • 前提:

1、在两个数组都没有越界的范围内(str1 是母串、str2 是标准串):

2、next[] 数组【存放 i 位置前面字符串的前缀和后缀的最长匹配长度】的求解方法

(1)next 数组下标为 0 的位置人为规定值为 - 1;

(2)next 数组下标为 1 的位置人为规定值为 0,因为前面只有一个字符,但前缀不能扩到最后一个字符,后缀也不能扩到第一个字符,所以人为规定为 0;

(3)求 i 位置的值,即 next[i] :利用前面的已得到的结果,cn 表示跳到的位置,即需要和 i-1 位置字符比较的位置

<1> str[cn] 和 str[i - 1] 相等,则得到结果 next[i++] = ++cn;

<2> 不相等就要继续往前面跳(跳到 next[cn] 处),直到有相等或者 cn = -1 没办法跳了。

  • 过程:

1、如果 str1 的 p1 位置和 str2 的 p2 位置的值相等,则 p1++、p2++;

2、如果 str1 的 p1 位置和 str2 的 p2 位置的值不相等:

(1)如果 p2 已经是 0 位置了(next[p2] = -1),则 p2 就不能往前走了,需要 p1++ 【因为 str1 的当前位置和 str2 的 0位置都不匹配,所以str1 要到下一个位置】;

(2)否则 p2 = next[p2](next[p2] 是 p2 位置前面字符串的最长前缀和最长后缀的匹配长度,从新的 p2 位置开始和 str1 的当前位置比较下去,即图中的 A 的下一位和 a 继续比较);

  • 实质:

1、尝试解决位置 j [O 中 B 的第一位] 开头能否匹配出 str2;

2、认为从 j 到 i 中间位置一律配不出 str2;

  • 越界:

1、如果是 p2 越界,说明标准串 str2 已经遍历完了,即 str1 匹配出了 str2;

2、如果是 p1 越界,说明母串 str1 已经遍历完了也没有匹配出标准串 str1。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Bjd2wxMjA2_size_16_color_FFFFFF_t_70

举例:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Bjd2wxMjA2_size_16_color_FFFFFF_t_70 1

3、代码实现

  1. public class KMP {
  2. /**
  3. * 主函数:返回 str2 在 str1 中第一次出现的位置
  4. * @param str1 :母串
  5. * @param str2 :标准串
  6. * @return str2 在 str1 中第一次出现的位置
  7. */
  8. public static int getIndexOf(String str1, String str2){
  9. if(str1 == null || str2 == null || str2.length() < 1 || str1.length() < str2.length()){
  10. return -1;
  11. }
  12. int p1 = 0; // str1的指针
  13. int p2 = 0; // str2的指针
  14. char[] s1 = str1.toCharArray();
  15. char[] s2 = str2.toCharArray();
  16. int[] next = getNextArray(s2);
  17. // 循环结束一顶是有一个数组越界了,即遍历完了
  18. while(p1 < str1.length() && p2 < str2.length()){
  19. if(s1[p1] == s2[p2]){
  20. // 继续往后匹配,两个一起往后走
  21. p1++;
  22. p2++;
  23. }else{
  24. // 不相等的时候,就要利用next数组往前跳
  25. if(next[p2] == -1){
  26. //s2到0位置了,没办法往前跳了,而你s1当前位置和我的0位置都不匹配,s1得往后走一步
  27. p1++;
  28. }else{
  29. p2 = next[p2];
  30. }
  31. }
  32. }
  33. // 只有是当p2越界跳出的循环,在str1中才会存在str2,否则不存在
  34. return p2 == s2.length ? p1 - p2 : -1;
  35. }
  36. // 求 str2 的 next 数组
  37. public static int[] getNextArray(char[] str2){
  38. int[] next = new int[str2.length];
  39. next[0] = -1; // 人为规定
  40. if(str2.length == 1){
  41. return next;
  42. }
  43. // 长度不止1时
  44. next[1] = 0; // 人为规定
  45. int i = 2; // 从左往右求每一个 i 的 next 值
  46. /**cn 有两层意思:
  47. * 1、cn 表示要跳到的位置,即需要和 i-1 位置处字符比较的位置
  48. * 2、cn 就是 i-1 处的next值,即 i-1处的最长前缀和最长后缀的匹配值
  49. */
  50. int cn = 0;
  51. while(i < next.length){
  52. if(str2[i-1] == str2[cn]){
  53. //得到了 i 位置的next值,可以求i+1位置的next值了
  54. next[i++] = ++cn;
  55. }else{
  56. // 不相等,就要往前跳,直到跳到next值为-1的位置,即0位置
  57. if(next[cn] == -1){
  58. // 不能再继续跳了,已经到0位置了,且0位置和i-1位置不相等
  59. next[i++] = 0;
  60. }else{
  61. // 继续往前跳,继续比较
  62. cn = next[cn];
  63. }
  64. }
  65. }
  66. return next;
  67. }
  68. public static void main(String[] args) {
  69. String str = "abcabcababaccc";
  70. String match = "ababa";
  71. System.out.println(getIndexOf(str, match));
  72. }
  73. }

20190706150357843.png

时间复杂度分析:

1、getNextArray 函数的时间复杂度 O(M)。设 str2 的长度为 M ,分析其时间复杂度的困惑在于,在 while 里面不是每次循环都执行 ++i 操作,所以整个 while 的执行次数不一定为 M。换个角度,注意到在每次循环中,无论 if 还是 else 都会修改 cn 的值且每次循环仅对 cn 进行一次修改,所以在整个 while 中 cn 被修改的次数即为 getNextArray 函数的时间复杂度。

2、那么每次成功匹配时,++i; ++n; , 由于 ++i 最多执行 M-1 次,故 ++j 也最多执行 M-1 次,即 cn 最多增加 M-1 次。对应的,只有在 cn =next[cn] 处, cn 的值一定会变小,由于 cn 最多增加 M - 1 次,故 n 最多减小 M - 1 次。所以 时间复杂度为 2M。

3、综上所述:getNextArray 函数的时间复杂度为 O(M),若母串长度为 M,标准串长度为 N,则 KMP 的时间复杂度为:O(M + N)。

4、KMP 的应用

4.1、子树问题

问题:T2 是不是 T1 的子树,即 T1 的某棵子树完全和 T2 一样,就说 T1 包含 T2。

分析:把 T1 先序遍历,序列化为字符串,把 T2 也序列化为字符串,利用 KMP 算法如果前者包含后者,则说明 T1 包含 T2。

  1. public class SubTree {
  2. public static class Node{
  3. public int val;
  4. public Node left;
  5. public Node right;
  6. public Node(int val){
  7. this.val = val;
  8. }
  9. }
  10. public static boolean isSubTree(Node T1, Node T2){
  11. String t1Str = serialByPre(T1);
  12. String t2Str = serialByPre(T2);
  13. return getIndexOf(t1Str, t2Str) != -1;
  14. }
  15. public static String serialByPre(Node root){
  16. if(root == null){
  17. return "#_";
  18. }
  19. String res = root.val + "_";
  20. res += serialByPre(root.left);
  21. res += serialByPre(root.right);
  22. return res;
  23. }
  24. public static int getIndexOf(String str1, String str2){
  25. if(str1 == null || str2 == null || str2.length() < 1 || str1.length() < str2.length()){
  26. return -1;
  27. }
  28. int p1 = 0; // str1的指针
  29. int p2 = 0; // str2的指针
  30. char[] s1 = str1.toCharArray();
  31. char[] s2 = str2.toCharArray();
  32. int[] next = getNextArray(s2);
  33. // 循环结束一顶是有一个数组越界了,即遍历完了
  34. while(p1 < str1.length() && p2 < str2.length()){
  35. if(s1[p1] == s2[p2]){
  36. // 继续往后匹配,两个一起往后走
  37. p1++;
  38. p2++;
  39. }else{
  40. // 不相等的时候,就要利用next数组往前跳
  41. if(next[p2] == -1){
  42. //s2到0位置了,没办法往前跳了,而你s1当前位置和我的0位置都不匹配,s1得往后走一步
  43. p1++;
  44. }else{
  45. p2 = next[p2];
  46. }
  47. }
  48. }
  49. // 只有是当p2越界跳出的循环,在str1中才会存在str2,否则不存在
  50. return p2 == s2.length ? p1 - p2 : -1;
  51. }
  52. // 求 str2 的 next 数组
  53. public static int[] getNextArray(char[] str2){
  54. int[] next = new int[str2.length];
  55. next[0] = -1;
  56. if(str2.length == 1){
  57. return next;
  58. }
  59. // 长度不止1时
  60. next[1] = 0;
  61. int i = 2;
  62. int cn = 0;
  63. while(i < next.length){
  64. if(str2[i-1] == str2[cn]){
  65. next[i++] = ++cn;
  66. }else{
  67. if(next[cn] == -1){
  68. next[i++] = 0;
  69. }else{
  70. cn = next[cn];
  71. }
  72. }
  73. }
  74. return next;
  75. }
  76. }

4.2、加最短字符问题

问题:给定一个字符串,如何在字符串后面加最短的字符(只能在原始串的后面进行添加)使其构成一个长的字符串且包含两个原始字符串。

分析:其实需要加的字符串就是原字符串的最长前后缀子串。那么就是和 next 数组相关的问题了:

举例:abcabc —> abcabcabc 最少增加 3 个。

  • 在 KMP 中 next 数组基础上多求一位终止位,图中最后一位 x 的 next 值为 5,但是前 4 个可以复用,将缺少的 a 补上即可。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Bjd2wxMjA2_size_16_color_FFFFFF_t_70 2

  1. public class ShortestHaveTwice {
  2. public static String getAddStr(String str){
  3. if(str == null || str.length() == 0){
  4. return "";
  5. }
  6. char[] chars = str.toCharArray();
  7. if(chars.length == 1){
  8. return str + str;
  9. }
  10. if(chars.length == 2){
  11. return chars[0] == chars[1] ? (str + String.valueOf(chars[0])) : (str + str);
  12. }
  13. int endNext = getEndNextLength(chars);
  14. return str += str.substring(endNext);
  15. }
  16. public static int getEndNextLength(char[] chars){
  17. // 多求一个终止位的next值
  18. int[] next = new int[chars.length + 1];
  19. next[0] = -1;
  20. next[1] = 0;
  21. int i = 2;
  22. int cn = 0;
  23. while(i < next.length){
  24. if(chars[i-1] == chars[cn]){
  25. next[i++] = ++cn;
  26. }else{
  27. if(next[cn] == -1){
  28. next[i++] = 0;
  29. }else{
  30. cn = next[cn];
  31. }
  32. }
  33. }
  34. return next[next.length - 1];
  35. }
  36. public static void main(String[] args) {
  37. String str1 = "a";
  38. System.out.println("str1 --> " + getAddStr(str1));
  39. String str2 = "aa";
  40. System.out.println("str2 --> " + getAddStr(str2));
  41. String str3 = "ab";
  42. System.out.println("str3 --> " + getAddStr(str3));
  43. String str4 = "abcabc";
  44. System.out.println("str4 --> " + getAddStr(str4));
  45. }
  46. }

20190706155721334.png

发表评论

表情:
评论列表 (有 0 条评论,419人围观)

还没有评论,来说两句吧...

相关阅读

    相关 KMP算法

    1.原始的字符串匹配方法 算法基本思想:从主串S的第pos个字符起和模式的第一个字符比较之,若相等,则继续逐个比较后续字符;否则从主串的下一个字符起再重新和模式的字符比较

    相关 KMP算法

    核心:1.当遇到不可匹配的字符的时候,我们希望找到一些规律,可以将搜索串往后多滑动几位    2.构建《部分匹配表》 下面链接是阮大神的见解,写的非常棒!    [字符串

    相关 Kmp算法

    举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? ![bg2013050101.jpg][]

    相关 KMP算法

    KMP算法的思想是 主串S 和模式串W,判断W是否匹配S 如果主串S在i位置与W串在j位置出现不匹配的情况的时候,利用已经得到的匹配把W串尽量右移动一段距离。 用伪代码写

    相关 KMP算法

    一 KMP算法介绍 1 KMP是一个解决模式串在文本串是否出现过,如果出现过,求算出现的位置的经典算法。 2 Knuth-Morris-Pratt 字符串查找算法,简称