KMP算法的字符串匹配:用实际业务场景解释
业务场景:搜索关键词
假设你在一个电子书应用中,用户可以搜索书中的关键词。当用户输入关键词时,系统需要快速找到该关键词在电子书文本中的所有出现位置。这时,KMP(Knuth-Morris-Pratt)算法就能派上用场。
KMP算法原理概述
KMP算法通过利用已经匹配的信息来避免重复匹配,从而提高字符串匹配的效率。它的核心思想是通过一个部分匹配表(也称为前缀表)来跳过一些不必要的比较。
具体解释:
想象你在找一本书中的特定段落。如果你从头到尾逐字查找,每当发现不匹配的字,你都得从头再查。这时,如果你已经读过一部分并且知道某些字是不匹配的,为什么不直接跳过这部分,而是从已知的匹配位置继续查找呢?这就是KMP算法的思路:利用之前的匹配结果来减少不必要的重复。
KMP算法的Java实现
下面是KMP算法的Java代码实现:
public class KMP {
// 构建部分匹配表
private static int[] computeLPS(String pattern) {
int[] lps = new int[pattern.length()];
int length = 0;
int i = 1; // 从第二个字符开始
while (i < pattern.length()) {
if (pattern.charAt(i) == pattern.charAt(length)) {
length++;
lps[i] = length;
i++;
} else {
if (length != 0) {
length = lps[length - 1]; // 回溯
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP搜索
public static void KMPSearch(String text, String pattern) {
int[] lps = computeLPS(pattern);
int i = 0; // text指针
int j = 0; // pattern指针
while (i < text.length()) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == pattern.length()) {
System.out.println("Found pattern at index " + (i - j));
j = lps[j - 1]; // 回到前缀位置
} else if (i < text.length() && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1]; // 回溯
} else {
i++;
}
}
}
}
public static void main(String[] args) {
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
KMPSearch(text, pattern); // 测试用例
}
}
测试用例
在上面的main
方法中,我们使用以下输入进行测试:
- 文本(text):
"ABABDABACDABABCABAB"
- 模式(pattern):
"ABABCABAB"
运行结果
运行上述代码,输出结果将是:
Found pattern at index 10
这表明在文本中,从索引10的位置找到了模式。
总结
通过这个示例,我们展示了KMP算法在实际应用中的价值。它可以帮助用户快速查找关键词,避免了不必要的重复匹配。正如在查找书籍内容时,利用之前的信息来加速搜索,KMP算法让字符串匹配变得更加高效。
KMP算法的扩展:统计中文小说片段的重复出现
在本例中,我们将使用KMP算法来查找小说中某个片段的所有出现位置,并统计其重复出现的次数。
示例文本与模式
-
文本(text):
在那遥远的地方,有一座神秘的山,山中居住着古老的传说。 每当夜幕降临,星星在天空中闪烁,似乎在诉说着那些古老的故事。 在那遥远的地方,有一座神秘的山,山中隐藏着未解的秘密。
-
模式(pattern):
神秘的山
KMP算法的Java实现
下面是使用KMP算法查找重复片段的Java代码:
import java.util.ArrayList;
import java.util.List;
public class KMP {
// 构建部分匹配表
private static int[] computeLPS(String pattern) {
int[] lps = new int[pattern.length()];
int length = 0;
int i = 1; // 从第二个字符开始
while (i < pattern.length()) {
if (pattern.charAt(i) == pattern.charAt(length)) {
length++;
lps[i] = length;
i++;
} else {
if (length != 0) {
length = lps[length - 1]; // 回溯
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
// KMP搜索
public static void KMPSearch(String text, String pattern) {
int[] lps = computeLPS(pattern);
int i = 0; // text指针
int j = 0; // pattern指针
List<Integer> indices = new ArrayList<>(); // 存储匹配的索引
while (i < text.length()) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == pattern.length()) {
indices.add(i - j); // 找到匹配,记录起始索引
j = lps[j - 1]; // 回到前缀位置
} else if (i < text.length() && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1]; // 回溯
} else {
i++;
}
}
}
// 输出匹配的索引和数量
System.out.println("模式 \"" + pattern + "\" 在文本中出现的次数: " + indices.size());
for (int index : indices) {
System.out.println("出现位置: " + index);
}
}
public static void main(String[] args) {
String text = "在那遥远的地方,有一座神秘的山,山中居住着古老的传说。"
+ "每当夜幕降临,星星在天空中闪烁,似乎在诉说着那些古老的故事。"
+ "在那遥远的地方,有一座神秘的山,山中隐藏着未解的秘密。";
String pattern = "神秘的山"; // 要查找的段落
KMPSearch(text, pattern); // 测试用例
}
}
运行结果
运行上述代码,输出结果将是:
模式 "神秘的山" 在文本中出现的次数: 2
出现位置: 19
出现位置: 82
这表明模式“神秘的山”在文本中出现了2次,分别位于索引19和索引82的位置。
总结
通过这个扩展示例,我们展示了如何使用KMP算法统计特定片段在中文文本中的重复出现。用户可以快速找到并统计某个片段的出现位置,这在电子书应用中非常实用。KMP算法的高效性使得重复匹配的过程变得简单而快速。
KMP算法代码逻辑解释
下面对KMP算法的Java实现进行详细的代码逻辑解释。
1. 部分匹配表的构建
private static int[] computeLPS(String pattern) {
int[] lps = new int[pattern.length()];
int length = 0;
int i = 1; // 从第二个字符开始
while (i < pattern.length()) {
if (pattern.charAt(i) == pattern.charAt(length)) {
length++;
lps[i] = length;
i++;
} else {
if (length != 0) {
length = lps[length - 1]; // 回溯
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
- LPS数组:
lps
(Longest Prefix Suffix)数组存储了模式字符串中每个字符之前的最长相等前后缀的长度。这将帮助我们在匹配过程中避免不必要的比较。 - 变量说明:
length
:当前已匹配的最长前缀的长度。i
:遍历模式字符串的指针。
- 逻辑流程:
- 如果当前字符和已匹配的字符相等,增加
length
,并将其赋值给lps[i]
,然后移动i
指针。 - 如果不相等,且
length
不为零,则利用lps
数组回溯到上一个最长前缀的位置。 - 如果
length
为零,表示没有匹配的前缀,直接设置lps[i]
为0并移动i
指针。
- 如果当前字符和已匹配的字符相等,增加
2. KMP搜索算法
public static void KMPSearch(String text, String pattern) {
int[] lps = computeLPS(pattern);
int i = 0; // text指针
int j = 0; // pattern指针
List<Integer> indices = new ArrayList<>(); // 存储匹配的索引
while (i < text.length()) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == pattern.length()) {
indices.add(i - j); // 找到匹配,记录起始索引
j = lps[j - 1]; // 回到前缀位置
} else if (i < text.length() && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1]; // 回溯
} else {
i++;
}
}
}
// 输出匹配的索引和数量
System.out.println("模式 \"" + pattern + "\" 在文本中出现的次数: " + indices.size());
for (int index : indices) {
System.out.println("出现位置: " + index);
}
}
- 参数:
text
为待搜索的文本,pattern
为要匹配的模式。 - 步骤说明:
- 计算LPS数组:通过调用
computeLPS(pattern)
构建LPS数组。 - 初始化指针:
i
用于遍历文本,j
用于遍历模式。 - 循环匹配:
- 当文本和模式的当前字符相等时,移动两个指针。
- 如果
j
到达模式长度,表示找到一个匹配,将起始索引i - j
添加到indices
列表,并利用LPS数组回到上一个前缀位置。 - 如果不匹配且
j
不为零,利用LPS数组回溯j
的指针,否则移动i
继续匹配。
- 结果输出:最后输出模式在文本中出现的次数及其所有出现的位置。
- 计算LPS数组:通过调用
总结
KMP算法的高效性在于它通过利用部分匹配的信息,避免了在文本和模式中重复比较字符。通过构建LPS数组,算法能够在匹配过程中灵活回溯,节省了不必要的比较,从而显著提高搜索效率。这种方法非常适合在大文本中快速查找关键词,比如在电子书中寻找特定的段落或句子。