一、 前言
记得在我们初中、高中、四六级的时候都做过英语阅读理解,在做阅读理解的时候时常需要在一篇文章中找到关键词、短语或者句子,这就好比在一段文本中寻找特定的字符串。如果我们想要知道某个单词在一篇阅读理解中出现的次数以便于我们日后复习这个单词,我们需要怎么做呢?
二、 朴素的模式匹配算法
1、概念
朴素模式匹配算法(Naive Pattern Matching Algorithm)是一种最简单直观的字符串模式匹配方法。该算法的基本思想是从文本的第一个字符开始,依次比较模式串和文本串中的字符,逐步滑动模式串,直到找到匹配的子串或者遍历完整个文本。
2、例子
比如我们需要在一串字符串"askdfgaiusfuabcfuabcuabsui"中找到"abc",我们需要怎么做呢?
java
public class NaivePatternMatching {
public static void main(String[] args) {
String text = "askdfgaiusfuabcfuabcuabsui";
String pattern = "abc";
int position = naivePatternMatching(text, pattern);
if (position != -1) {
System.out.println("Pattern found at position: " + position);
} else {
System.out.println("Pattern not found in the text.");
}
}
private static int naivePatternMatching(String text, String pattern) {
int n = text.length();
int m = pattern.length();
for (int i = 0; i <= n - m; i++) {
int j;
for (j = 0; j < m; j++) {
if (text.charAt(i + j) != pattern.charAt(j)) {
break;
}
}
if (j == m) {
return i;
}
}
return -1;
}
}
在上述例子中我们使用的就是:朴素的模式匹配算法。
思路如下:
- 初始化变量:
获取文本串
text
和模式串pattern
的长度,分别记为n
和m
。
- 循环遍历文本:
使用外层循环从
i = 0
开始,直到i = n - m
。这是因为在剩余的文本长度小于模式长度时,无需再进行匹配。
- 内层模式匹配循环:
在每个外层循环的位置
i
,使用内层循环比较文本和模式的字符。内层循环变量
j
从 0 到m-1
,对应模式串的每个字符。如果发现不匹配的字符,跳出内层循环,继续外层循环的下一个位置。
如果一直匹配到内层循环结束,即
j == m
,表示找到完整的匹配,返回当前位置i
。
- 返回结果:
如果外层循环结束都没有找到匹配,返回 -1 表示模式未在文本中找到。
3、利弊
朴素模式匹配算法的好处:
- 简单易懂: 朴素模式匹配算法是一种非常直观和容易理解的算法。它的实现逻辑简单,不涉及复杂的数据结构和算法。
- 容易实现: 由于其简单性,朴素模式匹配算法的实现相对容易,适用于初学者学习算法和数据结构的阶段。
- 适用于短模式串: 在模式串较短的情况下,朴素模式匹配算法可能表现得比较高效,因为在短模式下,算法不会涉及过多的字符比较。
朴素模式匹配算法的坏处:
- 效率低下: 在处理大规模文本和较长模式串时,朴素模式匹配算法的效率较低。它需要在每个位置都进行完整的模式匹配,导致时间复杂度较高。
- 重复比较: 由于算法的简单性,它可能会在文本和模式之间进行大量的重复比较,尤其是在未找到匹配时,导致性能不佳。
- 不适用于大规模数据: 随着数据规模的增大,朴素模式匹配算法的性能会显著下降,不太适用于处理大型数据集。
- 无法处理模式中的通配符: 对于包含通配符等复杂模式的情况,朴素模式匹配算法无法有效处理,因为它只是简单地逐字符比较。
三、KMP模式匹配算法
1、概念
KMP(Knuth-Morris-Pratt)模式匹配算法是一种高效的字符串匹配算法,用于在一个文本串中查找是否包含一个模式串。它的主要思想是在匹配过程中充分利用已经匹配过的信息,避免不必要的字符比较,从而提高匹配的效率。
2、关键词
- 最长公共前后缀(LPS):
对于模式串中的每个位置,找到其前缀和后缀的最长共同部分。这个信息被预处理并存储在一个部分匹配表中。
- 部分匹配表(Partial Match Table):
部分匹配表是一个数组,记录了模式串中每个位置的最长公共前后缀的长度。它帮助算法在匹配过程中跳过已经匹配过的部分,避免不必要的比较。
3、基本步骤
-
构建部分匹配表:
- 对于模式串,计算并构建部分匹配表,确定每个位置的最长公共前后缀。
-
匹配过程:
- 在文本串中从左到右逐字符匹配模式串。
- 当发现不匹配时,根据部分匹配表,将模式串向右移动一定的位数,继续匹配。
4、例子
考虑文本串 "ABABCABABCABC" 和模式串 "ABABC",利用KMP算法:
-
构建部分匹配表:0,0,1,2,00,0,1,2,0
- 位置 1: "A",无前缀和后缀,长度为 0
- 位置 2: "AB",无前缀和后缀,长度为 0
- 位置 3: "ABA",前缀 "A" 和后缀 "A",长度为 1
- 位置 4: "ABAB",前缀 "AB" 和后缀 "AB",长度为 2
- 位置 5: "ABABC",无前缀和后缀,长度为 0
-
匹配过程:
- 文本串的第一个字符 "A" 与模式串的第一个字符匹配。
- 下一个字符 "B" 匹配,继续。
- "A" 不匹配,根据部分匹配表将模式串右移 1 位。
- 继续匹配,找到完整的匹配。
java
public class KMPAlgorithm {
public static void main(String[] args) {
String text = "ABABCABABCABC";
String pattern = "ABABC";
int position = kmpSearch(text, pattern);
if (position != -1) {
System.out.println("Pattern found at position: " + position);
} else {
System.out.println("Pattern not found in the text.");
}
}
private static int kmpSearch(String text, String pattern) {
int[] lps = computeLPSArray(pattern);
int n = text.length();
int m = pattern.length();
int i = 0;
int j = 0;
while (i < n) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == m) {
return i - j;
} else if (i < n && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
return -1;
}
private static int[] computeLPSArray(String pattern) {
int m = pattern.length();
int[] lps = new int[m];
int len = 0;
int i = 1;
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
}
5、利弊
KMP模式匹配算法的优势:
- 高效的匹配过程: KMP算法通过部分匹配表的预处理,避免了在模式匹配的过程中对文本串中已经匹配的部分进行不必要的重复比较,从而提高了匹配效率。
- 适用于大规模数据: 相较于朴素模式匹配算法,KMP在处理大规模文本和模式串时表现更为出色。它的时间复杂度为O(N+M),其中N为文本长度,M为模式长度。
- 减少字符比较次数: 通过利用已经匹配的信息,KMP算法在每次不匹配时能够跳过一定的字符,减少了字符比较的次数。
- 适用于通用字符串匹配问题: KMP算法的思想可以扩展应用到通用的字符串匹配问题,例如在DNA序列等领域。
KMP模式匹配算法的劣势:
- 较复杂的实现: 相对于朴素模式匹配算法,KMP算法的实现稍显复杂,涉及到构建部分匹配表和多个指针的维护。
- 额外的空间开销: KMP算法需要额外的空间来存储部分匹配表,对于一些对内存要求较高的场景可能不太适用。
- 不适用于一些特定场景: 在一些特定场景下,例如模式串较短、数据量较小的情况下,KMP算法的优势可能不如其他算法显著。
总结
一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა