KMP算法
KMP算法的经典leetcode题目28. 找出字符串中第一个匹配项的下标
KMP算法主要是用来进行字符串匹配的,它的目标题目就是这种,一个一个字符进行匹配,当发现不匹配的时候,可以利用之前匹配过的字符。而不是从头开始。
例如:文本串:aabaabaaf 模式串:aabaaf 我们要找模式串第一次出现在文本串时,文本串的索引下标。
如果我们用暴力法,则需要两层循环,当文本串比较到索引为5的位置时(即b和f进行比较)。
索引: 0 1 2 3 4 5 6 7 8
文本串:a a b a a b a a f
模式串:a a b a a f
发现不相等,则最外层循环需要从头开始,即从文本串下表为 1的位置,再次和模式串从头开始判断,这次在索引为2的地方就不等(b和a不相等),再次开启新一轮的循环。
索引: 0 1 2 3 4 5 6 7 8
文本串:a a b a a b a a f
模式串: a a b a a f
此时的时间复杂度为O(m*n),故考虑时间复杂度为O(m+n)
在了解KMP算法之前要先熟悉几个概念
-
前缀
-
后缀
-
最长相同前后缀
-
前缀表
前缀
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
比如上例中的模式串aabaaf,不包含最后一个字符的所有以第一个开头的连续字符串即为:a aa aab aaba aabaa。这些都是模式串的前缀
后缀
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
比如上例中的模式串aabaaf,不包含第一个字符的所有以最后一个字符结尾的连续子串为:f af aaf baaf abaaf。这些都是模式串的后缀
最长相同前后缀
有的地方也叫最长公共前后缀,是指字符串所拥有的最长的且相同的前缀和后缀。
比如上例中的模式串aabaaf,它的前缀和后缀,没有一个是相等的,所以它的最长相同前后缀为空。但是如果删掉最后的f,变成aabaa的话
前缀:a aa aab aaba
后缀:a aa baa abba
此时,最长相同前后缀就为aa了
前缀表
以数组的形式(一般命名为next),记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。这句话比较拗口,我们还拿上面的模式串举例子
索引: 0 1 2 3 4 5
模式串:a a b a a f
在讲解最长相同前后缀的时候,我们分别举了aabaaf和aabaa的最长相同前后缀为空和aa。则下标为5和4的数组值为next[5] = 0
,next[4] = 2
。
而下表为3之前的字符串为aaba。它的最大相同前后缀为a,所以next[3] = 1
以此类推,得到所有的next数组的值
索引: 0 1 2 3 4 5
模式串:a a b a a f
前缀表:0 1 0 1 2 0
KMP思路
而KMP的思路的核心就是前缀表,因为前缀表里装的是最大相同前后缀长度,所以当发现匹配的字符不相等的时候,可以直接从next数组里找到最大相等前后缀的长度,直接从最大相等前后缀的长度后面一位进行比较。
我们还是拿上例做演示,而且模式串的前缀表我们也已经算出来了
索引: 0 1 2 3 4 5 6 7 8
文本串:a a b a a b a a f
模式串:a a b a a f
前缀表:0 1 0 1 2 0
用i和j做文本串和模式串的指针索引。i为文本串当前的索引,j为模式串当前的索引
比较到索引为5(i=5,j=5)的地方的时候,发现b和f不相等,此时我们不用像暴力法一样直接从头开始。
-
先看索引5的前一位的前缀表的值:索引4的前缀表的值。
next[j-1] = next[4] = 2
。 -
再看索引为2(
j = next[j-1]
即j = 2
)的位置上的模式串里的值(aabaaf)和当前文本串里的值(aabaabaaf)是否相等索引: 0 1 2 3 4 5 6 7 8
文本串:a a b a a b a a f
模式串:a a b a a f
前缀表:0 1 0 1 2 0
此例中是相等的,所以继续进行匹配比较,如果依旧不相等。则再次找模式串中j = next[j-1]的位置,看文本串[i]和匹配串[j]的值是否相等。直到找到相等的或者回溯到模式串[0]。
-
直到模式串的指针指向最后时的字符依旧和文本串的指针指向的字符相等,则结束。否则说明文本串中没有匹配的模式串。
索引: 0 1 2 3 4 5 6 7 8
i
文本串:a a b a a b a a f
模式串:a a b a a f
j
前缀表:0 1 0 1 2 0
代码
class Solution { public int strStr(String haystack, String needle) { int len = needle.length(); if (len == 0) { return len; } int[] next = new int[len]; getNext(next, needle); int j = 0; for (int i = 0; i < haystack.length(); i++) { while (j > 0 && haystack.charAt(i) != needle.charAt(j)) { j = next[j - 1]; } if (haystack.charAt(i) == needle.charAt(j)) { j++; } if (j == needle.length()) { return i - needle.length() + 1; } } return -1; } public void getNext(int[] next, String s) { //初始化 int j = 0; next[0] = j; //循环判断最长相等前后缀长度,填满next数组 for (int i = 1; i < s.length(); i++) { //当不等时 while (j > 0 && s.charAt(i) != s.charAt(j)) {//不一定j的位置回溯到next[j-1]后next[j]和next[i]就相等,所以要用循环 j = next[j - 1]; } if (s.charAt(i) == s.charAt(j)) { j++; } next[i] = j; } } }
进阶题目
本题的核心还是KMP算法,只不过中间有一点逻辑上的判断。即:在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串
例如:abab
前缀:a ab aba
后缀:b ab bab
最长相等先后缀:ab
所以重复子串为:abab - ab = ab;
代码
class Solution { public boolean repeatedSubstringPattern(String s) { //KMP算法 //构建next数组 int len = s.length(); int[] next = new int[len]; int j = 0; for (int i = 1; i < len; i++) { //不等 while (j > 0 && s.charAt(i) != s.charAt(j)) { j = next[j-1]; } //相等 if (s.charAt(i) == s.charAt(j)) { j++; } next[i] = j; } //判断是否能由子串重复组成 while (next[len-1] > 0 && len % (len-next[len-1]) == 0) { return true; } return false; } }
参考资料: