在本文中,我们将展示几种用于在大文本中搜索模式的算法。我们将使用提供的代码和简单的数学背景来描述每种算法。
请注意,提供的算法并不是在更复杂的应用程序中执行全文搜索的最佳方式。为了正确进行全文搜索,我们可以使用 Solr 或 ElasticSearch。
我们将从一种朴素的文本搜索算法开始,这是最直观的算法,有助于发现与该任务相关的其他高级问题。
在开始之前,让我们定义计算我们在 Rabin Karp 算法中使用的素数的简单方法:
public static long getBiggerPrime(int m) {
BigInteger prime = BigInteger.probablePrime(getNumberOfBits(m) + 1, new Random());
return prime.longValue();
}
private static int getNumberOfBits(int number) {
return Integer.SIZE - Integer.numberOfLeadingZeros(number);
}
一、 简单文本搜索
该算法的名称比任何其他解释都更能描述它。这是最自然的解决方案:
public static int simpleTextSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0;
while ((i + patternSize) <= textSize) {
int j = 0;
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
i += 1;
}
return -1;
}
该算法的思想很简单:遍历文本,如果模式的第一个字母匹配,则检查模式的所有字母是否都与文本匹配。
如果 m 是模式中的字母数,n 是文本中的字母数,则该算法的时间复杂度为 O(m(n-m + 1))。
最坏的情况发生在 String 具有许多部分出现的情况下:
Text: baeldunbaeldunbaeldunbaeldun
Pattern: baeldung
二、 Rabin Karp算法
如上所述,当模式很长并且模式中有很多重复元素时,简单文本搜索算法效率非常低。
Rabin Karp 算法的想法是使用哈希来查找文本中的模式。在算法开始时,我们需要计算模式的哈希值,该哈希值稍后在算法中使用。这个过程叫做指纹计算,我们可以在这里找到详细的解释。
预处理步骤的重要之处在于其时间复杂度为 O(m),通过文本迭代将需要 O(n),从而给出整个算法的时间复杂度 O(m+n)。
算法代码:
public static int RabinKarpMethod(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
long prime = getBiggerPrime(patternSize);
long r = 1;
for (int i = 0; i < patternSize - 1; i++) {
r *= 2;
r = r % prime;
}
long[] t = new long[textSize];
t[0] = 0;
long pfinger = 0;
for (int j = 0; j < patternSize; j++) {
t[0] = (2 * t[0] + text[j]) % prime;
pfinger = (2 * pfinger + pattern[j]) % prime;
}
int i = 0;
boolean passed = false;
int diff = textSize - patternSize;
for (i = 0; i <= diff; i++) {
if (t[i] == pfinger) {
passed = true;
for (int k = 0; k < patternSize; k++) {
if (text[i + k] != pattern[k]) {
passed = false;
break;
}
}
if (passed) {
return i;
}
}
if (i < diff) {
long value = 2 * (t[i] - r * text[i]) + text[i + patternSize];
t[i + 1] = ((value % prime) + prime) % prime;
}
}
return -1;
}
在最坏情况下,该算法的时间复杂度为 O(m(n-m+1)))。然而,平均而言,该算法具有 O(n+m) 的时间复杂度。
此外,该算法还有蒙特卡洛版本,速度更快,但可能会导致错误匹配(误报)。
三、Knuth-Morris-Pratt算法
在简单文本搜索算法中,我们看到如果文本中有许多部分与模式匹配,算法可能会很慢。
Knuth-Morris-Pratt 算法的思想是移位表的计算,它为我们提供了应该在其中搜索候选模式的信息。
KMP 算法的 Java 实现:
public static int KnuthMorrisPrattSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
int[] shift = KnuthMorrisPrattShift(pattern);
while ((i + patternSize) <= textSize) {
while (text[i + j] == pattern[j]) {
j += 1;
if (j >= patternSize)
return i;
}
if (j > 0) {
i += shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i++;
j = 0;
}
}
return -1;
}
以下是我们如何计算班次表:
public static int[] KnuthMorrisPrattShift(char[] pattern) {
int patternSize = pattern.length;
int[] shift = new int[patternSize];
shift[0] = 1;
int i = 1, j = 0;
while ((i + j) < patternSize) {
if (pattern[i + j] == pattern[j]) {
shift[i + j] = i;
j++;
} else {
if (j == 0)
shift[i] = i + 1;
if (j > 0) {
i = i + shift[j - 1];
j = Math.max(j - shift[j - 1], 0);
} else {
i = i + 1;
j = 0;
}
}
}
return shift;
}
该算法的时间复杂度也为O(m+n)。
四、 简单的Boyer-Moore算法
两位科学家Boyer和Moore提出了另一个想法。为什么不将模式与从右到左而不是从左到右的文本进行比较,同时保持移位方向相同:
public static int BoyerMooreHorspoolSimpleSearch(char[] pattern, char[] text) {
int patternSize = pattern.length;
int textSize = text.length;
int i = 0, j = 0;
while ((i + patternSize) <= textSize) {
j = patternSize - 1;
while (text[i + j] == pattern[j]) {
j--;
if (j < 0)
return i;
}
i++;
}
return -1;
}
正如预期的那样,这将在 O(m * n) 时间内运行。但是这种算法导致了发生和匹配启发式的实现,这大大加快了算法的速度。我们可以在这里找到更多。
五、 Boyer-Moore-Horspool算法
Boyer-Moore 算法的启发式实现有许多变体,最简单的一种是 Horspool 变体。
这个版本的算法被称为Boyer-Moore-Horspool,这个变体解决了负偏移的问题(我们可以在Boyer-Moore算法的描述中读到负偏移问题)。
与 Boyer-Moore 算法一样,最坏情况的时间复杂度为 O(m * n),而平均复杂度为 O(n)。空间使用不取决于模式的大小,而只取决于字母的大小,即 256,因为这是英文字母中 ASCII 字符的最大值:
public static int BoyerMooreHorspoolSearch(char[] pattern, char[] text) {
int shift[] = new int[256];
for (int k = 0; k < 256; k++) {
shift[k] = pattern.length;
}
for (int k = 0; k < pattern.length - 1; k++){
shift[pattern[k]] = pattern.length - 1 - k;
}
int i = 0, j = 0;
while ((i + pattern.length) <= text.length) {
j = pattern.length - 1;
while (text[i + j] == pattern[j]) {
j -= 1;
if (j < 0)
return i;
}
i = i + shift[text[i + pattern.length - 1]];
}
return -1;
}
在本文中,我们介绍了几种用于文本搜索的算法。由于几种算法需要更强的数学背景,因此我们试图表示每种算法背后的主要思想,并以简单的方式提供它。