算法训练营(day09)

KMP算法理论基础

  • 什么是KMP

    由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP

  • KMP有什么用

    • KMP主要应用在字符串匹配上

    • KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了

  • 什么是前缀表

    KMP中定义的 next数组 就是前缀表

    前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配

    所以前缀表就是:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀 的数组

  • 为什么一定要用前缀表

    KMP详解1

    如动画所示,文本串中第六个字符 b 和 模式串的第六个字符 f 不匹配。如果暴力匹配,发现不匹配,此时就要从头匹配。但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符 b 继续开始匹配,提高了查询效率

  • 如何计算前缀表

    计算前缀表之前,需要引入一个概念: 最长公共前后缀

    • 前缀是指 不包含最后一个字符的所有以第一个字符开头的连续子串
    • 后缀是指 不包含第一个字符的所有以最后一个字符结尾的连续子串

    前缀表要求的就是 相同前后缀的长度

    具体的前缀表计算可以看 帮你把KMP算法学个通透!B站(理论篇)

  • 前缀表与next数组

    next数组就是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。这并不涉及到KMP的原理,而是具体实现

  • 使用next数组来匹配

    以下我们以前缀表统一减一之后的next数组来做演示。有了next数组,就可以根据next数组来 匹配文本串 s,和模式串 t 了。

    注意next数组是前缀表(旧前缀表统一减一)。匹配过程动画如下:

    KMP精讲4

  • 时间复杂度分析

    其中 n 为文本串长度,m 为模式串长度

    因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是 O(n) ,之前还要单独生成next数组,时间复杂度是 O(m)

    • KMP算法的时间复杂度是 O(n+m)

    • 暴力解法显而易见是 O(n × m)

    所以KMP在字符串匹配中极大地提高了搜索的效率。

  • 构造next数组

    定义一个函数 getNext 来构建next数组,函数参数为 指向next数组的指针 和 一个字符串

    方式一:前缀表统一 -1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void getNext(int[] next, String needle){
    int j = -1;
    next[0] = j;
    for(int i = 1; i < needle.length(); i++){
    while(j >= 0 && needle.charAt(j + 1) != needle.charAt(i)){
    j = next[j];
    }
    if(needle.charAt(j + 1) == needle.charAt(i)){
    j++;
    }
    next[i] = j;
    }
    }

    方式二:单纯前缀表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void getNext(int[] next, String needle){
    int j = 0;
    next[0] = j;
    for(int i = 1; i < needle.length(); i++){
    while(j > 0 && needle.charAt(j) != needle.charAt(i)){
    j = next[j - 1];
    }
    if(needle.charAt(j) == needle.charAt(i)){
    j++;
    }
    next[i] = j;
    }
    }
  • 使用next数组来做匹配

28.找出字符串中第一个匹配项的下标

题目链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

解题思路

解题过程:

  1. 本题可使用暴力解法逐个字符遍历查询
  2. 亦可使用 KMP算法 进行查询

详细代码

解法一:暴力解法

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
class Solution {
public int strStr(String haystack, String needle) {
if (needle == null || needle.length() == 0){
return 0;
}
int hlen = haystack.length();
int nlen = needle.length();

if (hlen < nlen){
return -1;
}

char[] ch = haystack.toCharArray();
char[] cn = needle.toCharArray();
for (int i = 0; i <= hlen - nlen; i++){
int h = i, n = 0;
while (n < nlen && ch[h] == cn[n]){
h++;
n++;
}
if (n == nlen){
return i;
}
}
return -1;
}
}

解法二:KMP算法(单纯前缀表)

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
class Solution {
public int strStr(String haystack, String needle) {
if(needle.length() == 0){
return 0;
}
int[] next = new int[needle.length()];
getNext(next, needle);

int j = 0;
for(int i = 0; i < haystack.length(); i++){
while(j > 0 && needle.charAt(j) != haystack.charAt(i)){
j = next[j - 1];
}
if(needle.charAt(j) == haystack.charAt(i)){
j++;
}
if(j == needle.length()){
return i - needle.length() + 1;
}
}
return -1;
}

public void getNext(int[] next, String needle){
int j = 0;
next[0] = j;
for(int i = 1; i < needle.length(); i++){
while(j > 0 && needle.charAt(j) != needle.charAt(i)){
j = next[j - 1];
}
if(needle.charAt(j) == needle.charAt(i)){
j++;
}
next[i] = j;
}
}
}

解法三:前缀表统一 减一

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
class Solution {
public int strStr(String haystack, String needle) {
if(needle.length() == 0){
return 0;
}
int[] next = new int[needle.length()];
getNext(next, needle);

int j = -1;
for(int i = 0; i < haystack.length(); i++){
while(j >= 0 && needle.charAt(j + 1) != haystack.charAt(i)){
j = next[j];
}
if(needle.charAt(j + 1) == haystack.charAt(i)){
j++;
}
if(j == needle.length() - 1){
return i - needle.length() + 1;
}
}
return -1;
}

public void getNext(int[] next, String needle){
int j = -1;
next[0] = j;
for(int i = 1; i < needle.length(); i++){
while(j >= 0 && needle.charAt(j + 1) != needle.charAt(i)){
j = next[j];
}
if(needle.charAt(j + 1) == needle.charAt(i)){
j++;
}
next[i] = j;
}
}
}

459.重复的子字符串

题目链接:https://leetcode.cn/problems/repeated-substring-pattern/description/

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

解题思路

解题过程:

  1. 本题可使用库函数 substring() 暴力判断
  2. 本题亦可使用 KMP算法 进行判断

详细代码

解法一:库函数暴力解法

1
2
3
4
5
6
class Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
return str.substring(1, str.length()- 1).contains(s);
}
}

解法二:KMP算法

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
class Solution {
public boolean repeatedSubstringPattern(String s) {
if (s.equals("")) return false;

int len = s.length();
// 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
s = " " + s;
char[] chars = s.toCharArray();
int[] next = new int[len + 1];

// 构造 next 数组过程,j从0开始(空格),i从2开始
for (int i = 2, j = 0; i <= len; i++) {
// 匹配不成功,j回到前一位置 next 数组所对应的值
while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
// 匹配成功,j往后移
if (chars[i] == chars[j + 1]) j++;
// 更新 next 数组的值
next[i] = j;
}

// 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值
if (next[len] > 0 && len % (len - next[len]) == 0) {
return true;
}
return false;
}
}

解法三:KMP算法

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
class Solution {
public boolean repeatedSubstringPattern(String s) {
if(s.equals("")){
return false;
}
int len = s.length();
s = " " + s;
char[] cs = s.toCharArray();
int[] next = new int[len + 1];

for(int i = 2, j = 0; i <= len; i++){
while(j > 0 && cs[i] != cs[j + 1]){
j = next[j];
}
if(cs[i] == cs[j + 1]){
j++;
}
next[i] = j;
}

if(next[len] > 0 && len % (len - next[len]) == 0){
return true;
}
return false;
}
}