Двусторонний алгоритм #
Двусторонний алгоритм (англ. Two-Way String-Matching algorithm) — алгоритм поиска подстроки в строке, разработанный и представленный Максимом Крочемором (Maxime Crochemore) и Домиником Перреном (Dominique Perrin) в 1991 году.[2] Относится к группе алгоритмов поиска подстроки с ограниченным потреблением памяти, константным на практике (constant-space string matching).
По заданной строке Text длины n и шаблону поиска Pattern длины m алгоритм позволяет найти все вхождения Pattern в Text за время , где — этап предварительных вычислений над шаблоном, а — этап поиска. При этом, во время поиска выполняется не более операций сравнения.
В отличие от алгоритмов Кнута-Морриса-Пратта и Бойера-Мура, данный алгоритм требует памяти: на этапе препроцессинга строка шаблона разбивается на 2 подстроки и хранится только позиция этого разбиения. Численное значение этой позиции меньше длины шаблона , поэтому требование по памяти может быть представлено как бит и на практике может быть взято за .
Другое отличие данного алгоритма заключается в том, что ему требуется упорядоченный алфавит, так как во время препроцессинга символы сравниваются на больше и меньше.
Алгоритм в целом считается достаточно эффективным и благодаря тому, что потребление памяти
практически не зависит от длины текста или шаблона, он используется в реализациях функций
strstr()
, memem()
некоторых вариантов библиотек libc: glibc, newlib musl при
достаточно длинной строке шаблона.
Общая схема #
Сканирование строки на этапе поиска двустороннего алгоритма похоже на сканирование в примитивном алгоритме, алгоритмах Кнута-Морриса-Пратта и Бойера-Мура.
В алгоритме Кнута-Морриса-Пратта строка сканируется слева направо, сравнение символов шаблона с символами текста также выполняется слева направо. Когда происходит несовпадение в какой-то позиции шаблона (либо все символы шаблона равны и найдено вхождение), шаблон сдвигается вправо на несколько позиций по заранее вычисленной таблице сдвигов паттернов поиска.
В алгоритме Бойера-Мура строка также сканируется слева направо, но символы шаблона сравниваются справа налево. При встрече несовпадающего символа, шаблон сдвигается на смещение, которое вычисляется как максимальное по 2 эвристикам: правилу сдвига плохого символа и правилу хорошего суффикса.
В двустороннем алгоритме строка шаблона делится на 2 подстроки: и . Сначала сравнивается правая подстрока слева направо, затем, если все символы правой подстроки совпали, то сравнивается левая подстрока справа налево. Если встречается несовпадающий символ, то шаблон сдвигается вправо на некоторое количество позиций. Правила сдвигов при несовпадении разные для правой и левой части разбиения. Величина сдвига вычисляется за константное время и константную память.
Общую схему для сканирования можно представить следующим псевдокодом:
fun stringMatching(text, pattern): Boolean {
val n = text.length
val m = pattern.length
val (u, v) = factorize(pattern)
var pos = 0, i = u.length
while (pos <= n-m) {
// Сравнение правой части
while (i < m && pattern[i] == text[pos + i]) i += 1
if (i == m) {
// Сравнение левой части
if (text.substring(pos .. pos + u.lastIndex) == u) {
return true
}
}
(pos, i) = shift(pos, i)
}
return false
}
factorize()
— функция разбиения шаблона на 2 подстроки.
Двусторонний алгоритм использует разбиение шаблона на подстроки, называемый критическим
разбиением (Critical Factorization) и базируется на теореме о критическом разбиении
(Critical Factorization Theorem).shift()
— правила сдвигов.
Двусторонний алгоритм выбирает разбиение строки , такое, что длина левой
подстроки меньше глобального периода строки шаблона (), длина
минимальна, а правая подстрока периодична.
Есть также другой алгоритм, относящийся к алгоритмам с ограниченным потреблением памяти и работающий по такой же схеме — это алгоритм Галиля — Сейфераса (Galil - Seiferas) [1], [4]. В нем используется разбиение, называемое идеальным разбиением (perfect factorization). Выбирается позиция, где начинается наиболее часто повторяющаяся подстрока шаблона.
Базовые понятия #
Зададим некоторые связанные со строками базовые определения, которые понадобятся в описании алгоритма.
- Алфавит
Алфавит (alphabet) — это конечное непустое множество символов. Обозначим его через . Примеры наиболее часто используемых алфавитов:
- — множество строчных букв английского алфавита
- — множество цифр
- : бинарный алфавит
и т.п.
- Строка
Строка (текст, слово) (string, text, word) — конечная последовательность символов из некоторого алфавита . Длина строки — это количество элементов в последовательности. Например, длина строки “” равна 6.
Введем следующие обозначения для некоторый строки :
— длина строки .
— -ый символ строки (считаем, что первый символ находится по индексу 0).
— подстрока строки .
Пустую строку обозначим через . Множество всех последовательностей алфавита — через , а множество всех непустых строк — через .
- Подстрока
Строка длины является подстрокой (или фактором) (factor, subword) строки , если для некоторого целого . Мы можем говорить, что подстрока встречается в строке в позиции . Стоит отметить, что подстрока — это не то же самое, что и подпоследовательность.
Строка является подпоследовательностью (subsequence) строки , если она может быть получена удалением из нуля или более (не обязательно смежных) символов. Другими словами, если , где — увеличивающаяся последовательность индексов строки .- Операции над строками
Конкатенацией двух строк и называют строку с последовательной записью их символов. Обозначим конкатенацию строк и как . Например, если , то .
Через обозначим конкатенацию копий строки . Например, для , . Стоит отметить, что конкатенация ассоциативна, т.е. , поэтому скобки здесь не обязательны и выражение можно записать как .
Рассмотрим строку , представляющую из себя композицию из 3 подстрок: , и
. Подстроки , и называются факторами
строки . Кроме того:
— это префикс строки ,
— это суффикс строки .
- Префикс
Строка называется префиксом (prefix) строки , если существует такая строка , что . Например, — это префикс строки . Префикс можно рассматривать как особый случай подстроки. Обычный префикс может быть как пустой строкой (ε), так и всей строкой.
Собственный префикс (proper prefix) — это префикс с дополнительными ограничениями: он не может быть пустым и не может быть равен всей строке целиком.- Суффикс
Строка называется суффиксом (suffix) строки , если существует такая строка , что Например, — суффикс строки . Также, как и в случае с префиксом, обычный суффикс может быть как пустым, так и всей строкой.
Собственный суффикс (proper suffix) — это суффикс, который не может быть равен пустой строке и всей строке целиком.- Период
Период (period) строки — это целое число , такое, что
Проще говоря, это длина постоянно повторяющегося фрагмента текста, причем, последнее вхождение может быть “обрезано”. Например, строка (её длина равна 9), имеет периоды 4, 7, 8 и 9:
Отметим, что длина строки всегда является её периодом, а это значит, что любой строки
всегда есть хотя бы один период.
Обозначим наименьший период строки через (численное значение), а саму
подстроку наименьшего периода — через .
Возьмем непустую строку и какое-то число такое, что . Во всех следующих случаях будет периодом строки :
Если — это подстрока некоторой строки , где и .
Пример
Если взять за любой префикс или подстроку (фактор) строки , то один из её периодов будет тоже 3.Если строку можно представить как , где , строка — непустая и
ПримерВозьмем некоторые строки , равной длины и строку , такие, что ; . В этом случае длина и будет периодом строки : .
Пример
- Грань
Грань (также граница или бордер) (border) строки — это любой собственный префикс строки, который равен её суффиксу. Т.е. грань — это такая строка , что . Грань не может быть равна всей строке, но суффикс и префикс строки могут быть перекрывающимися. Например, грани строки : .
Обозначим наибольшую грань строки через . Мы можем рассматривать такую грань как наибольшее совпадающее пересечение при наложении строки самой на себя.
Строка свободна от граней (border free), если наибольшая грань строки — это пустая строка, или другими словами, если наименьший период равен длине строки ()
Между гранями и периодами имеется взаимно однозначное соответствие, это можно увидеть по пункту 3 из описания периода.
Описание алгоритма #
Пусть — непустая строка, а — разбиение . Обозначим через длину левой части
разбиения минус 1 (). .
Непустая строка называется повторением для разбиения
(repetition for factorization) или повторением в позиции строки
, если выполняются оба условия:
- — суффикс или — суффикс
- — префикс или — префикс
Базовый случай повторения ( — суффикс и — префикс ) — это когда разбиение между и — это центр квадрата где-то в строке :
Другие случаи соответствуют ситуациям, когда находится близко к краям строки или при переполнении . Стоит заметить, что если есть строка , а в качестве взять строку , то эта строка будет повторением для разбиения (выполнятся оба условия). Т.е. любое разбиение непустой строки всегда имеет хотя бы одно повторение.
Длина повторения для разбиения называется локальным периодом или локальным периодом строки в позиции . Другими словами, число является локальным периодом строки в позиции , если для всех индексов и выполняются оба условия повторения.
Наименьшее возможное значение локального периода для разбиения обозначается как .
Примеры
Пусть строка .
Рассмотрим наименьший локальный период в позиции :
,
наименьшее повторение .
Рассмотрим наименьший локальный период в позиции :
,
наименьшее повторение .
Пусть строка
Рассмотрим наименьший локальный период в позиции :
,
наименьшее повторение .
Рассмотрим наименьший локальный период позиции :
,
наименьшее повторение .
Наименьший локальный период разбиения не может быть больше глобального периода строки: , это следует из определений.
Разбиение , при котором наименьший локальный период равен глобальному периоду всей строки называется критическим разбиением (critical factorization), а позиция — критической позицией (critical position).
Пример
Пусть .
Глобальный период строки : 7 ()
Локальные периоды для каждой позиции разбиения показаны в следующей таблице:
Критические разбиения: и .
В двустороннем алгоритме в качестве разбиения для строки шаблона используется критическое разбиение, при этом длина левой подстроки меньше глобального периода всей строки шаблона ().
Используются следующие правила сдвигов:
При несовпадении в процессе сравнения правой подстроки шаблон сдвигается вправо на количество просканированных символов, так, чтобы позиция разбиения шаблона оказалась на позиции несовпадающего символа:
При несовпадении в процессе сравнения левой подстроки шаблон сдвигается на глобальный период шаблона :
Такие правила сдвигов приводят к квадратичной сложности в худшем случае. Для улучшения
асимптотической сложности худшего случая, в алгоритме также используется техника
запоминания префикса (prefix memorization): когда производится сдвиг на
по правилу сдвига для левой подстроки, дополнительно запоминается префикс совпавшей части
шаблона, чтобы не сравнивать его ещё раз в следующем цикле.
Это та же техника, которая используется в модификации алгоритма Бойера-Мура с добавлением
правила Галиля (Wikipedia).
Ниже представлена первый вариант двустороннего алгоритма. Считаем, что глобальный период строки шаблона и критическое разбиение с вычислены заранее.
Код на kotlin, без обработки суррогатных пар.
data class Factorization(val left: IntRange, val right: IntRange)
/**
* Возвращает позиции всех вхождений шаблона [pattern] в строке [text]
*/
fun cp1(text: String, pattern: String): List<Int> {
val result = mutableListOf<Int>()
// factorize() разбивает строку шаблона pattern на 2 подстроки:
// left и right так, что |left| < period.
// left, right - закрытые диапазоны индексов соответствующих подстрок
// period - глобальный период строки шаблона
val (left, right) = factorize(pattern)
val period = pattern.getMinimalPeriod()
var pos = 0
var memPrefix = -1
while (pos + pattern.length <= text.length) {
// Сравнение правой части
val i = (maxOf(right.first, memPrefix + 1)..right.last).find{ i->
pattern[i] != text[pos + i]
}
if (i != null) {
pos = pos + i - right.first + 1
memPrefix = -1
} else {
// Сравнение левой части
val match = (left.last downTo memPrefix + 1).all { j ->
pattern[j] == text[pos + j]
}
if (match) result.add(pos)
pos += period
memPrefix = pattern.length - period - 1
}
}
return result
}
memprefix
— позиция запомненного префикса, обновляется при встрече несовпадающих
символов. Может быть установлена в неотрицательное значение только при сканировании левой
подстроки.
- Лемма
Пусть — критическое разбиение строки . Если — суффикс и — префикс (базовый случай), то длина кратна глобальному периоду строки .
Доказательство
Свойство тривиально выполняется для пустой строки, в противном случае, так как , т.е. равен периоду строки , то можно записать как , где , — не пустая, . Если не будет пустой, то она будет повторением для разбиения . Но будет противоречить тому, что — критическое разбиение. Поэтому, .
Доказательство корректности алгоритма есть в [1]. В таком виде алгоритм использует максимум операций сравнения. (доказательство там же).
Глобальный период строки шаблона требуется только тогда, когда он относительно не большой (меньше примерно половины длины шаблона), в остальных случаях можно обойтись без его вычисления.
Второй вариант двустороннего алгоритма рассчитан на применение при большом периоде строки шаблона. Дополнительное его преимущество — относительно небольшое максимальное количество сравнений.
Вариант отличается от первого тем, что в нем больше не используется запоминание префикса,
а при несовпадении в левой подстроке используется другое правило — шаблон сдвигается не на
позиций, а на некоторое количество , которое может быть меньше
глобального периода ().
Если взять в качестве значения , то алгоритм станет аналогичен
наивному алгоритму с квадратичной сложностью, поэтому число выбирается с
дополнительным ограничением: , где — критическое разбиение
. В таком виде число сравнений во втором варианте по прежнему будет
меньше , как и в первом, но вычисление глобального периода не
потребуется.
Запоминание префикса в данном случае не может быть применено из-за того, что
может быть меньше .
fun twoWay2(text: String, pattern: String): List<Int> {
val result = mutableListOf<Int>()
val (left, right) = factorize(pattern)
val q = maxOf(left.length, right.length) + 1
var pos = 0
while (pos + pattern.length <= text.length) {
// Сравнение правой части
val i = right.find { i -> pattern[i] != text[pos + i] }
if (i != null) {
pos = pos + i - right.first + 1
} else {
// Сравнение левой части
val match = left.reversed().all { j -> pattern[j] == text[pos + j] }
if (match) result.add(pos)
pos += q
}
}
return result
}
Доказательство корректности и максимального количества сравнений аналогично доказательству первого варианта и также есть в [1].
Конечный двусторонний алгоритм использует оба предыдущих варианта, выбирая нужный в зависимости от того, является ли левая подстрока суффиксом периодической части правой. В алгоритме, кроме критического разбиения строки шаблона на , где , также используется заранее вычисленный период только правой подстроки . Его вычисление может быть произведено без дополнительных затрат одновременно с вычислением критического разбиения.
data class Factorization(
val left: IntRange,
val right: IntRange,
val rightPeriod: Int
)
fun twoWay(text: String, pattern: String): List<Int> {
// factorize() разбивает строку шаблона pattern на 2 подстроки:
// left и right так, что |left| < per(pattern) и вычисляет период правой подстроки.
// left, right — закрытые диапазоны индексов соответствующих подстрок.
// rightPeriod — период правой подстроки
val (left, right, rightPeriod) = factorize(pattern)
// left — суффикс подстроки right[0..rightPeriod-1]
val leftIsSuffixOfRight = left.all { i ->
pattern[i] == pattern[right.first + rightPeriod - left.length + i]
}
return if (leftIsSuffixOfRight) {
// rightPeriod равен периоду строки шаблона per(pattern).
// Вызываем первый вариант алгоритма, используя left, right
// и rightPeriod в качестве глобального периода
cp1(text, pattern, left, right, rightPeriod)
} else {
// Вызываем второй вариант алгоритма, используя left, right
// и q.
val q = maxOf(left.length, right.length) + 1
cp2(text, pattern, left, right, q)
}
}
Доказательства корректности алгоритма, а также того, что соответствует условию в случае, если не является суффиксом , даны в [1].
Алгоритм выполняется за время с константной памятью, при этом выполняется не более сравнений.
Условие “” выполняется максимум за сравнений, вычисление периода и критического разбиения — сравнений, поэтому весь препроцессинг выполняется максимум за сравнений.
Пример работы алгоритма #
1. Возьмем шаблон . Критическое разбиение: . Поиск в тексте потребует сравнений. Несовпадение встречается в левой части, алгоритм сдвигает шаблон на 3 позиции.
2. Шаблон с глобальным периодом . Критическое разбиение: . Левая часть — суффикс периодической части правой, поэтому будет использован первый вариант алгоритма и сравнений при поиске в тексте .
3. Шаблон . Алгоритм будет использовать второй вариант и также выполнит сравнений при поиске в тексте .
Теорема о критическом разбиении #
Работа двустороннего алгоритма основывается на комбинаторных свойствах строки, обозначенных в теореме о критическом разбиении (Critical Factorization Theorem).
Теорема гласит, что любая строка имеет хотя бы одно критическое разбиение (т.е. такое разбиение, что ). Более того, подстрока может быть выбрана такой, что .
Впервые она была сформулирована и доказана в работах Césari и Vincent в 1978, а в современном виде — в работе J. Duval (1989). Теорема имеет несколько доказательств, но для двустороннего алгоритма было представлено свое собственное, на основе вычисления максимальных суффиксов.
Введем отношения порядка строк.
Лексикографический (или алфавитный) порядок определяется на множестве строк упорядоченного алфавита . Для двух строк строка тогда, когда выполняется одно из условий:
- — префикс
- строго меньше . Т. е. и можно представить как , где — строки из множества ; а — два символа, такие, что .
Обратный лексикографический порядок — лексикографический порядок на алфавите , порядок символов которого заменен на обратный. Т.е. строго меньше , если и можно представить как , где — строки из множества , а — два символа, такие, что .
Обозначим через прямой лексикографический порядок, а через — обратный.
Стоит отметить, что порядок — не инверсия порядка . Например, возьмем алфавит с . Для строк и верны оба равенства:
Более того, в пересечении порядков и совпадает только первое условие префикса. Т.е.оба выражения и верны только в том случае, если — префикс .
Ниже представлены все суффиксы строки , отсортированные в прямом и обратном порядке:
Суффиксы ≤ ⪯
ABAABAA A BAA
BAABAA AA BAABAA
AABAA AABAA A
ABAA ABAA ABAA
BAA ABAABAA ABAABAA
AA BAA AA
A BAABAA AABAA
Максимальный суффикс — это наибольшая подстрока строки в соответствии с заданным лексикографическим порядком. Обозначим его через . В примере выше равен для прямого порядка и для обратного.
Теорема
Пусть — непустая строка из символов алфавита и , где — лексикографически максимальный суффикс строки в соответствии с прямым порядком (), а — лексикографически максимальный суффикс в соответствии с обратным порядком (⪯). Если , то — критическое разбиение , а иначе — критическое разбиение . Более того, и .
Доказательство дано в [1]
Имея эту теорему, задача нахождения критического разбиения сводится к задаче нахождения максимальных суффиксов для прямого и обратного алфавитного порядка.
Вычисление критического разбиения #
Ниже представлен алгоритм вычисления критического разбиения и максимальных суффиксов строки.
data class Factorization(
val left: IntRange,
val right: IntRange,
val rightPeriod: Int
)
fun factorize(pattern: String): Factorization {
val naturalOrder = computeMaxSuffixAndPeriod(pattern, naturalOrder())
val reverseOrder = computeMaxSuffixAndPeriod(pattern, reverseOrder())
return if (naturalOrder.right.length <= reverseOrder.right.length)
naturalOrder else reverseOrder
}
fun computeMaxSuffixAndPeriod(pattern: String,
comparator: Comparator<Char>): Factorization…
computeMaxSuffixAndPeriod()
— функция, вычисляющая максимальный суффикс и период правой
подстроки.
Пример 1
Шаблон
Период :
Порядок | Макс. суффикс | Разбиение |
---|---|---|
Прямой | ||
Обратный |
Разбиение для прямого порядка — не критическое, его локальный период = 2 (). Разбиение для обратного порядка — критическое.
Пример 2
Шаблон
Период :
Порядок | Макс. суффикс | Разбиение |
---|---|---|
Прямой | ||
Обратный |
Оба разбиения критические.
Алгоритм вычисления максимального суффикса основан на использовании декомпозиции Линдона. Вместе с максимальным суффиксов в одном цикле вычисляется также период правой подстроки .
fun computeMaxSuffixAndPeriod(pattern: String, comparator: Comparator<in Char>): Factorization {
var maxSuffix = -1
var j = 0
var k = 1
var period = 1
while (j + k < pattern.length) {
val a = pattern[j + k]
val b = pattern[maxSuffix + k]
val abOrder = comparator.compare(a, b)
when {
// a < b
abOrder < 0 -> {
j += k
k = 1
period = j - maxSuffix
// a == b
abOrder == 0 -> {
if (k != period) {
k += 1
} else {
j += period
k = 1
}
// a > b
else -> {
maxSuffix = j
j = maxSuffix + 1
k = 1
period = 1
}
}
}
return Factorization(0 .. maxSuffix,
maxSuffix + 1 .. pattern.lastIndex,
period
)
}
Обозначим через максимальный суффикс строки в соответствии с алфавитным порядком.
Пусть , где:
, — подстроки
(длина — это период строки максимального суффикса)
— собственный префикс подстроки ;
— целое число .
Введем обозначения:
Отметим, что — это грань строки и , даже при .
Переменная maxSuffix
в алгоритме — позиция перед первым символом рассматриваемой строки
максимального суффикса.period
— период строки максимального суффикса, длина .j
… j+k
— подстрока в последнего встреченного вхождения в
подстроке максимального суффикса.
Алгоритм выполняется за время , при этом выполняется менее сравнений.
Рассмотрим на примере, как изменяется максимальный суффикс строки при добавлении в её конец символов .
Пусть . В соответствии с алфавитным порядком её максимальный суффикс равен
.
Макс. суффикс для строки : = . Слово
без границ.
Макс. суффикс для строки : = . У
строки такой же период, как и у .
Макс. суффикс для строки : .
Доказательство корректности алгоритма основывается на следующей лемме.
- Лемма
Возьмем строку , символ и символ такой, что — это префикс . Тогда значения будут равны следующим:
Условие , либо Доказательство дано в [1].
Реализация алгоритма #
Ниже приведена полная реализация алгоритма (на Kotlin, без учета суррогатных пар).
internal data class Factorization(
val pattern: String,
val left: IntRange,
val right: IntRange,
val rightPeriod: Int
) {
fun leftIsSuffixOfRight(): Boolean {
return left.all {
i -> pattern[i] == pattern[right.first + rightPeriod - left.length + i]
}
}
}
fun findSubstringTwoWay(text: String, pattern: String): List<Int> {
when {
text.isEmpty() -> return emptyList()
pattern.isEmpty() -> return text.indices.toList()
}
val result: MutableList<Int> = mutableListOf()
val factorization = factorize(pattern)
if (factorization.leftIsSuffixOfRight()) {
// CP1
val (_, left, right, period) = factorization
var pos = 0
var memPrefix = -1
while (pos + pattern.length <= text.length) {
// Сравнение правой части
val i = (maxOf(right.first, memPrefix + 1)..right.last).find { i ->
pattern[i] != text[pos + i]
}
if (i != null) {
pos = pos + i - right.first + 1
memPrefix = -1
} else {
// Сравнение левой части
val match = (left.last downTo memPrefix + 1).all { j ->
pattern[j] == text[pos + j]
}
if (match) result.add(pos)
pos += period
memPrefix = pattern.length - period - 1
}
}
} else {
// CP2
val left = factorization.left.reversed()
val right = factorization.right
val q = maxOf(left.length, right.length) + 1
var pos = 0
while (pos + pattern.length <= text.length) {
// Сравнение правой части
val i = right.find { i -> pattern[i] != text[pos + i] }
if (i != null) {
pos = pos + i - right.first + 1
} else {
// Сравнение левой части
val match = left.all { j -> pattern[j] == text[pos + j] }
if (match) result.add(pos)
pos += q
}
}
}
return result
}
private fun factorize(pattern: String): Factorization {
val naturalOrder = computeMaxSuffixAndPeriod(pattern, naturalOrder())
val reverseOrder = computeMaxSuffixAndPeriod(pattern, reverseOrder())
return if (naturalOrder.right.length <= reverseOrder.right.length)
naturalOrder else reverseOrder
}
internal fun computeMaxSuffixAndPeriod(pattern: String,
comparator: Comparator<in Char>): Factorization {
var maxSuffix = -1
var j = 0
var k = 1
var period = 1
while (j + k < pattern.length) {
val a = pattern[j + k]
val b = pattern[maxSuffix + k]
val abOrder = comparator.compare(a, b)
when {
// a < b
abOrder < 0 -> {
j += k
k = 1
period = j - maxSuffix
}
// a == b
abOrder == 0 -> {
if (k != period) {
k += 1
} else {
j += period
k = 1
}
}
// a > b
else -> {
maxSuffix = j
j = maxSuffix + 1
k = 1
period = 1
}
}
}
return Factorization(
pattern,
0..maxSuffix,
maxSuffix + 1..pattern.lastIndex,
period
)
}
Она же в репозитории: twoway.kt.
Источники #
- CROCHEMORE, M., RYTTER, W., 1994, Text Algorithms, Oxford University Press. (ссылка)
- CROCHEMORE M., PERRIN D., 1991, Two-way string-matching, Journal of the ACM 38(3):651-675. (ссылка)
- Wikipedia: Two-way string-matching algorithm (ссылка).
- Викиконспекты ИТМО: Двусторонний алгоритм (ссылка).
- Кутенин Данила М., 2019. Умные алгоритмы обработки строк в ClickHouse (ссылка)
- Galil-Seiferas algorithm (ссылка)