Функция Java indexOf более эффективна, чем Rabin-Karp? Эффективность поиска в тексте

Несколько недель назад я задал вопрос Stackoverflow о создании эффективного алгоритма для поиска шаблона в большом фрагменте текста. Прямо сейчас я использую функцию String indexOf для выполнения поиска. Одно из предложений заключалось в том, чтобы использовать Рабина-Карпа в качестве альтернативы. Я написал небольшую тестовую программу следующим образом, чтобы протестировать реализацию Rabin-Karp следующим образом.

public static void main(String[] args) {
    String test = "Mary had a little lamb whose fleece was white as snow";

    String p = "was";
     long start  = Calendar.getInstance().getTimeInMillis();
     for (int x = 0; x < 200000; x++)
         test.indexOf(p);
     long end = Calendar.getInstance().getTimeInMillis();
     end = end -start;
     System.out.println("Standard Java Time->"+end);

    RabinKarp searcher = new RabinKarp("was");
    start  = Calendar.getInstance().getTimeInMillis();
    for (int x = 0; x < 200000; x++)
    searcher.search(test);
    end = Calendar.getInstance().getTimeInMillis();
    end = end -start;
    System.out.println("Rabin Karp time->"+end);

}

И вот реализация Rabin-Karp, которую я использую:

import java.math.BigInteger;
import java.util.Random;

public class RabinKarp {
private String pat; // the pattern // needed only for Las Vegas
private long patHash; // pattern hash value
private int M; // pattern length
private long Q; // a large prime, small enough to avoid long overflow
private int R; // radix
private long RM; // R^(M-1) % Q
static private long dochash = -1L;

public RabinKarp(int R, char[] pattern) {
    throw new RuntimeException("Operation not supported yet");
}

public RabinKarp(String pat) {
    this.pat = pat; // save pattern (needed only for Las Vegas)
    R = 256;
    M = pat.length();
    Q = longRandomPrime();

    // precompute R^(M-1) % Q for use in removing leading digit
    RM = 1;
    for (int i = 1; i <= M - 1; i++)
        RM = (R * RM) % Q;
    patHash = hash(pat, M);
}

// Compute hash for key[0..M-1].
private long hash(String key, int M) {
    long h = 0;
    for (int j = 0; j < M; j++)
        h = (R * h + key.charAt(j)) % Q;
    return h;
}

// Las Vegas version: does pat[] match txt[i..i-M+1] ?
private boolean check(String txt, int i) {
    for (int j = 0; j < M; j++)
        if (pat.charAt(j) != txt.charAt(i + j))
            return false;
    return true;
}

// check for exact match
public int search(String txt) {
    int N = txt.length();
    if (N < M)
        return -1;
    long txtHash;
    if (dochash == -1L) {
        txtHash = hash(txt, M);
        dochash = txtHash;
    } else
        txtHash = dochash;

    // check for match at offset 0
    if ((patHash == txtHash) && check(txt, 0))
        return 0;

    // check for hash match; if hash match, check for exact match
    for (int i = M; i < N; i++) {
        // Remove leading digit, add trailing digit, check for match.
        txtHash = (txtHash + Q - RM * txt.charAt(i - M) % Q) % Q;
        txtHash = (txtHash * R + txt.charAt(i)) % Q;

        // match
        int offset = i - M + 1;
        if ((patHash == txtHash) && check(txt, offset))
            return offset;
    }

    // no match
    return -1; // was N
}

// a random 31-bit prime
private static long longRandomPrime() {
    BigInteger prime = new BigInteger(31, new Random());
    return prime.longValue();
}

// test client

}

Реализация Rabin-Karp работает так, что возвращает правильное смещение строки, которую я ищу. Что мне удивительно, но это статистика времени, которая произошла, когда я запускал тестовую программу. Вот они:

Standard Java Time->39
Rabin Karp time->409

Это было действительно удивительно. Не только Rabin-Karp (по крайней мере, как он реализован здесь) не быстрее стандартной функции java indexOf String, тем медленнее на порядок. Я не знаю, что не так (если угодно). У кого-нибудь есть мысли об этом?

Спасибо,

Эллиот

Ответ 1

Я ответил на этот вопрос раньше, и Эллиот указал, что я просто ошибаюсь. Я извиняюсь перед сообществом.

В коде String.indexOf нет ничего волшебного. Это не изначально оптимизировано или что-то в этом роде. Вы можете скопировать метод indexOf из исходного кода String, и он работает так же быстро.

Здесь у нас есть разница между эффективностью O() и фактической эффективностью. Рабин-Карп для строки длины N и картины длины M, Рабин-Карп - O (N + M) и худший случай O (NM). Когда вы смотрите на него, String.indexOf() также имеет лучший вариант O (N + M) и худший случай O (NM).

Если текст содержит много частичных совпадений с началом шаблона, Rabin-Karp будет близок к его лучшей производительности, в то время как String.indexOf не будет. Например, я тестировал вышеприведенный код (правильно на этот раз:-)) на миллион '0, за которым следует одиночный' 1 ', и искали 1000' 0, а затем один '1'. Это вынудило String.indexOf к наихудшему результату. Для этого высоко вырожденного теста алгоритм Рабина-Карпа был примерно в 15 раз быстрее, чем indexOf.

Для текста на естественном языке Rabin-Karp останется близким к лучшему, а indexOf лишь немного ухудшится. Таким образом, решающим фактором является сложность операций, выполняемых на каждом шаге.

В этом внутреннем цикле indexOf сканирует соответствующий первый символ. На каждой итерации она должна:

  • увеличить счетчик циклов
  • выполнить два логических теста
  • сделать доступ к одному массиву

В Rabin-Karp каждая итерация должна:

  • увеличить счетчик циклов
  • выполнить два логических теста
  • сделать два доступа к массиву (фактически два вызова метода)
  • обновить хэш, для чего требуется 9 числовых операций

Поэтому на каждой итерации Рабин-Карп будет падать все дальше и дальше. Я попробовал упростить хэш-алгоритм только для символов XOR, но у меня все еще был дополнительный доступ к массиву и две дополнительные числовые операции, поэтому он все еще медленнее.

Кроме того, если совпадение найдено, Rabin-Karp знает только соответствие хэшей и поэтому должен проверять каждый символ, тогда как indexOf уже знает первые совпадения символов и, следовательно, имеет одно меньшее испытание.

Прочитав в Википедии, что Рабин-Карп используется для обнаружения плагиата, я взял Библейскую книгу Рут, удалил все знаки препинания и сделал все нижнее дело, в котором осталось менее 10000 символов. Затем я искал "andwomenherneighboursgaveitaname", который встречается в самом конце текста. String.indexOf был еще быстрее, даже с хешем XOR. Однако, если я удалил преимущество String.indexOfs возможности доступа к частному внутреннему массиву символов String и заставил его скопировать массив символов, тогда, наконец, Rabin-Karp был действительно быстрее.

Однако я сознательно выбрал этот текст, поскольку в Книге Руфь и 28 есть "213" и "и". Если вместо этого я искал только для последних символов "ursgaveitaname", то в тексте всего 3 "urs", поэтому indexOf возвращается ближе к лучшему и снова выигрывает.

В качестве более справедливого теста я выбрал случайные 20 символьных строк со второй половины текста и приурочил их. Rabin-Karp был примерно на 20% медленнее, чем алгоритм indexOf, выполняемый за пределами класса String, и на 70% медленнее, чем фактический алгоритм indexOf. Таким образом, даже в случае использования, предположительно, это подходит, это был еще не лучший выбор.

Итак, что хорошего - Рабин-Карп? Независимо от длины или характера текста, который нужно искать, у каждого персонажа он будет медленнее. Независимо от того, какую функцию хеша мы выберем, мы обязательно должны сделать дополнительный доступ к массиву и по крайней мере две числовые операции. Более сложная хеш-функция даст нам менее ложные совпадения, но потребует больше числовых операторов. Просто невозможно, чтобы Рабин-Карп не отставал.

Как показано выше, если нам нужно найти совпадение с префиксом часто повторяющегося блока текста, indexOf может быть медленнее, но если мы знаем, что мы делаем это, похоже, нам все равно будет лучше использовать indexOf для поиска для текста без префикса, а затем проверьте, присутствует ли префикс.

Основываясь на моих исследованиях сегодня, я не вижу времени, когда дополнительная сложность Рабина Карпа окупится.

Ответ 2

Здесь является источником java.lang.String. indexOf - строка 1770.

Мое подозрение связано с тем, что вы используете его в такой короткой строке ввода, дополнительные накладные расходы алгоритма Рабина-Карпа над кажущейся наивной реализацией java.lang.String indexOf, вы не видите истинной производительности алгоритм. Я бы предложил попробовать его на гораздо более длинной входной строке для сравнения производительности.

Ответ 3

Из моего понимания, Rabin Karp лучше всего использовать при поиске блока текста для нескольких слов/фраз.

Подумайте о неправильном поиске слов, чтобы помечать оскорбительный язык.

Если у вас есть список из 2000 слов, включая деривации, тогда вам нужно будет вызвать indexOf 2000 раз, по одному для каждого слова, которое вы пытаетесь найти.

RabinKarp помогает в этом, выполняя поиск по-другому. Сделайте 4 символа хэша каждого из 2000 слов и поместите его в словарь с быстрым поиском.

Теперь, для каждого 4 символа текста поиска, хеш и проверки против словаря.

Как вы можете видеть, поиск теперь наоборот - мы ищем слова 2000 для возможного соответствия. Затем мы получаем строку из словаря и выполняем равные проверки, чтобы быть уверенными.

Это также быстрый поиск таким образом, потому что мы ищем словарь вместо соответствия строк.

Теперь представьте, что сценарий WORST case делает все эти поиски indexOf - самое последнее слово, которое мы проверяем, является совпадением...

Статья в Википедии для RabinKarp даже упоминает о неполноценности в описываемой вами ситуации.;-) http://en.wikipedia.org/wiki/Rabin-Karp_algorithm

Ответ 4

Но это вполне естественно! Ваш тестовый ввод в первую очередь слишком тривиален.

indexOf возвращает индекс was для поиска небольшого буфера (String internal char array`), в то время как Rabin-Karp должен выполнить предварительную обработку, чтобы настроить свои данные на работу, которая занимает дополнительное время.

Чтобы увидеть разницу, вам нужно будет проверить в действительно большом тексте, чтобы найти выражения.

Также обратите внимание, что при использовании более сложного алгоритма строкового поиска у них может быть "дорогая" настройка/предварительная обработка, чтобы обеспечить действительно быстрый поиск.
В вашем случае вы просто ищете was в предложении. В любом случае вы всегда должны учитывать данные

Ответ 5

Не заглядывая в детали, мне приходят две причины:

Ответ 6

Не просто попробуйте более длинную статическую строку, но попробуйте генерировать случайные длинные строки и каждый раз вставлять цель поиска в случайное местоположение. Без его рандомизации вы увидите фиксированный результат для indexOf.

Редакция: Случайное - неправильное понятие. Большинство текстов не являются случайными. Но вам нужно много разных длинных строк, чтобы быть эффективными, а не просто тестировать одну и ту же строку несколько раз. Я уверен, что есть способы извлечь "случайные" большие строки из еще большего источника текста или что-то в этом роде.

Ответ 7

Для такого поиска Кнут-Моррис-Пратт может работать лучше. В частности, если подстрока не просто повторяет символы, тогда KMP должен превзойти indexOf(). Худший случай (строка всех одинаковых символов) будет одинаковой.