Поиск подстроки в строке с использованием хеширования. Алгоритм Рабина-Карпа — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Алгоритм)
(Метод хеширования)
Строка 2: Строка 2:
 
==Метод хеширования==
 
==Метод хеширования==
  
Для решения задачи удобно использовать полиномиальный хеш, так его легко пересчитывать: <tex>\mathrm{hash}(s[1..n]) = (p^{n - 1} s[1] + ... + p^{0} s[n]) \bmod r</tex>, где <tex>p</tex> {{---}} это некоторое простое число, а <tex>r</tex> {{---}} некоторое большое число, для уменьшения числа коллизий (обычно берётся <tex>2^{32}</tex> или <tex>2^{64}</tex>, чтобы модуль брался автоматически при переполнении типов). Стоит обратить внимание, что если 2 строчки имеют одинаковый хэш, то они в большинстве таких случаев равны.
+
Для решения задачи удобно использовать полиномиальный хеш, так его легко пересчитывать: <tex>\mathrm{hash}(s[1..n]) = (p^{n - 1} s[1] + ... + p^{0} s[n]) \bmod r</tex>, где <tex>p</tex> {{---}} это некоторое простое число, а <tex>r</tex> {{---}} некоторое большое число, для уменьшения числа коллизий (обычно берётся <tex>2^{32}</tex> или <tex>2^{64}</tex>, чтобы модуль брался автоматически при переполнении типов). Стоит обратить внимание, что если 2 строчки имеют одинаковый хеш, то они в большинстве таких случаев равны.
  
 
При удалении первого символа строки и добавлении символа в конец считать хеш новой строки при помощи хеша изначальной строки возможно за <tex>O(1)</tex>:
 
При удалении первого символа строки и добавлении символа в конец считать хеш новой строки при помощи хеша изначальной строки возможно за <tex>O(1)</tex>:

Версия 22:34, 14 июня 2015

Алгоритм Рабина-Карпа предназначен для поиска подстроки в строке.

Метод хеширования

Для решения задачи удобно использовать полиномиальный хеш, так его легко пересчитывать: [math]\mathrm{hash}(s[1..n]) = (p^{n - 1} s[1] + ... + p^{0} s[n]) \bmod r[/math], где [math]p[/math] — это некоторое простое число, а [math]r[/math] — некоторое большое число, для уменьшения числа коллизий (обычно берётся [math]2^{32}[/math] или [math]2^{64}[/math], чтобы модуль брался автоматически при переполнении типов). Стоит обратить внимание, что если 2 строчки имеют одинаковый хеш, то они в большинстве таких случаев равны.

При удалении первого символа строки и добавлении символа в конец считать хеш новой строки при помощи хеша изначальной строки возможно за [math]O(1)[/math]:

[math]\mathrm{hash}(s[i + 1..i + m - 1]) = (\mathrm{hash}(s[i..i + m - 1]) - p^{m - 1} s[i]) \bmod r[/math].

[math]\mathrm{hash}(s[i + 1..i + m]) = (p \cdot \mathrm{hash}(s[i + 1..i + m - 1]) + s[i + m]) \bmod r[/math].

Получается : [math]\mathrm{hash}(s[i + 1..i + m]) = (p \cdot \mathrm{hash}(s[i..i + m - 1]) - p^{m} s[i] + s[i + m]) \bmod r[/math].

Алгоритм

Алгоритм начинается с подсчета [math]\mathrm{hash}(s[1..m])[/math] и [math]\mathrm{hash}(p[1..m])[/math].

Для [math]i \in [1..n - m + 1][/math] вычисляется [math]\mathrm{hash}(s[i..i + m - 1])[/math] и сравнивается с [math]\mathrm{hash}(p[1..m])[/math]. Если они оказались равны, то образец [math]p[/math] скорее всего содержится в строке [math]s[/math] начиная с позиции [math]i[/math], хотя возможны и ложные срабатывания алгоритма. Если требуется свести такие срабатывания к минимуму или исключить вовсе, то применяют сравнение некоторых символов из этих строк, которые выбраны случайным образом, или применяют явное сравнение строк, как в наивном алгоритме поиска подстроки в строке. В первом случае проверка произойдет быстрее, но вероятность ложного срабатывания, хоть и небольшая, останется. Во втором случае проверка займет время, равное длине образца, но полностью исключит возможность ложного срабатывания.

Для ускорения работы алгоритма оптимально предпосчитать [math]p^{m}[/math].

Псевдокод

Приведем пример псевдокода, который находит все вхождения строки [math]w[/math] в [math]s[/math] и возвращает массив позиций, откуда начинаются вхождения.

vector<int> rabinKarp (s : string, w : string):
   vector<int> answer
   int n = s.length
   int m = w.length
   int hashS = hash(s[1..m])
   int hashW = hash(w[1..m])
   for i = 1 to n - m + 1
        if hashS == hashW
             answer.add(i)
        hashS = (p * hashS - p[math]^{m}[/math] * hash(s[i]) + hash(s[i + m])) mod r //r - некоторое большое число, p - некоторое просто число
   return answer

Новый хеш [math]hashS[/math] был получен с помощью быстрого пересчёта. Для сохранения корректности алгоритма нужно считать, что [math]s[n + 1][/math] — пустой символ.

Время работы

Изначальный подсчёт хешей выполняется за [math]O(m)[/math]. В цикле всего [math]n - m + 1[/math] итераций, каждая выполняется за [math]O(1)[/math]. Итоговое время работы алгоритма [math]O(n + m)[/math].

Надёжность

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

Например если взять [math] r = 2^{32}, p = 237 [/math], за [math] s [/math] принять строку Туэ-Морса[1] длиной [math] 1024 [/math] , то алгоритм находит лишние вхождения почти в половине случаев.

Примечания

Источники информации

  • Кормен, Томас Х., Лейзерсон, Чарльз И., Ривест, Рональд Л., Штайн Клиффорд Алгоритмы: построение и анализ, 3-е издание. Пер. с англ. — М.:Издательский дом "Вильямс", 2014. — 1328 с.: ил. — ISBN 978-5-8459-1794-2 (рус.) — страницы 1036–1041.

Ссылки