Поиск, замена и удаление подстрок
Основные понятия
Подстрока - непрерывная последовательность символов, выделенная внутри строки. В задачах поиска мы обычно рассматриваем два объекта: текст и шаблон, где шаблон проверяется на вхождение в текст.
Текст - длинная строка, в которой производится поиск. Его длину часто обозначают как .
Шаблон - строка, которую нужно найти внутри текста. Длина шаблона обозначается как . Вхождение шаблона — это позиция в тексте, с которой совпадение длиной шаблона начинается; допустимые позиции описываются неравенством .
Пример: в слове «banana» шаблон «ana» встречается в нескольких позициях — набор стартовых индексов таких вхождений можно записать как .
Наивный поиск и его ограничения
Самый простой метод — наивное сравнение: сдвигаем шаблон по тексту и на каждой позиции сравниваем символы по порядку. В худшем случае этот подход выполняет большое число сравнений; классическое оценивание даёт временную сложность — это означает, что при больших текстах и шаблонах реализация будет медленной.
На практике наивный метод может быть приемлем, если шаблон короткий или входные данные малы. Но для промышленных задач с большими данными или при многократных запросах по одному и тому же тексту стоит выбирать более эффективные алгоритмы.
Иллюстрация работы: если текст длины , а шаблон длины , наивный алгоритм потенциально выполнит сравнение для каждой пар позиции и символа — это и приводит к оценке .
Алгоритм Кнута — Морриса — Пратта (KMP)
KMP оптимизирует наивный подход, исключая повторное сравнение префиксов. Ключевая структура — префикс-функция, которая для каждой позиции шаблона даёт длину наибольшего собственного префикса, являющегося также суффиксом строки до этой позиции. Формально это выражается как .
Преимущество KMP в том, что общее время работы состоит из построения префикс-функции и прохода по тексту; суммарная оценка времени — . Построение префикс-функции само по себе занимает {FORMULA_20} времени и не требует дополнительных дорогостоящих итераций.
Применение KMP полезно, когда нужно искать один и тот же шаблон в нескольких текстах или при анализе потоковых данных, где важна гарантированная верхняя оценка времени поиска.
Хеширование и Rabin–Karp
Идея Rabin–Karp — вычислять числовой хеш для каждого окна длиной шаблона и сравнивать хеши вместо посимвольного сравнения. Чаще всего используется полиномиальная свёртка по модулю; одна из стандартных формул хеша записывается как .
Благодаря использованию «скользящего» хеша вычисление хеша для следующего окна делается за O(1) операций с помощью формулы обновления . Это даёт эффективный проверочный фильтр: редко совпадающие хеши позволяют пропускать посимвольную проверку, но нужно учитывать вероятность ложного совпадения, примерно равную (зависит от выбираемого модуля).
Rabin–Karp удобен при одновременном поиске множества шаблонов (вычисляются хеши всех шаблонов) или при поиске в потоковых данных. Однако важно правильно выбирать базу и модуль, чтобы снизить число коллизий.
Boyer–Moore и эвристики ускорения
Boyer–Moore использует эвристики «плохого символа» и «хорошего суффикса», чтобы перескакивать вперёд более чем на один символ. Одним из ключевых правил смещения является выражение для плохого символа: — это помогает избежать многих проверок в практике.
В среднем для крупных алфавитов и случайных данных алгоритм часто работает быстрее и даёт сложность порядка , то есть с ростом длины шаблона число сравнений существенно снижается.
Бoyer–Moore особенно эффективен при поиске в тексте большого объёма и относительно коротких шаблонах, а также в ситуациях, где заранее можно предобработать шаблон и таблицы смещений.
Замена подстрок: стратегии
Задача замены состоит в нахождении всех вхождений шаблона и подставлении на их место другой строки. В простейшем случае можно сначала найти все стартовые индексы, затем выполнить замену. Если количество вхождений обозначить как , итоговая стоимость операции будет зависеть от числа замен и от длины вставляемой строки.
Важно различать замену невложенных (непересекающихся) вхождений и замену при допущении перекрытий. При пересекающихся шаблонах нужно заранее решить, разрешать ли пересечение; например, для шаблона «aa» в строке «aaaa» возможные стартовые позиции — . Алгоритм замены должен учитывать выбранную семантику.
Практическая реализация: если использовать наивное перечисление вхождений и выполнять операции вставки в середине строки, сложность может вырасти до в худшем случае из‑за постоянного копирования при изменении длины строки.
Удаление подстрок и сжатие текста
Удаление — частный случай замены, когда строку-замену выбирают пустой. Для массовых удалений эффективнее собирать результат в новый буфер или выполнять операцию in-place со сдвигами. Простейший метод со сдвигом символов имеет стоимость, пропорциональную объёму текста, то есть порядка .
Более экономный подход — метод «двух указателей»: один указатель проходит исходную строку, второй пишет в результирующее место только те символы, которые не удаляются. Такая стратегия выполняется за время и не требует дополнительной памяти, кроме константной.
Если работаете с символами в многобайтовой кодировке (UTF‑8), важно учитывать количество кодовых точек: длина в байтах и длина в кодовых точках различны, обозначение количества кодовых точек можно записать как . При обработке юникода операции по индексам должны работать с позицией в кодовых точках, а не в байтах.
Регулярные выражения и расширенные возможности
Регулярные выражения дают мощный инструмент для поиска и замены: они поддерживают шаблоны, группировки и обратные ссылки. При замене с использованием групп обычно применяют ссылки на захваченные группы, например вставка первой группы обозначается как в синтаксисе некоторых реализаций.
Регекспы удобны для сложных трансформаций (удаление повторов, нормализация пробелов, перестановки частей строки), но требуют внимания к производительности: сложные выражения могут быть медленнее специализированных алгоритмов поиска или приводить к экспоненциальной работе при некорректных конструкциях.
При выборе между собственными алгоритмами и использованием регулярных выражений оценивайте частоту операций, размеры входных данных и требования к читаемости кода. Для массовой обработки логов и больших файлов часто эффективнее комбинировать предобработку на основе KMP или хеша с последующими трансформациями регексами.
Практические советы и распространённые ошибки
1) Всегда тестируйте поведение на граничных случаях: пустой шаблон, пустой текст, совпадение в начале и в конце. 2) Не забывайте про кодировку: при работе с UTF‑8 индексация по байтам и по символам различается — см. .
3) При замене большого числа маленьких подстрок предпочтительнее собирать результат в буфер, а не выполнять множество вставок в середине строки — это уменьшает накладные расходы на копирования и даёт поведение ближе к линейному времени. 4) Отдавайте предпочтение адаптированным библиотечным функциям, если они соответствуют требованиям по семантике (пересечения, чувствительность к регистру и т. п.).
Итог: выбирайте алгоритм под задачу. Для гарантированной производительности — KMP, для практической скорости на случайных данных — Boyer–Moore, для поиска множества шаблонов — Aho–Corasick (комбинированный автомат), для гибких замен — регулярные выражения; но всегда учитывайте ограничения памяти и особенности кодировки.