Алгоритмы поиска строк для больших текстов

Алгоритмы поиска строк для больших текстов

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);
}

Название этого алгоритма описывает его лучше, чем любое другое объяснение. Это наиболее естественное решение:

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.