Поиск подстроки: различия между версиями

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
[непроверенная версия][отпатрулированная версия]
Содержимое удалено Содержимое добавлено
Преамбула: перенесена на КУ
 
(не показано 140 промежуточных версий 32 участников)
Строка 1: Строка 1:
<noinclude>{{к удалению|2021-12-05}}</noinclude>
''Поиск информации'' — одно из основных использований компьютера. Одна из простейших задач поиска информации — '''поиск точно заданной подстроки в строке'''. Тем не менее, она чрезвычайно важна.
'''Поиск подстроки в строке''' — одна из простейших задач [[Поиск информации|поиска информации]]. Применяется в виде встроенной функции в [[текстовый редактор|текстовых редакторах]], [[СУБД]], [[поисковая машина|поисковых машинах]], [[язык программирования|языках программирования]] и т. п.


В задачах поиска традиционно принято обозначать шаблон поиска как ''needle'' ({{lang-en|«иголка»}}), а строку, в которой ведётся поиск — как ''haystack'' ({{lang-en|«стог сена»}}). Также обозначим через Σ алфавит, на котором проводится поиск.
В задачах поиска традиционно принято обозначать шаблон поиска как ''needle'' ({{tr-en|иголка}}), а строку, в которой ведётся поиск — как ''haystack'' ({{tr-en|[[wikt:искать иголку в стоге сена|стог сена]]}}). Также обозначим через Σ алфавит, на котором проводится поиск.{{Нет АИ|4|1|2016}}


== Несостоятельность простейшего алгоритма ==
== Несостоятельность примитивного алгоритма ==
Если считать, что строки нумеруются с 1, простейший алгоритм ({{lang-en|brute force algorithm, naïve algorithm}}) выглядит так.

Если считать, что строки нумеруются с 1, простейший алгоритм выглядит так.


for i=0...|haystack|-|needle|
for i=0...|haystack|-|needle|
for j=1...|needle|
for j=0...|needle|
if haystack[i+j]<>needle[j]
if haystack[i+j + 1]<>needle[j]
then goto 1
then goto 1
output("Найдено: ", i+1)
output("Найдено: ", i+1)
1:
1:


Простейший алгоритм поиска даже в ''лучшем'' случае проводит |''haystack''|&minus;|''needle''|+1 сравнение; если же есть много частичных совпадений, скорость снижается до ''O''(|''haystack''|·|''needle''|).
Простейший алгоритм поиска даже в ''лучшем'' случае проводит |''haystack''||''needle''|+1 сравнение; если же есть много частичных совпадений, скорость снижается до ''O''(|''haystack''|·|''needle''|).


Доказано, что примитивный алгоритм отрабатывает в среднем 2''h'' сравнений<ref>[http://www-igm.univ-mlv.fr/~lecroq/string/node3.html Brute force algorithm] {{Wayback|url=http://www-igm.univ-mlv.fr/~lecroq/string/node3.html |date=20090121190217 }}{{ref-en}}</ref>.
== Для чего нужно так много алгоритмов? ==


== Для чего нужно так много алгоритмов? ==
Программисту приходится выбирать нужный алгоритм в зависимости от таких факторов.
На сегодняшний день существует огромное разнообразие алгоритмов поиска подстроки. Программисту приходится выбирать подходящий в зависимости от таких факторов.
# Нужна ли вообще оптимизация, или хватает примитивного алгоритма? Как правило, именно его реализуют стандартные библиотеки языков программирования.
# «Враждебность» пользователя. Другими словами: будет ли пользователь намеренно задавать данные, на которых алгоритм будет медленно работать? Существуют очень простые алгоритмы, оценка которых ''O''(|''haystack''|·|''needle''|) в худшем случае, но на «обычных» данных количество сравнений намного меньше |''haystack''|. Только в [[1990-е]] годы были созданы алгоритмы, дающие сложность ''O''(|''haystack''|) в худшем случае и меньше |''haystack''| в среднем.
# «Враждебность» пользователя. Другими словами: будет ли пользователь намеренно задавать данные, на которых алгоритм будет медленно работать? Существуют очень простые алгоритмы, оценка которых ''O''(|''haystack''|·|''needle''|) в худшем случае, но на «обычных» данных количество сравнений намного меньше |''haystack''|. Только в [[1990-е]] годы были созданы алгоритмы, дающие сложность ''O''(|''haystack''|) в худшем случае и меньше |''haystack''| в среднем.
# [[Грамматика (наука)|Грамматика]] языка может быть недружественной к тем или иным эвристикам, которые ускоряют поиск «в среднем».
# Архитектура процессора. Некоторые процессоры имеют автоинкрементные или [[SIMD]]-операции, которые позволяют быстро сравнить два участка ОЗУ (например, <code>rep cmpsd</code> на [[x86]]). На таких процессорах заманчиво применить алгоритм, который просто бы сравнивал ''needle'' с ''haystack'' — разумеется, не во всех позициях.
# [[Архитектура процессора]]. Некоторые процессоры имеют [[автоинкрементная операция|автоинкрементные]] или [[SIMD]]-операции, которые позволяют быстро сравнить два участка ОЗУ (например, <code>rep cmpsd</code> на [[x86]]). На таких процессорах заманчиво применить алгоритм, который просто бы сравнивал ''needle'' с ''haystack'' — разумеется, не во всех позициях.
# Размер алфавита. Многие алгоритмы (особенно основанные на сравнении с конца) имеют эвристики, связанные с несовпавшим символом. На больших алфавитах таблица символов будет занимать много памяти, на малых — соответствующая эвристика будет неэффективной.
# Размер алфавита. Многие алгоритмы (особенно основанные на сравнении с конца) имеют эвристики, связанные с несовпавшим символом. На больших алфавитах таблица символов будет занимать много памяти, на малых — соответствующая эвристика будет неэффективной.
# Возможность проиндексировать ''haystack''. Если таковая есть, поиск серьёзно ускорится.
# Возможность проиндексировать ''haystack''. Если таковая есть, поиск серьёзно ускорится.
# Требуется ли одновременный поиск нескольких строк? Некоторые алгоритмы (например, [[алгоритм Ахо-Корасик]]) позволяют это.
# Требуется ли одновременный поиск нескольких строк? Приблизительный поиск? Побочные свойства некоторых алгоритмов ([[алгоритм Ахо-Корасик|Ахо-Корасик]], [[Двоичный алгоритм поиска подстроки|двоичного алгоритма]]) позволяют такое.

Как правило, в [[текстовый редактор|текстовом редакторе]] достаточно взять самый простой эвристический алгоритм наподобие [[алгоритм Бойера — Мура — Хорспула|Бойера — Мура — Хорспула]] — даже очень медленный ПК справится с поиском за доли секунды. Если же объём текста измеряется гигабайтами, либо поиск запущен на сервере, который обрабатывает множество запросов — приходится выбирать наиболее удачный алгоритм из доступных. Например, программы [[Определение плагиата|определения плагиата]] осуществляют онлайн-проверку, используя алгоритмы поиска подстроки среди большого количества документов, хранящихся в собственной базе.


== Алгоритмы ==
== Алгоритмы ==
Для сокращения обозначим:
* |Σ|=σ — размер алфавита.
* |''haystack''|=''H'' — длина строки, в которой ведётся поиск.
* |''needle''|=''n'' — длина шаблона поиска.

Вычислительная сложность определяется ''до первого совпадения''. '''Жирным шрифтом''' выделены важнейшие с практической точки зрения алгоритмы.

=== Основанные на сравнении как «[[Чёрный ящик|чёрном ящике]]» ===
Во всех этих алгоритмах точка, где «иголка» не совпала со «стогом сена», не участвует в принятии решения. Это позволяет использовать стандартные функции [[memcmp|сравнения участков памяти]], зачастую оптимизированные на ассемблерном уровне под тот или иной процессор.

К этой категории относится и примитивный алгоритм поиска.

{| class="wikitable"
! rowspan=2 | Название
! rowspan=2 | Предв. обработка
! colspan=2 | Сложность
! rowspan=2 | Примечания
|-
! типичная
! макс.
|-
| '''Примитивный алгоритм'''
| Нет
| 2''H''
| ''O''(''Hn'')
|
|-
| '''[[Алгоритм Бойера — Мура — Хорспула]]'''
| ''O''(''n''+σ)
| ~ 2''H'' / σ<ref>{{Cite web |url=http://www-igm.univ-mlv.fr/~lecroq/string/node18.html#SECTION00180 |title=Horspool algorithm<!-- Заголовок добавлен ботом --> |access-date=2013-05-02 |archive-date=2012-08-29 |archive-url=https://web.archive.org/web/20120829203936/http://www-igm.univ-mlv.fr/~lecroq/string/node18.html#SECTION00180 |deadlink=no }}</ref>
| ''O''(''Hn'')
| Упрощённый до предела алгоритм Бойера — Мура; использует только видоизменённую эвристику стоп-символа — за стоп-символ всегда берётся символ ''haystack'', расположенный напротив последнего символа ''needle''.
|-
| '''[[Алгоритм быстрого поиска подстроки|Алгоритм быстрого поиска]]'''<br>[[Алгоритм Санди]]
| ''O''(''n''+σ)
| <''H''
| ''O''(''Hn'')
| Также использует исключительно эвристику стоп-символа — но за стоп-символ берётся символ ''haystack'', идущий за последним символом ''needle''.
|}

=== Основанные на сравнении с начала ===
Это семейство алгоритмов страдает невысокой скоростью на «хороших» данных, что компенсируется отсутствием регрессии на «плохих».

{| class="wikitable"
! rowspan=2 | Название
! rowspan=2 | Предв. обработка
! colspan=2 | Сложность
! rowspan=2 | Примечания
|-
! типичная
! макс.
|-
| [[Алгоритм Рабина-Карпа]]
| ''O''(''n'')
| <''H''+''n''
| ''O''(''Hn'')
| [[Хеширование]] позволяет серьёзно снизить сложность в среднем
|-
| {{nobr|'''Автоматный алгоритм'''}}<br>[[Алгоритм Ахо-Корасик]]
| ''O''(''n''σ)
| colspan=2 align=center | = ''H''
| Строит [[конечный автомат]], который распознаёт язык, состоящий из одной-единственной строки. После небольшой модификации позволяет за один проход по ''haystack'' найти одну строку из нескольких.
|-
| '''[[Алгоритм Кнута-Морриса-Пратта]]'''
| ''O''(''n'')
| colspan=2 align=center | ≤ 2''H''
| Один из первых алгоритмов с линейной оценкой в худшем случае. Модификация алгоритма Ахо-Корасик, строящая автомат неявно на основе префикс-функции.
|-
| [[Алгоритм Апостолико-Крошмора]]
| ''O''(''n'')
| < ''H''
| ≤1,5''H''
|
|-
| '''[[Алгоритм Shift-Or]]'''<br>[[Bitap-алгоритм]]<br>[[Двоичный алгоритм поиска подстроки|Двоичный алгоритм]]
| ''O''(''n''+σ)
| colspan=2 align=center | {{nobr|1==''H''·[[потолок числа|ceil]](''n/w'')}}
| Эффективен, если размер ''needle'' (в символах) не больше размера [[машинное слово|машинного слова]] (в битах, обозначен как ''w''). Легко переделывается на приблизительный поиск, поиск нескольких строк.
|}

=== Основанные на сравнении с конца ===
В этом семействе алгоритмов ''needle'' движется по ''haystack'' слева направо, но сравнение этих строк друг с другом проводится справа налево. Сравнение справа налево позволяет в случае несовпадения сдвинуть ''needle'' не на одну позицию, а на несколько.

{| class="wikitable"
! rowspan=2 | Название
! rowspan=2 | Предв. обработка
! colspan=2 | Сложность
! rowspan=2 | Примечания
|-
! типичная
! макс.
|-
| '''[[Алгоритм Бойера — Мура]]'''
| ''O''(''n''+σ)
| <''H''
| ''O''(''Hn'')
| Стандартный алгоритм поиска подстроки в строке. Считается наиболее эффективным алгоритмом общего назначения.<ref>{{Cite web |url=http://www-igm.univ-mlv.fr/~lecroq/string/node14.html#SECTION00140 |title=Boyer-Moore algorithm<!-- Заголовок добавлен ботом --> |access-date=2013-05-02 |archive-date=2013-02-06 |archive-url=https://web.archive.org/web/20130206123458/http://www-igm.univ-mlv.fr/~lecroq/string/node14.html#SECTION00140 |deadlink=no }}</ref>
|-
| [[Алгоритм Чжу-Такаоки]]
| ''O''(''n''+σ²)
| <''H''
| ''O''(''Hn'')
| Алгоритм Бойера — Мура, оптимизированный под короткие алфавиты
|-
| [[Алгоритм Апостолико-Джанкарло]]
| ''O''(''n''+σ)
| <''H''
| ≤1,5''H''
| Одна из первых попыток получить <''H'' в типичном случае и ''O''(''H'') в худшем. Очень сложен в реализации.
|-
| [[Турбо-алгоритм Бойера — Мура]]
| ''O''(''n''+σ)
| <''H''
| ≤2''H''
| Один из наиболее эффективных алгоритмов, не дающих регрессии на «плохих» данных
|}

=== Проводящие сравнение в необычном порядке ===
{| class="wikitable"
! rowspan=2 | Название
! rowspan=2 | Предв. обработка
! colspan=2 | Сложность
! rowspan=2 | Примечания
|-
! типичная
! макс.
|-
| Непримитивный алгоритм
| const
| <''H''
| ''O''(''Hn'')
| Простой алгоритм, сравнивающий второй символ, затем начиная с третьего в режиме «чёрного ящика», и, наконец, первый. При {{nobr|''n''[1]≠''n''[2]}}<ref>Напомним, символы нумеруются с 1, как в [[Паскаль (язык программирования)|Паскале]].</ref> и несовпадении на второй-третьей стадии — сдвиг на 2 вправо.
|-
| Алгоритм Райты<br>Алгоритм Бойера — Мура — Хорспула — Райты
| ''O''(''n''+σ)
| <''H''
| ''O''(''Hn'')
| [[Эмпирический алгоритм]], оптимизированный под [[английский язык|английские]] тексты. Сравнивает последний символ, потом первый, потом средний, потом все остальные; при несовпадении — сдвиг по Хорспулу.
|}

== См. также ==
* [[Подстрока]]
* [[Список алгоритмов#Алгоритмы на строках|Алгоритмы на строках]]
* [[Сопоставление с образцом]]
* [[Алгоритмы: построение и анализ]]

== Примечания ==
{{примечания}}


== Литература ==
{{compu-stub}}
* {{книга
|автор = {{nobr|Гасфилд Д.}}
|заглавие = Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология
|оригинал = Algorithms on String, Trees, and Sequences. Computer Science and Computational Biology
|ссылка =
|ответственный = Пер. с англ. И. В. Романовского
|издание = 2-е изд
|место = СПб.
|издательство = Невский Диалект
|год = 2003
|страниц = 654
|isbn = 5-7940-0103-8, 5-94157-321-9, 0-521-58519-8
}}
* {{книга
|автор = {{nobr|Смит Б.}}
|заглавие = Методы и алгоритмы вычислений на строках
|оригинал = Computing Patterns in Strings
|ссылка =
|ответственный =
|издание =
|место = М.
|издательство = Вильямс
|год = 2006
|страниц = 496
|isbn = 5-8459-1081-1, 0-201-39839-7
}}
* {{книга
|автор = {{nobr|Окулов С. М.}}
|заглавие = Алгоритмы обработки строк
|ссылка =
|ответственный =
|издание =
|место = М.
|издательство = Бином
|год = 2013
|страниц = 255
|isbn = 978-5-9963016-2-1
}}


== Ссылки ==
[[Категория:Поиск подстроки|*]]
* [http://www-igm.univ-mlv.fr/~lecroq/string/index.html Большая подборка алгоритмов поиска подстроки]{{ref-en}}
{{Строки}}{{rq|style|wikify|check}}


[[Категория:Поиск подстроки| ]]
[[en:String searching algorithm]]

Текущая версия от 11:43, 25 августа 2023

Поиск подстроки в строке — одна из простейших задач поиска информации. Применяется в виде встроенной функции в текстовых редакторах, СУБД, поисковых машинах, языках программирования и т. п.

В задачах поиска традиционно принято обозначать шаблон поиска как needle (с англ. — «иголка»), а строку, в которой ведётся поиск — как haystack (с англ. — «стог сена»). Также обозначим через Σ алфавит, на котором проводится поиск.[источник не указан 3292 дня]

Несостоятельность примитивного алгоритма

[править | править код]

Если считать, что строки нумеруются с 1, простейший алгоритм (англ. brute force algorithm, naïve algorithm) выглядит так.

for i=0...|haystack|-|needle|
  for j=0...|needle|
    if haystack[i+j + 1]<>needle[j] 
      then goto 1
  output("Найдено: ", i+1)
  1:

Простейший алгоритм поиска даже в лучшем случае проводит |haystack|−|needle|+1 сравнение; если же есть много частичных совпадений, скорость снижается до O(|haystack|·|needle|).

Доказано, что примитивный алгоритм отрабатывает в среднем 2h сравнений[1].

Для чего нужно так много алгоритмов?

[править | править код]

На сегодняшний день существует огромное разнообразие алгоритмов поиска подстроки. Программисту приходится выбирать подходящий в зависимости от таких факторов.

  1. Нужна ли вообще оптимизация, или хватает примитивного алгоритма? Как правило, именно его реализуют стандартные библиотеки языков программирования.
  2. «Враждебность» пользователя. Другими словами: будет ли пользователь намеренно задавать данные, на которых алгоритм будет медленно работать? Существуют очень простые алгоритмы, оценка которых O(|haystack|·|needle|) в худшем случае, но на «обычных» данных количество сравнений намного меньше |haystack|. Только в 1990-е годы были созданы алгоритмы, дающие сложность O(|haystack|) в худшем случае и меньше |haystack| в среднем.
  3. Грамматика языка может быть недружественной к тем или иным эвристикам, которые ускоряют поиск «в среднем».
  4. Архитектура процессора. Некоторые процессоры имеют автоинкрементные или SIMD-операции, которые позволяют быстро сравнить два участка ОЗУ (например, rep cmpsd на x86). На таких процессорах заманчиво применить алгоритм, который просто бы сравнивал needle с haystack — разумеется, не во всех позициях.
  5. Размер алфавита. Многие алгоритмы (особенно основанные на сравнении с конца) имеют эвристики, связанные с несовпавшим символом. На больших алфавитах таблица символов будет занимать много памяти, на малых — соответствующая эвристика будет неэффективной.
  6. Возможность проиндексировать haystack. Если таковая есть, поиск серьёзно ускорится.
  7. Требуется ли одновременный поиск нескольких строк? Приблизительный поиск? Побочные свойства некоторых алгоритмов (Ахо-Корасик, двоичного алгоритма) позволяют такое.

Как правило, в текстовом редакторе достаточно взять самый простой эвристический алгоритм наподобие Бойера — Мура — Хорспула — даже очень медленный ПК справится с поиском за доли секунды. Если же объём текста измеряется гигабайтами, либо поиск запущен на сервере, который обрабатывает множество запросов — приходится выбирать наиболее удачный алгоритм из доступных. Например, программы определения плагиата осуществляют онлайн-проверку, используя алгоритмы поиска подстроки среди большого количества документов, хранящихся в собственной базе.

Для сокращения обозначим:

  • |Σ|=σ — размер алфавита.
  • |haystack|=H — длина строки, в которой ведётся поиск.
  • |needle|=n — длина шаблона поиска.

Вычислительная сложность определяется до первого совпадения. Жирным шрифтом выделены важнейшие с практической точки зрения алгоритмы.

Основанные на сравнении как «чёрном ящике»

[править | править код]

Во всех этих алгоритмах точка, где «иголка» не совпала со «стогом сена», не участвует в принятии решения. Это позволяет использовать стандартные функции сравнения участков памяти, зачастую оптимизированные на ассемблерном уровне под тот или иной процессор.

К этой категории относится и примитивный алгоритм поиска.

Название Предв. обработка Сложность Примечания
типичная макс.
Примитивный алгоритм Нет 2H O(Hn)
Алгоритм Бойера — Мура — Хорспула O(n+σ) ~ 2H / σ[2] O(Hn) Упрощённый до предела алгоритм Бойера — Мура; использует только видоизменённую эвристику стоп-символа — за стоп-символ всегда берётся символ haystack, расположенный напротив последнего символа needle.
Алгоритм быстрого поиска
Алгоритм Санди
O(n+σ) <H O(Hn) Также использует исключительно эвристику стоп-символа — но за стоп-символ берётся символ haystack, идущий за последним символом needle.

Основанные на сравнении с начала

[править | править код]

Это семейство алгоритмов страдает невысокой скоростью на «хороших» данных, что компенсируется отсутствием регрессии на «плохих».

Название Предв. обработка Сложность Примечания
типичная макс.
Алгоритм Рабина-Карпа O(n) <H+n O(Hn) Хеширование позволяет серьёзно снизить сложность в среднем
Автоматный алгоритм
Алгоритм Ахо-Корасик
O(nσ) = H Строит конечный автомат, который распознаёт язык, состоящий из одной-единственной строки. После небольшой модификации позволяет за один проход по haystack найти одну строку из нескольких.
Алгоритм Кнута-Морриса-Пратта O(n) ≤ 2H Один из первых алгоритмов с линейной оценкой в худшем случае. Модификация алгоритма Ахо-Корасик, строящая автомат неявно на основе префикс-функции.
Алгоритм Апостолико-Крошмора O(n) < H ≤1,5H
Алгоритм Shift-Or
Bitap-алгоритм
Двоичный алгоритм
O(n+σ) =H·ceil(n/w) Эффективен, если размер needle (в символах) не больше размера машинного слова (в битах, обозначен как w). Легко переделывается на приблизительный поиск, поиск нескольких строк.

Основанные на сравнении с конца

[править | править код]

В этом семействе алгоритмов needle движется по haystack слева направо, но сравнение этих строк друг с другом проводится справа налево. Сравнение справа налево позволяет в случае несовпадения сдвинуть needle не на одну позицию, а на несколько.

Название Предв. обработка Сложность Примечания
типичная макс.
Алгоритм Бойера — Мура O(n+σ) <H O(Hn) Стандартный алгоритм поиска подстроки в строке. Считается наиболее эффективным алгоритмом общего назначения.[3]
Алгоритм Чжу-Такаоки O(n+σ²) <H O(Hn) Алгоритм Бойера — Мура, оптимизированный под короткие алфавиты
Алгоритм Апостолико-Джанкарло O(n+σ) <H ≤1,5H Одна из первых попыток получить <H в типичном случае и O(H) в худшем. Очень сложен в реализации.
Турбо-алгоритм Бойера — Мура O(n+σ) <H ≤2H Один из наиболее эффективных алгоритмов, не дающих регрессии на «плохих» данных

Проводящие сравнение в необычном порядке

[править | править код]
Название Предв. обработка Сложность Примечания
типичная макс.
Непримитивный алгоритм const <H O(Hn) Простой алгоритм, сравнивающий второй символ, затем начиная с третьего в режиме «чёрного ящика», и, наконец, первый. При n[1]≠n[2][4] и несовпадении на второй-третьей стадии — сдвиг на 2 вправо.
Алгоритм Райты
Алгоритм Бойера — Мура — Хорспула — Райты
O(n+σ) <H O(Hn) Эмпирический алгоритм, оптимизированный под английские тексты. Сравнивает последний символ, потом первый, потом средний, потом все остальные; при несовпадении — сдвиг по Хорспулу.

Примечания

[править | править код]
  1. Brute force algorithm Архивная копия от 21 января 2009 на Wayback Machine (англ.)
  2. Horspool algorithm. Дата обращения: 2 мая 2013. Архивировано 29 августа 2012 года.
  3. Boyer-Moore algorithm. Дата обращения: 2 мая 2013. Архивировано 6 февраля 2013 года.
  4. Напомним, символы нумеруются с 1, как в Паскале.

Литература

[править | править код]
  • Гасфилд Д. Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология = Algorithms on String, Trees, and Sequences. Computer Science and Computational Biology / Пер. с англ. И. В. Романовского. — 2-е изд. — СПб.: Невский Диалект, 2003. — 654 с. — ISBN 5-7940-0103-8, 5-94157-321-9, 0-521-58519-8.
  • Смит Б. Методы и алгоритмы вычислений на строках = Computing Patterns in Strings. — М.: Вильямс, 2006. — 496 с. — ISBN 5-8459-1081-1, 0-201-39839-7.
  • Окулов С. М. Алгоритмы обработки строк. — М.: Бином, 2013. — 255 с. — ISBN 978-5-9963016-2-1.