diff --git "a/\345\255\246\344\271\240\347\233\270\345\205\263/\345\260\261\344\270\232/\350\256\241\347\256\227\346\234\272\345\270\270\350\200\203\345\205\253\350\202\241\346\226\207\346\200\273\347\273\223/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\255\227\347\254\246\344\270\262/KMP\347\256\227\346\263\225/index.md" "b/\345\255\246\344\271\240\347\233\270\345\205\263/\345\260\261\344\270\232/\350\256\241\347\256\227\346\234\272\345\270\270\350\200\203\345\205\253\350\202\241\346\226\207\346\200\273\347\273\223/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\255\227\347\254\246\344\270\262/KMP\347\256\227\346\263\225/index.md" new file mode 100644 index 0000000..14659be --- /dev/null +++ "b/\345\255\246\344\271\240\347\233\270\345\205\263/\345\260\261\344\270\232/\350\256\241\347\256\227\346\234\272\345\270\270\350\200\203\345\205\253\350\202\241\346\226\207\346\200\273\347\273\223/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\255\227\347\254\246\344\270\262/KMP\347\256\227\346\263\225/index.md" @@ -0,0 +1,155 @@ +**本模块是公共类课程,适合绝大部分计算机岗位。由于 Java 受众最广且易读性强,本模块示例代码使用 Java 编写,其他语言也可参考学习。** + +## 什么是 KMP 算法? + +KMP 算法是一种字符串匹配算法,用于在一个主串中查找一个模式串的出现位置。KMP 算法的核心思想是利用已匹配的信息,尽量减少不必要的匹配,提高匹配效率。 + +KMP 算法的主要步骤如下: + +1. 构建模式串的最长前缀后缀数组(Next 数组)。 +2. 在主串中查找模式串。 + +KMP 算法的时间复杂度为 O(m+n),其中 m 为主串长度,n 为模式串长度。 + +## KMP 算法的实现 + +KMP 算法的实现主要包括两个步骤:构建模式串的最长前缀后缀数组和在主串中查找模式串。 + +假设现在有一个主串 text 和一个模式串 pattern,主串的内容为"ABABACB",模式串的内容为"ABAC",现在要在主串中查找模式串的位置。 + +### 第一步,开始构建 Next 数组 + +此时不妨列出如下表格: + +| pattern | A | B | A | C | +| ------------ | --- | --- | --- | --- | +| index | 0 | 1 | 2 | 3 | +| cursor(i, j) | j | i | | | +| next | 0 | | | | + +指定两个指针 i 和 j,i 初始化为 1,j 初始化为 0,next[0] = 0。 + +### 第二步,开始比较 pattern[i]和 pattern[j],直到遍历完整个模式串 + +如果相等,则 next[i] = j + 1,i 和 j 分别加 1;如果不相等,则 j 回溯到 next[j-1]的位置,如果 j 为 0,则 next[i] = 0,i++。直到遍历完整个模式串。 + +比如,此时 pattern[1] = 'B',pattern[0] = 'A',不相等,由于 j = 0,所以直接设置 next[1] = 0,i++。 + +| pattern | A | B | A | C | +| ------------ | --- | --- | --- | --- | +| index | 0 | 1 | 2 | 3 | +| cursor(i, j) | j | | i | | +| next | 0 | 0 | | | + +接下来,pattern[2] = 'A',pattern[0] = 'A',相等,next[2] = 1,i++,j++。 + +| pattern | A | B | A | C | +| ------------ | --- | --- | --- | --- | +| index | 0 | 1 | 2 | 3 | +| cursor(i, j) | | j | | i | +| next | 0 | 0 | 1 | | + +最后,pattern[3] = 'C',pattern[1] = 'B',不相等,j 回溯到 next[j-1]的位置,即 j = 0,next[3] = 0,i++。此时模式串的最长前缀后缀数组为 next = [0, 0, 1, 0]。 + +| pattern | A | B | A | C | +| ------------ | --- | --- | --- | --- | +| index | 0 | 1 | 2 | 3 | +| cursor(i, j) | j | | | i | +| next | 0 | 0 | 1 | 0 | + +### 第三步,开始在主串中查找模式串 + +指定两个指针 i 和 j,i 和 j 初始化为 0。 + +先列出如下表格: + +| text | A | B | A | B | A | C | B | +| ------------ | --- | --- | --- | --- | --- | --- | --- | +| textIndex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| i | i | | | | | | | +| pattern | A | B | A | C | | | | +| patternIndex | 0 | 1 | 2 | 3 | | | | +| j | j | | | | | | | +| next | 0 | 0 | 1 | 0 | | | | + +开始比较 text[i]和 pattern[j],如果相等,则 i 和 j 分别加 1;如果不相等,则 j 回溯到 next[j-1]的位置,如果 j 为 0,则 i++。直到遍历完整个主串。 + +比如,直到 i 和 j 都为 2 时,text[2] = 'A',pattern[2] = 'A',相等,i++,j++。 + +| text | A | B | A | B | A | C | B | +| ------------ | --- | --- | --- | --- | --- | --- | --- | +| textIndex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| i | | | | i | | | | +| pattern | A | B | A | C | | | | +| patternIndex | 0 | 1 | 2 | 3 | | | | +| j | | | | j | | | | +| next | 0 | 0 | 1 | 0 | | | | + +此时,i 和 j 都为 3,text[3] = 'B',pattern[3] = 'C',不相等,j 回溯到 next[j-1]的位置,即 j = 1.因为 j != 0,所以这里 i 不变。由于 j 向左发生了 2 格的偏移,这里不妨把 pattern 及以下的表格向右移动 2 格。 + +| text | A | B | A | B | A | C | B | +| ------------ | --- | --- | --- | --- | --- | --- | --- | +| textIndex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| i | | | | i | | | | +| pattern | | | A | B | A | C | | +| patternIndex | | | 0 | 1 | 2 | 3 | | +| j | | | | j | | | | +| next | | | 0 | 0 | 1 | 0 | | + +接下来,由于 j 直接指向了字符'B',免去了从'A'开始的比较,直接比较 text[3] = 'B'和 pattern[1] = 'B',相等,i++,j++,直到遍历完模式串,返回匹配的位置`i - j`。 + +| text | A | B | A | B | A | C | B | +| ------------ | --- | --- | --- | --- | --- | --- | --- | +| textIndex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | +| i | | | | | | | i | +| pattern | | | A | B | A | C | | +| patternIndex | | | 0 | 1 | 2 | 3 | 4 | +| j | | | | | | | j | +| next | | | 0 | 0 | 1 | 0 | | + +最终,模式串在主串中的索引为`i - j = 6 - 4 = 2`。 + +## KMP 算法的代码实现 + +例题:给定一个主串 text 和一个模式串 pattern,返回模式串在主串中的索引,如果不存在则返回 -1。*(手写`String.indexOf()`方法)* + +```java +// 计算模式串的最长前缀后缀 +public int[] getNext(String pattern) { + int n = pattern.length(); + // 这里不需要 next[0] = 0,因为 int 数组默认值为 0,如果是 js 等弱类型语言则需要手动初始化为 0 + int[] next = new int[n]; + int i = 1, j = 0; + while (i < n) { + if (pattern.charAt(i) == pattern.charAt(j)) + next[i++] = ++j; + else if (j > 0) + j = next[j - 1]; + else + next[i++] = 0; + } + return next; +} + +// KMP 算法,返回模式串在主串中的索引 +public int kmp(String text, String pattern) { + int m = text.length(), n = pattern.length(); + // 主串长度不可以小于模式串长度 + if (m < n) return -1; + // 空串默认视为在主串的起始位置 + if (n == 0) return 0; + + int[] next = getNext(pattern); + int i = 0, j = 0; + while (i < m && j < n) { + if (text.charAt(i) == pattern.charAt(j)) { + i++; + j++; + } else if (j > 0) + j = next[j - 1]; + else + i++; + } + return j == n ? i - j : -1; +} +``` \ No newline at end of file