Ссылка на github
D
Dead
S
Simple
C
Code

Алгоритм Кнута-Морриса-Пратта

Содержание

  1. Введение
  2. Общие сведения
  3. Этап 1. Префикс функция
  4. Этап 2. Реализация поиска
  5. Дополнительно. Поиск всех вхождений

Введение

В данной статье рассмотрим алгоритм Кнута-Морриса-Пратта, реализуем его на языке Go(Golang) и оценим сложность алгоритма используя O-большое.

Общие сведения

Алгоритм Кнута-Морриса-Пратта используется для поиска подстроки в тексте, и является более эффективным в сравнении с наивным поиском который сравнивает каждый символ подстроки и текста. Вместо этого данный алгоритм предлагает вычислить значение префикс функции, чтобы в случае несовпадения символов использовать полученное значение для определения ближайшего символа с которым может быть продолжено сравнение, для наивного алгоритма это всегда первый символ подстроки, что оказывает значительное влияние на время работы поиска. В общем виде алгоритм Кнута-Морриса-Пратта состоит из двух этапов: вычисление значения префикс функции и поиск по тексту с использование полученного значения на первом этапе.

Этап 1. Префикс функция

Префикс функцией строки str является массив целых чисел pfx, где элемент массива pfx[i] это максимальная длина собственного префикса подстроки str[0…i], который также является суффиксом подстроки str[0…i]. Рассмотрим пример вычисления префикс функции.

Изображение иллюстрирует вычисление префикс функции
Изображение иллюстрирует вычисление префикс функции

На изображении выше сравниваются символы строки str[i] и str[j], если символы равны, то в ячейку массива pfx[i] будет записано значение j + 1. В случае когда символы не равны и j равно 0 в ячейку массива pfx[i] записывается значение 0. Однако, если значение j больше 0, тогда потребуется изменить значение j на значение ячейки pfx[j-1] и продолжить сравнение символов. Описанный алгоритм префикс функции указанной строки может быть реализован следующим образом:

123456789101112131415161718192021222324252627282930
// Вычисление префикс функции.
// str - список символов для вычисления префикс функции
func Prefix(str []rune) []int {
  prf := make([]int, len(str))

  j := 0
  i := 1
  for i < len(str) {
    // Если символы равны, то переходим к следующим
    // символам для сравнения
    if str[i] == str[j] {
      prf[i] = j + 1
      i++
      j++
    } else {
      // Если символы не равны и j равен 0, то переходим
      // к следующему символу для сравнения
      if j == 0 {
        i++
        // Если символы не равны и j больше 0, то переходим
        // используем полученное значение префикс функции для
        // определения j
      } else {
        j = prf[j-1]
      }
    }
  }

  return prf
}

Данная реализация работает за O(n), где n - длина указанной строки.

Этап 2. Реализация поиска

Получив значение префикс функции подстроки алгоритм поиска сводится к следующим шагам:

  1. Символ подстроки ptr[j] сравнивается с символом текста str[i], если символы равны, то необходимо перейти к сравнению следующих символов, как только значение j достигает длины подстроки поиск считается успешным и его можно завершить. В случае когда символ ptr[j] не равен str[i] происходит переход к шагу 2.

  2. Если символы ptr[j], str[i] не равны и значение j равно 0, тогда значение i сдвигается на единицу и алгоритм возвращается к шагу 1, в ином случае алгоритм переходит к шагу 3.

  3. Если символы ptr[j], str[i] не равны и значение j больше 0, необходимо использовать значение префикс функции, чтобы найти следующий символ для сравнения присвоив значение pfx[j-1] индексу j, и далее продолжить поиск перейдя к шагу 1.

  4. Изображение ниже иллюстрирует описанные шаги на примере:

    Изображение иллюстрирует алгоритм Кнута-Морриса-Пратта по шагам
    Изображение иллюстрирует алгоритм Кнута-Морриса-Пратта по шагам

    Поиск может быть реализован следующим образом:

    123456789101112131415161718192021222324252627282930313233343536
    // Поиск первого вхождения подстроки с использованием
    // алгоритма Кнута-Морриса-Пратта.
    // str - строка в которой будет происходить поиск
    // ptr - искомая подстрока
    func Search(str []rune, ptr []rune) int {
      prf := Prefix(ptr)
      size := len(ptr)
    
      i := 0
      j := 0
      for i < len(str) {
        // Если символы равны, то переходим к сравнению
        // следующих символов
        if str[i] == ptr[j] {
          i++
          j++
    
          // Если j достиг длины ptr, то подстрока
          // найдена и поиск завершается
          if j == size {
            return i - size
          }
        } else {
          // Если символы не равны и j = 0, переходим к
          // следующему символу текста
          if j == 0 {
            i++
            // Если символы не равны и j > 0, находим новое
            // значение для j используя значение префикс функции
          } else {
            j = prf[j-1]
          }
        }
      }
      return -1
    }
    

    В строках 14-23 сравниваются символы искомой подстроки и текста, если индекс j достиг длины подстроки, то поиск завершается, что соответствуют шагу 1 из описания выше. В строках 26-30 происходит инкремент индекса i в том случае, если индекс j равен 0, данное условие аналогично шагу 2. Строки 30-32 соответствуют шагу 3, который использует значение префикс функции для поиска следующего значения индекса j.

    Поиск подстроки в данной реализации выполняется за O(m + n), где O(n) это вычисление префикс функции, а O(m) поиск подстроки в тексте.

    Дополнительно. Поиск всех вхождений

    Чтобы найти все вхождения подстроки в текст используя алгоритм Кнута-Морриса-Пратта потребуются незначительные изменения кода приведенного выше. Когда индекс j достигает значения равного длине подстроки необходимо добавить значение в массив и продолжить сравнение с первого символа подстроки. Поиск всех вхождений может быть реализован следующим образом:

    1234567891011121314151617181920212223242526272829303132333435363738
    // Поиск всех вхождений подстроки с использованием
    // алгоритма Кнута-Морриса-Пратта.
    // str - строка в которой будет происходить поиск
    // ptr - искомая подстрока
    func SearchAll(str []rune, ptr []rune) []int {
      prf := Prefix(ptr)
      size := len(ptr)
      res := []int{}
    
      i := 0
      j := 0
      for i < len(str) {
        // Если символы равны, то переходим к сравнению
        // следующих символов
        if str[i] == ptr[j] {
          i++
          j++
    
          // Если j достиг длины ptr, то необходимо добавить значение в res
          if j == size {
            res = append(res, i-size)
            j = 0
            i++
          }
        } else {
          // Если символы не равны и j = 0, переходим к
          // следующему символу текста
          if j == 0 {
            i++
            // Если символы не равны и j > 0, находим новое
            // значение для j используя значение префикс функции
          } else {
            j = prf[j-1]
          }
        }
      }
      return res
    }
    

    В строках 20-24 заключается главное отличие от реализации представленной в предыдущем разделе, поиск не завершается с нахождением единственного соответствия, а продолжается до конца текста. Данная реализация выполняется за O(m + n), как и поиск первого вхождения приведенный ранее, но требует дополнительной памяти для хранения найденных индексов.

    Весь исходный код примеров к данной статье вы можете найти на GitHub.

Copyright © 2024 DeadSimpleCode.