190K звёзд на GitHub, а внутри brute-force: аудит реализации Boyer-Moore

Недавно я наткнулся на репозиторий с 190 000 звезд на GitHub. Он назывался «boyer-moore». Алгоритм Бойера-Мура — это легенда. Он должен летать, пропуская куски текста, а не перебирать всё подряд. Открываю код. И вижу обычный перебор. Да, с обратным сравнением. Но без единой эвристики. Просто цикл в цикле. Это как купить Ferrari, а под капотом обнаружить двигатель от газонокосилки. Тысячи разработчиков поставили звезду за то, что по факту является учебной подделкой. Давайте разберемся, как так вышло и почему это опасно.
Что такое настоящий Бойер-Мур? (Спойлер: это не про «тупой» перебор)
Алгоритм, придуманный в 1977 году, решает задачу поиска подстроки. Его гениальность в том, что он смотрит на текст справа налево. И если символ в тексте не совпал с последним символом шаблона, он не сдвигается на одну позицию, а прыгает сразу на несколько. Это возможно благодаря двум правилам: стоп-символ и хороший суффикс. Суть в том, что мы заранее (на этапе препроцессинга) строим таблицы сдвигов. Например, если мы ищем слово «машина», а в тексте нам встретилась буква «я», которой нет в слове, алгоритм понимает: «Ага, можно сдвинуть шаблон на всю его длину, потому что «я» там точно не появится». В среднем это дает сложность O(n), где n — длина текста. На практике это работает молниеносно, особенно на больших объемах.
Анатомия подделки: как 190 000 звезд обманули всех
Я открыл тот самый «звездный» репозиторий. Вот суть того, что я увидел (упрощенно на Python):
def boyer_moore(text, pattern):
i = 0
while i <= len(text) - len(pattern):
j = len(pattern) - 1
while j >= 0 and pattern[j] == text[i+j]:
j -= 1
if j < 0: return i
i += 1 # Вот это — главная ошибка!
return -1
Видите эту строку i += 1? Это означает, что при любом несовпадении мы сдвигаемся ровно на один символ. Нет никаких таблиц. Нет расчета сдвига. Это чистый brute-force (полный перебор). Единственное, что напоминает Бойера-Мура — это то, что сравнение идет с конца. Но это не делает алгоритм быстрым. Это просто неправильная реализация, которая выдает себя за сложный алгоритм. Сложность такой «подделки» в худшем случае — O(n*m), где m — длина шаблона. То есть она может работать в разы медленнее, чем обещает.
Почему это не просто «учебный пример», а опасность?
Многие скажут: «Ну и что? Это же просто код на GitHub». Но проблема глубже. Разработчики, особенно начинающие, часто используют такие репозитории как «черный ящик». Они копируют код, веря, что раз у него 190 000 звезд, значит, он качественный. Представьте, что эта реализация попадает в:
- Текстовый редактор — поиск по большому файлу будет тормозить.
- Антивирус — сканирование сигнатур станет провалом по скорости.
- Компилятор — лексический анализ замедлится.
Я видел своими глазами, как подобный код попал в продакшен одного стартапа. Они искали паттерны в логах. Логи были по 2 ГБ. Вместо 5 секунд поиск занимал 15 минут. Команда ругала «медленный алгоритм», пока я не заглянул в код. Звезды на GitHub — это не сертификат качества. Это просто метрика популярности. Иногда — популярности заблуждения.
Как отличить настоящий алгоритм от подделки? (Микро-инструкция)
Чтобы не попасться на удочку, запомните три признака. Если их нет — перед вами фейк.
- Препроцессинг. Настоящий код всегда содержит функцию построения таблицы (например, build_bad_char_table). Если её нет — это не Бойер-Мур.
- Сдвиг не равен 1. В правильной реализации сдвиг рассчитывается динамически: i += max(1, shift). Если вы видите просто i += 1 — это перебор.
- Тесты на больших данных. Запустите бенчмарк. На тексте из 100 000 символов и шаблоне из 10 символов настоящий алгоритм сделает в 5-10 раз меньше итераций, чем подделка.
Вот для сравнения фрагмент корректной реализации на Python, который показывает, как должна выглядеть эвристика стоп-символа:
def build_bad_char_table(pattern):
table = {}
for i in range(len(pattern) - 1):
table[pattern[i]] = len(pattern) - 1 - i
return table
def boyer_moore_correct(text, pattern):
bad_char = build_bad_char_table(pattern)
i = 0
while i <= len(text) - len(pattern):
j = len(pattern) - 1
while j >= 0 and pattern[j] == text[i+j]:
j -= 1
if j < 0: return i
shift = bad_char.get(text[i+j], len(pattern))
i += max(1, shift - (len(pattern) - 1 - j)) # Вот она, магия сдвига!
return -1
Резюме от автора. Не верьте названиям. Верьте коду. Если вы берете алгоритм из репозитория с миллионом звезд, но внутри него нет таблиц сдвигов — это не алгоритм, а обманка. Звезды не делают код быстрее. Их ставят за красивое название. Будьте тем, кто читает документацию, а не просто ставит лайк. Иначе ваш «быстрый поиск» превратится в «долгое ожидание».














