Поиск подстроки в строке
Задача поиска подстроки одна из достаточно распространённых в информатике. Строкой называют последовательность символов (в произвольном порядке) взятых из заданного алфавита. Так например, алфавитом могут быть цифры {0, 1} из которых можно составлять неограниченную по длине цепочку символов, например, 0110100110 или 0. Понятие алфавита и другие раскрываются в теории формальных языков.
В рамках этой статьи рассмотрены задачи нахождения одной строки в другой. Результатом поиска могут быть как простой ответ «да / нет» на вопрос «содержит ли данная строка представленный образец», так и информация о точном месте начала (или начал, если их несколько) совпадения подстроки с образцом. Назовём образцом искомую строку, а подстрокой — массив  для индексов 
 таких что 
, где 
 — длина строки, а 
 — сама строка. Также введём образец как 
.
Простой (наивный) поиск
Суть алгоритма простого поиска состоит в последовательном переборе с последующим сравнением символов строки и образца. Для этого достаточно выполнить следующий алгоритм:
  for i = 0 to S.length do:
    for j = 0 to P.length do: 
      if S[i + j] != P[j] then break
    end for
    if j == P.length then return i
  end for
  return -1
Переменная i отвечает за сдвиг образца на один символ на каждой итерации. Внутренний цикл проверяет совпадение символа строки с индексом i + j и символа образца с индексом j. Если символы не совпадают, тогда внутренний цикл прекращает работу. Оговоримся, что если алгоритм отрабатывает все итерации, то переменная цикла будет равна значению правой границы указанного как N. В таком случае, если длина образца и переменной равны, то это будет означать, что внутренний цикл не нашёл разных символов, а значит подстрока найдена, а индексом начала подстроки является переменная i. В противном случае внешний цикл продолжает работу.
Здесь намеренно не приводится ограничений на правую границу внешнего цикла, т.к. сути алгоритма это не меняет. В общем легко посчитать, что сложность такого поиска будет , где 
 — длина строки 
, а 
 — образца 
.
Алгоритм Рабина — Карпа
Этот алгоритм старается уменьшить количество проверок во внутреннем цикле простого поиска за счёт использования хэш-функции.
Понятие хэш-функции
Хэш-функция в данном случае преобразовывает исходную строку в числовое значение. Само преобразование называется хэшированием и в общем может выполняться не только для строк, но и для произвольного массива данных, а выходным значением является битовый массив заданной длины. Рассмотрим эти определения на простых примерах, где задано множество  чисел для которых нужно посчитать хэш-функцию в множество 
. Тогда нам нужна функция 
, значение которой будет принадлежать множеству 
.
Например, у нас есть множество целых чисел от 0 до 1 000 000 и ограничение на значение хэш-функции в 10 битов. 10 битов это  доступных значений функции. Существуют различные способы написать такую функцию, например:
- методом деления, когда входное число делится на количество доступных значений хэш-функции, а результатом является остаток от такого деления: ; 
- метод середины квадрата умножает входное число само на себя, а из его середины берётся необходимое количество знаков;
- метод умножения имеет вид , где — количество символов представимых машинным словом (для 32-битной системы это будет ), а — взаимно простое число к . 
При любой функции могут случаться коллизии. Коллизиями называется ситуация, когда разные элементы множества  отображаются в один элемент множества 
. Например, для примера в методе деления такое происходит при 
. Если коллизии не случаются, то такое хэширование называют идеальным хэшированием.
Также существуют хэш-таблицы, которые являются ещё одним способом создания словаря (англ. map). Они используют разные стратегии для разрешения коллизий при сохранении пары ключ-значения в таблицу, например, метод цепочек или метод открытой адресации.
Для работы алгоритма необходимо написать хэш-функцию для заданной строки. Если пронумеровать символы алфавита из которых состоит строка, то можно написать тривиальный скользящий хэш, представляющий из себя сумму индексов каждого символа. Например, когда задан алфавит {A, B, C, D}, а символы имеют индексы от 1 до 4, то для строки ACDDAB будем иметь, что 1 + 3 + 4 + 4 + 1 + 2 = 15.
При сдвиге образца на один символ значение хэш-функции для подстроки будет меняться как «минус предыдущий элемент, плюс следующий». Заранее посчитав значение хэш-функции для образца и пользуясь предложенной хэш-функцией при сдвиге образца, мы каждый раз сравниваем сами значения получаемых хэш-функций. Если они совпадают, то нам нужно уже проверить строки посимвольно и, если они полностью совпали, то ответ получен (сравнивать необходимо из-за возможности коллизий):
PH = hash(P)
SH = hash(S(0, P.length))
for i = 0 to S.length do
  if PH == SH and S(i, i + P.length) == P then
    return i
  end if
  SH = SH - S[i] + S[i + P.length]
end for
return -1
В общем случае без использования скользящего хэша необходимо на каждой итерации считать хэш от подстроки и скорость работы напрямую зависит от того, насколько быстро этот хэш можно посчитать.
Алгоритм Бойера — Мура
Идея этого алгоритма заключается в том, что строки можно сравнивать с конца. Это позволит пропускать не 1 символ как в наивном алгоритме, но зачастую сразу всю строку. Это возможно, поскольку в тот момент, когда обнаруживается несовпадающий символ между строкой и образцом, то образец необходимо сдвинуть вправо на столько символов, чтобы несовпадающий символ строки теперь совпал с символом образца. Если такого символа в образце нет, то строка сдвигается полностью.
Рассмотрим пример:
ровкдткотор кот********
Начиная проверку с конца к началу определяем, что не совпадают символы «в» и «т». Сдвиг, величина шага которого зависит от таблицы, описанную позже, в этом случае будет равен 3:
ровкдткотор ***кот*****
Снова проверяем с конца: после совпадения «т» в обеих строках не совпадают «д» и «о». Снова сдвигаем, шаг в этом случае равен 3:
ровкдткотор ******кот**
В данном примере все символы совпадают, поэтому алгоритм останавливается. Теперь определим, каким образом определяется величина шага для каждого символа. Поскольку суть алгоритма в том, чтобы при несовпадении символа мы двигали образец вправо до тех пор, пока этот символ не совпадёт с символом образца:
персональные данные данные************* -> не совпал "н" (не совпал в первом символе, индекс 5), сдвигаем на 5 - 3 = 2 (3 по таблице) **данные*********** -> не совпал "л", сдвигаем на 6, т.к. символа нет в таблице ********данные***** -> не совпал "д" (не совпал в первом символе индекс 5), сдвигаем на 5 - 0 = 5 (0 по таблице) *************данные -> совпали
Для составления таблицы используется следующее правило: значение для символа равно максимальному индексу этого элемента в образце (исключая последний символ, для него и всех других значение равно количеству символов в образце). Так для строки «данные» это будет:
| д | а | н | ы | * | 
| 0 | 1 | 3 | 4 | 6 | 
Тогда величина сдвига при сравнении справа налево и при не совпадении символа на j-ой позиции, где сам символ , будет равна 
. Приведём алгоритм с приведённой эвристикой, называемой эвристикой стоп-символа:
table = create map
for i = 0 to P.length - 1 do
  table[P[i]] = i
end for
for i = 0 to S.length do #NEXT
  for j = P.length - 1 down to 0 do
    if P[j] != S[j + i] then
      step = j - table[S[j + i]]
      i = i + step
      go to #NEXT
    end if
  end for
  return i
end for
return -1
Эффективность поиска в таком алгоритме достигает 
Алгоритм Кнута — Морриса — Пратта
Последний из рассматриваемых алгоритмов и являющимся самым эффективным, т.к. работает за линейное время. Основой алгоритма является определение префикс-функции. Префикс-функция  вычисляет длину наибольшего собственного (т.е. не равного самой подстроке) префикса совпадающего с суффиксом этой подстроки. Так, для строки 
котокот префикс «кот» совпадает с суффиксом и длина его равна 3.
Наивный алгоритм подсчёта префикс-функции для произвольной строки  при всех значениях 
 (при этом 
) занимает квадратичное время. Для более эффективного расчёта нужно отметить, что при добавлении нового символа подстроки 
 при 
 у нас может получится, что 
, т.е. увеличенный суффикс совпадает с префиксом. Это означает, что в этом случае можно сказать, что 
. Если они не совпадают, то мы пробуем посмотреть на меньший суффикс. Поскольку 
 к этому моменту уже подсчитано и мы знаем для него префикс, то полагаем, что 
 и снова проверяем условие 
. Если в какой-то момент 
, то полагаем, что 
.
Таким образом задача сводится к заполнению таблицы вида:
| i | 0 | 1 | … | N | 
| π | 0 | * | … | * | 
Для решения задачи поиска подстроки достаточно склеить образец и строку с помощью символа, который не входит в алфавит, напр.: P + ‘#’ + S, после чего начать считать префикс-функцию. Как только значение функции будет равно длине образца, то это будет означать, что подстрока найдена:
C = P + '#' + S
PI = create array with C.length
for i = 1 to C.length do
  j = PI[i - 1]
  while j > 0 and C[j] != C[i] do
    j = PI[j - 1]
  end while
  if C[j] == C[i] then
    PI[i] = j + 1
  end if
  if PI[i] == P.length then return TRUE
end for
return FALSE