首页 > 技术文章 > KMP算法——字符串模式匹配

pengsay 2021-03-21 23:04 原文

什么是KMP算法?

1. 背景
KMP算法一种改进的模式匹配算法,是D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合发表,KMP算法又称克努特-莫里斯-普拉特操作。它的改进在于:每当从某个起始位置开始一趟比较后,在匹配过程中出现失配,不回溯i,而是利用已经得到的部分匹配结果,将一种假想的位置定位“指针”在模式上向右滑动尽可能远的一段距离到某个位置后,继续按规则进行下一次的比较。
2. 算法流程 (这部分读完可能会有很多问题,没关系,我们会针对每一步进行详细的讲解)
规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。

3. KMP只需要将j值模式串中j的位置回溯到next[j]位,而免除了前面不需要的匹配,以此来换取时间。

一. 基础问题

1、什么是字符串的模式匹配?
给定两个串S=“s1s2s3 …sn”和T=“t1t2t3 …tn”,在主串S中寻找子串T的过程叫做模式匹配,T称为模式。

2、如何寻找?
我们先从比较好理解的暴力匹配(朴素模式匹配BF算法)开始,进而引出KMP算法。

. 暴力匹配(朴素模式匹配BF)

规定i是主串S的下标,j是模式T的下标。现在假设现在主串S匹配到 i 位置,模式串T匹配到 j 位置。   

如果当前字符匹配成功(即S[i] = T[j]),则i++,j++,继续匹配下一个字符;   

如果失配(即S[i] != T[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯到本次失配起始字符的下一个字符,j 回溯到0。

 1 package com.lzp.util;
 2 
 3 /**
 4  * @Author LZP
 5  * @Date 2021/3/21 22:40
 6  * @Version 1.0
 7  *
 8  * 字符串模式匹配
 9  * 暴力匹配
10  * 基本思想:用两个指针i、j去分别指向原串S与模式串P,匹配问题中只有两种情况,一种是匹配成功,另一种
11  * 是匹配不成功,如果此时i指向的原串中的字符与j指向的模式串中的字符不匹配,那么就将指针i回溯到一开始
12  * i指向的字符与j指向的字符第一次匹配成功时的下一个位置,即i = i - (j - 1),同时j也要置为0,模式
13  * 串从头开始,就一直重复这个过程,直到有其中一个指针等于其串的长度就结束该过程,最后判断此时j是否等于
14  * 模式串的长度,若等于,则表明匹配成功,反之则匹配失败。
15  */
16 public class BF {
17 
18     public static void main(String[] args) {
19         String text = "ABababaaabaBA";
20         String pattern = "ababaaaba";
21         // 2、开始模式匹配
22         System.out.println(BF(text, pattern));
23     }
24 
25     public static int BF(String text, String pattern) {
26         // 指向文本串的指针
27         int i = 0;
28         // 指向模式串的指针
29         int j = 0;
30         // 如果有一个指针等于了该串的长度,就跳出循环
31         while (i < text.length() && j < pattern.length()) {
32             if (text.charAt(i) == pattern.charAt(j)) {
33                 // 此刻两个指针指向的字符相同,两个指针同时向后移动一位
34                 i++;
35                 j++;
36             } else {
37                 i = i - (j - 1);
38                 j = 0;
39             }
40         }
41 
42         if (j == pattern.length()) {
43             return i - j;
44         } else {
45             return -1;
46         }
47     }
48 }

. KMP算法实现

  1 package com.lzp.util.kmp;
  2 
  3 import java.util.Arrays;
  4 
  5 /**
  6  * @Author LZP
  7  * @Date 2021/5/2 21:35
  8  * @Version 1.0
  9  *
 10  * ABABCABAA
 11  * 001201231
 12  * -100120123
 13  */
 14 public class KMP {
 15 
 16     /**
 17      * 返回一个指定模式串的next数组
 18      * @param pattern
 19      * @return
 20      */
 21     public static int[] prefixTable(String pattern) {
 22         char[] c = pattern.toCharArray();
 23         int n = c.length;
 24 
 25         int[] next = new int[n];
 26 
 27         // init
 28         next[0] = 0;
 29         int i = 1;
 30         // len代表当前最长公共前后缀
 31         // 我的理解:一个字符没有公共前后缀这个说法,所以为了后面计算方便,我们默认第一个字符的公共前缀和为0
 32         int len = 0;
 33         while (i < n) {
 34             if (c[len] == c[i]) {
 35                 // 相等,比较好处理
 36                 len++;
 37                 next[i] = len;
 38                 // 指针i向后移动
 39                 i++;
 40             } else {
 41                 // 这里需要判断一下,防止数组下标越界
 42                 if (len > 0) {
 43                     // 不相等,回溯
 44                     // 斜着移动
 45                     len = next[len - 1];
 46                 } else {
 47                     // 防止死循环,加一个else处理
 48                     // 当第二个字符与第一个字符不相同时,直接将第二个字符对应的next数组的值设置为0,不需要再回溯了
 49                     // 进入该else循环表示:此时指针指向第二个字符,即i = 1,且第二个字符与第一个字符不相同
 50                     next[i] = 0;
 51                     // 指针往后移动继续判断
 52                     i++;
 53                 }
 54             }
 55         }
 56 
 57         return next;
 58     }
 59 
 60     /**
 61      * kmp算法,返回模式串在主串中第一次出现位置的下标
 62      * @param src
 63      * @param pattern
 64      * @return
 65      */
 66     public static int kmp(String src, String pattern) {
 67         int[] next = prefixTable(pattern);
 68         movePrefix(next);
 69 
 70         // kmp
 71         int n = src.length();
 72         int m = pattern.length();
 73         int i = 0, j = 0;
 74         while (i < n && j < m) {
 75             if (src.charAt(i) == pattern.charAt(j)) {
 76                 i++;
 77                 j++;
 78             } else {
 79                 // 回溯模式串
 80                 j = next[j];
 81                 if (j == -1) {
 82                     // 表示此时模式串的第一个字符与当前指针i指向的字符不相等,所以此时i也需要往后移动一位
 83                     i++;
 84                     j++;
 85                 }
 86             }
 87         }
 88         if (j == m) {
 89             return i - m;
 90         }
 91         return -1;
 92     }
 93 
 94     /**
 95      * 将模式串的next数组中的所有数都后移一位(除了第一个数)
 96      * 这样一来就把原先next数组中的最后一个数给覆盖掉了
 97      * @param next
 98      */
 99     public static void movePrefix(int[] next) {
100         int n = next.length;
101         for (int i = n - 1; i > 0; i--) {
102             next[i] = next[i - 1];
103         }
104         next[0] = -1;
105     }
106 
107     public static void main(String[] args) {
108         String src = "ABABCABABDOGSDAABABCABAA";
109         String pattern = "ABABCABAA";
110         System.out.println(kmp(src, pattern));
111     }
112 }

运行结果

四. 补充

什么是最长公共前后缀?

字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。例如对于字符串 abacaba,其前缀有 a, ab, aba, abac,abaca, abacab,后缀有bacaba, acaba, caba, aba, ba, a。最长公共前后缀就是 aba。

推荐阅读