Алгоритмы поиска строк для больших текстов
1. Вступление
В этой статье мы покажем несколько алгоритмов поиска шаблона в большом тексте. Мы опишем каждый алгоритм с предоставленным кодом и простой математической базой.
Обратите внимание, что предоставленные алгоритмы - не лучший способ выполнить полнотекстовый поиск в более сложных приложениях. Чтобы правильно выполнять полнотекстовый поиск, мы можем использоватьSolr илиElasticSearch.
2. Алгоритмы
Мы начнем с наивного алгоритма текстового поиска, который является наиболее интуитивным и помогает обнаружить другие сложные проблемы, связанные с этой задачей.
2.1. Вспомогательные методы
Прежде чем мы начнем, давайте определим простые методы вычисления простых чисел, которые мы используем в алгоритме Рабина Карпа:
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);
}
2.2. Простой текстовый поиск
Название этого алгоритма описывает его лучше, чем любое другое объяснение. Это наиболее естественное решение:
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: example
2.3. Алгоритм Рабина Карпа
Как упомянуто выше, алгоритм простого текстового поиска очень неэффективен, когда шаблоны длинные и когда в шаблоне много повторяющихся элементов.
Идея алгоритма Рабина Карпа заключается в использовании хеширования для поиска шаблона в тексте. В начале алгоритма нам нужно вычислить хэш шаблона, который позже будет использован в алгоритме. Этот процесс называется вычислением отпечатка пальца, и мы можем найти подробное объяснениеhere.
Важным моментом в этапе предварительной обработки является то, что его временная сложность составляет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).
Кроме того, существует версия алгоритма Монте-Карло, которая работает быстрее, но может привести к неправильным совпадениям (ложным срабатываниям).
2.4 Knuth-Morris-Pratt Algorithm
В алгоритме простого текстового поиска мы увидели, как алгоритм может быть медленным, если есть много частей текста, которые соответствуют шаблону.
Идея алгоритма Кнута-Морриса-Пратта заключается в расчете таблицы сдвигов, которая предоставляет нам информацию, в которой мы должны искать кандидатов-паттернов. Мы можем узнать больше о таблице сдвигаhere.
Java-реализация алгоритма KMP:
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).
2.5. Простой алгоритм Бойера-Мура
Два ученых, Бойер и Мур, пришли с другой идеей. Почему бы не сравнить шаблон с текстом справа налево, а не слева направо, сохраняя при этом направление смещения:
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) времени. Но этот алгоритм привел к реализации вхождения и эвристики соответствия, что значительно ускоряет алгоритм. Мы можем найти ещеhere.
2.6. Алгоритм Бойера-Мура-Хорспула
Существует много вариантов эвристической реализации алгоритма Бойера-Мура, и самым простым из них является вариант Хорсула.
Эта версия алгоритма называется Бойер-Мур-Хорспул, и эта вариация решила проблему отрицательных сдвигов (о проблеме отрицательных сдвигов можно прочитать в описании алгоритма Бойера-Мура).
Как и алгоритм Бойера-Мура, временная сложность наихудшего сценария составляет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;
}
4. Заключение
В этой статье мы представили несколько алгоритмов для поиска текста. Поскольку несколько алгоритмов требуют более сильного математического фона, мы постарались представить основную идею под каждым алгоритмом и представить ее в простой форме.
И, как всегда, исходный код можно найтиover on GitHub.