Алгоритм был разработан Кнутом (Knuth) и Праттом (Pratt) и независимо от них Моррисом (Morris) в 1977 г.
Он относится к "правильным" подходам решения поставленной задачи, в отличии от тривиального подхода, рассмотренного ранее.
Данный подход хоть и считается достаточно тривиальным, описания, которые нашел я, зачастую пестрят математическими основами и доказательствами, которые сбивают с сути. Так в книге, уважаемого Никлауса Вирта, приводится описание, которое я так и не одолел.
Однако я нашел пару статей, которые достаточно информативны, они приведены в ссылках и рекомендуемы для ознакомления.
Для понимания: Префикс строки A[..i] — это строка из i первых символов строки A. Суффикс строки A[j..] — это строка из |A|-j+1 последних символов. Подстроку из Aбудем обозначать как A[i..j], а A[i] — i-ый символ строки.
В статье http://habrahabr.ru/post/111449/ сказано:
Идея КМП-поиска – при каждом несовпадении двух символов текста и образа образ сдвигается на все пройденное расстояние, так как меньшие сдвиги не могут привести к полному совпадению.
Многие люди ошибаются в этом пункте, считая, что не надо возвращаться назад, а можно продолжать обработку строки A с текущей позиции. Почему это не так легко продемонстрировать на примере поиска X=«AAAB» в A=«AAAAB». Первая гипотеза нас приведет к четвертому символу A: "AAAAB", где мы обнаружим несоответствие. Если не откатиться назад, то вхождение мы так и не обнаружим, хотя оно есть.
Для реализации данного алгоритма, нам необходимо рассмотреть, так называемую, префикс функцию.
Префикс-функция - это массив чисел, вычисляющийся, как наибольшая длина суффикса, совпадающего с её префиксом. Как пример, берем каждый возможный префикс строки и смотрим самое длинное совпадение начала с концом префикса (не учитывая тривиальное совпадение самого с собой).
Вот пример для «ababcaba»:
| суффикс | префикс | p | 
|---|---|---|
| a | a | 0 | 
| ab | ab | 0 | 
| aba | aba | 1 | 
| abab | abab | 2 | 
| ababc | ababc | 0 | 
| ababca | ababca | 1 | 
| ababcab | ababcab | 2 | 
| ababcaba | ababcaba | 3 | 
Итак, идея ясна? Ну тогда попробум написать префикс функцию в лоб:
def predkompil(x):
    #первый символ всегда 0, поэтому заносим и пропускаем
    d = {0:0}
    for i in xrange(1,len(x)):
     # проходоим от конца к началу
        j = i
        sdvig = 0
        while j>0:
            j -= 1
            if x[j] == x[i-sdvig]:
                sdvig += 1
            else:
                j += sdvig
                sdvig = 0
        d[i] = sdvig
    return d
Вроде работает, но как то много сравнений и проходов. Попробуем оптимизировать.
Начать можно с замены строки j = i, на j = d[i-1]+1.
Что нам это дает? А то, что незачем просматривать каждый раз с конца и до начала. Можно заметить, что если мы на предыдущем шаге (i-1) нашли вхождение > 0 (d[i-1]), то и начинать стоит с данной позиции (d[i-1]).
Пример:
При i=4 ABAAB -> 2, тогда на i+1, т.е. i=5 смотрим не с j=i(5), а с 3! Т.к. либо счетчик вырастет и станет 3 в случае ABAABA, либо сбросится в 0 при любой другой букве.
И хотя мы делаем меньше сравнений(пропускаем середину слова), однако весь совпадающий префикс мы проходим, что не есть хорошо.
Это уже не плохо, однако, мы с Вами так и не научились использовать информацию, полученную на предыдущих итерациях.
Зная длину совпадения префикса с суффиксом по предыдущей букве, мы можем начать поиск сразу с этой позиции (d[j-1]) и продвигаясь вперед либо увеличивать данное значение (суффикс равный префиксу продолжит свой рост), либо обнуляется (нет совпадения).
def predkompil2(x):
    d = {0:0}
    for i in xrange(1,len(x)):
        j = d[i-1]#эту строку мы заменили
        while j > 0 and x[j] <> x[i]:
            j = d[j-1]
        if x[j] == x[i]:
            j += 1
        d[i] = j
    return d
В статье на хабре, вы можете ознакомиться с так называемой z-функцией.
Идея её очень схожа с префикс функцией, поэтому её реализацию я опущу :)
Итак, префикс функция готова, осталось применить её и решить поставленную задачу.
def kmpSearch(s, x):
    d = predkompil2(x)
    i = j = 0
    while i<len(s) and j<len(x):
        if x[j] == s[i]:
            i+=1
            j+=1
        elif j==0:
            i+=1
        else:
            j = d[j-1]
    else:
        if j == len(x):
            return i-j
        return None
Вот такая идея описывается в книге Вирта, зная значения префикс ф-ии сдвигаться на заданное расстояние, однако кто-то мог заметить, что для решения задачи, нам достаточно было использовать только префикс функцию. Для этого достаточно сделать конкатенацию искомой подстроки, со строкой, через разделитель, заведомо уникальный(что-то вроде #) и применив префикс ф-ию. Если она даст значение равное длине искомой подстроки, значит мы нашли это место иначе - нет.
def kmpSearchByCompil(s, x):
    d = {0:0}
    template = x + '#' + s
    for i in xrange(1,len(template)):
        j = d[i-1]#незачем смотреть всю строку, можно посмотреть со сдвига прошлой подстроки
        while j > 0 and template[j] <> template[i]:
            j = d[j-1]
        if template[j] == template[i]:
            j += 1
        d[i] = j
        if j == len(x):
            return i
    return None 
Надеюсь, идея понятна.
Мы рассмотрели с Вами один из "правильных" алгоритмов поиска подстроки, однако, классикой данной задачи стал алгоритм Бойера - Мура.
Автор статьи на Хабре утверждает, что он достаточно не тривиален и запутан, однако вынужден с ним не согласиться, что и покажу в следующей заметке.

Комментариев нет:
Отправить комментарий