Каков самый быстрый алгоритм поиска подстроки?

Хорошо, так что я не похож на идиота. Я буду более подробно излагать проблему/требования:

  • Игла (шаблон) и стога сена (текст для поиска) - это строки с нулевым завершением в стиле C. Информация о длине отсутствует; если необходимо, он должен быть вычислен.
  • Функция должна возвращать указатель на первое совпадение или NULL, если совпадение не найдено.
  • Случаи отказа не допускаются. Это означает, что любой алгоритм с непостоянными (или большими постоянными) требованиями к хранению должен иметь резервный случай для отказа в распределении (и производительность в резервной помощи, тем самым, способствует наихудшей производительности).
  • Реализация должна быть в C, хотя хорошее описание алгоритма (или ссылки на такое) без кода тоже отлично.

... а также то, что я подразумеваю под "самым быстрым":

  • Детерминированный O(n) где n= длина стога сена. (Но может быть возможно использовать идеи из алгоритмов, которые обычно O(nm) (например, хеш для прокатки), если они объединены с более надежным алгоритмом для получения детерминированных результатов O(n)).
  • Никогда не выполняет (измеримо, пара часов для if (!needle[1]) и т.д. в порядке) хуже, чем алгоритм наивной грубой силы, особенно на очень коротких иглах, которые, вероятно, являются наиболее распространенным случаем. (Безусловная тяжелая предварительная обработка накладных расходов плоха, так как пытается улучшить линейный коэффициент для патологических игл за счет возможных игл.)
  • Учитывая произвольную иглу и стог сена, сопоставимую или лучшую производительность (не более чем на 50% больше времени поиска) по сравнению с любым другим широко реализованным алгоритмом.
  • Помимо этих условий, я оставляю определение "быстрее" открытым. Хороший ответ должен объяснить, почему вы рассматриваете подход, который вы предлагаете "быстрее".

Моя текущая реализация выполняется примерно на 10% медленнее и в 8 раз быстрее (в зависимости от ввода), чем реализация двухсторонней реализации glibc.

Обновление: Мой текущий оптимальный алгоритм выглядит следующим образом:

  • Для игл с длиной 1 используйте strchr.
  • Для игл с длиной 2-4 используйте машинные слова для сравнения 2-4 байта одновременно следующим образом: предварительно загрузите иглу в 16- или 32-битное целое число с битами и выведите байты из старого байта/новые байты из стога сена на каждой итерации. Каждый байт стога сена читается ровно один раз и берет проверку против 0 (конец строки) и одно 16- или 32-битное сравнение.
  • Для игл с длиной > 4 используйте двухсторонний алгоритм с плохой таблицей сдвига (например, Boyer-Moore), который применяется только к последнему байту окна. Чтобы избежать накладных расходов на инициализацию таблицы 1kb, которая была бы чистой потерей для многих игл средней длины, я сохраняю бит (32 байта), который указывает, какие записи в таблице сдвига инициализируются. Биты, которые не заданы, соответствуют байтовым значениям, которые никогда не появляются в иголке, для которых возможен сдвиг длины в длину.

В моем сознании остаются большие вопросы:

  • Есть ли способ лучше использовать таблицу плохого сдвига? Boyer-Moore наилучшим образом использует его, сканируя назад (справа налево), но Two-Way требует сканирования слева направо.
  • Единственные два жизнеспособных алгоритма-кандидата, которые я нашел для общего случая (без проблем с памятью или квадратичной производительностью), Двусторонняя и Строковое соответствие по упорядоченным алфавитам. Но есть ли легко обнаруживаемые случаи, когда оптимальные алгоритмы? Разумеется, многие из O(m) (где m - длина иглы) в космических алгоритмах могут использоваться для m<100 или так. Также можно было бы использовать алгоритмы, которые наихудшего квадратичны, если есть простой тест на иглы, которые, предположительно, требуют только линейного времени.

Бонусные баллы за:

  • Можете ли вы улучшить производительность, предположив, что игла и стога сена являются хорошо сформированными UTF-8? (С символами, имеющими разные длины байтов, четко сформированная последовательность задает некоторые требования к выравниванию строк между иглой и стоге сена и позволяет автоматически сдвигать 2-4 байта, когда встречается несоответствующий старший байт. Но эти ограничения покупают вам много/ничего сверх того, что максимальные вычисления суффикса, хорошие сдвиги суффиксов и т.д. уже дают вам различные алгоритмы?)

Примечание:Я хорошо разбираюсь в большинстве алгоритмов, просто не так хорошо, как они работают на практике. Вот хорошая ссылка, чтобы люди не оставляли мне ссылки на алгоритмы как комментарии/ответы: http://www-igm.univ-mlv.fr/~lecroq/string/index.html

Ответ 1

Создайте тестовую библиотеку возможных игл и стогов сена. Проведите тесты по нескольким алгоритмам поиска, включая грубую силу. Выберите тот, который лучше всего работает с вашими данными.

Boyer-Moore использует таблицу плохих символов с хорошей таблицей суффиксов.

Boyer-Moore-Horspool использует таблицу плохих символов.

Knuth-Morris-Pratt использует таблицу частичного соответствия.

Rabin-Karp использует бегущие хэши.

Они все накладные расходы для уменьшения сравнений в разной степени, поэтому производительность реального мира будет зависеть от средних длин как иглы, так и стога сена. Чем больше первоначальных накладных расходов, тем лучше с более длинными входами. С очень короткими иглами может победить грубая сила.

Edit:

Другой алгоритм может быть лучше всего для поиска пар оснований, английских фраз или отдельных слов. Если бы был один лучший алгоритм для всех входов, он был бы опубликован.

Подумайте о следующей маленькой таблице. Каждый вопросительный знак может иметь другой лучший алгоритм поиска.

                 short needle     long needle
short haystack         ?               ?
long haystack          ?               ?

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

Если мне нужен набор образцов, я думаю, что я очищу сайт, например google или wikipedia, а затем удалите html со всех страниц результатов. Для сайта поиска введите слово, затем используйте одну из предложенных поисковых фраз. Выберите несколько разных языков, если это применимо. Используя веб-страницы, все тексты будут короткими до средних, поэтому объедините достаточно страниц, чтобы получить более длинные тексты. Вы также можете найти общедоступные книги, юридические записи и другие крупные тексты. Или просто создавайте случайный контент, выбирая слова из словаря. Но точка профилирования - это тест против типа контента, который вы будете искать, поэтому, если возможно, используйте образцы реального мира.

Я оставил короткое и длинное расплывчатое. Для иглы, я думаю, короткий как под 8 символами, средний как под 64 символами, и длиной как под 1k. Для стога сена, я думаю, короткий как под 2 ^ 10, средний как под 2 ^ 20, и длинный как до 2 ^ 30 символов.

Ответ 2

Опубликованный в 2011 году, я считаю, что вполне может быть алгоритмом "Простое согласование по постоянному пространству в пространстве в реальном времени" Дани Бреслауэр, Роберто Гросси и Филиппо Mignosi.

Обновление:

В 2014 году авторы опубликовали это улучшение: На пути к оптимальной комбинации упакованных строк.

Ответ 3

http://www-igm.univ-mlv.fr/~lecroq/string/index.html ссылка, на которую указывает отличный источник и резюме некоторых из наиболее известных и исследованных строковых алгоритмов.

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

Если вы хотите разработать конкретный алгоритм для поиска строк, тогда игнорируйте остальное, что я должен сказать, если вы хотите разработать обобщенную службу поиска строк затем попробуйте следующее:

Проведите некоторое время, просматривая конкретные сильные и слабые стороны алгоритмы, на которые вы уже ссылались. Проведите с целью найти набор алгоритмы, которые охватывают диапазон и объем поиска строк, которые вы заинтересованный. Затем создайте селектор поиска на передней панели, основанный на классификаторе чтобы настроить лучший алгоритм для данных входов. Таким образом, вы можете используйте наиболее эффективный алгоритм для выполнения задания. Это особенно эффективен, когда алгоритм очень хорош для определенных поисков, но плохо ухудшается. Для Например, грубая сила, вероятно, лучше всего подходит для игл длиной 1, но быстро ухудшается по мере увеличения длины иглы, после чего sustik-moore algoritim может стать более эффективным (по небольшим алфавитам), затем для более длинных игл и больших алфавитов, алгоритмы KMP или Boyer-Moore могут быть лучше. Это просто примеры, иллюстрирующие возможную стратегию.

Множественный алгоритм не является новой идеей. Я считаю, что он был коммерческие пакеты Сортировка/Поиск (например, SYNCSORT, обычно используемый для мэйнфреймов несколько алгоритмов сортировки и использует эвристику для выбора "наилучшего" для данных входов)

Каждый алгоритм поиска имеет несколько вариантов, которые может существенно повлиять на его производительность, например, это бумага.

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

Ответ 4

Я был удивлен, увидев наш технический отчет, приведенный в этой дискуссии; Я один из авторов алгоритма, названного выше Sustik-Moore. (Мы не использовали этот термин в нашей статье.)

Я хотел здесь подчеркнуть, что для меня наиболее интересной особенностью алгоритма является то, что довольно просто доказать, что каждая буква рассматривается не чаще одного раза. Для более ранних версий Бойер-Мура они доказали, что каждая буква рассматривается не более 3 и более поздних 2 раза максимум, и эти доказательства были более активными (см. Цитирует в статье). Поэтому я также вижу дидактическое значение при представлении/изучении этого варианта.

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

Наша главная цель - довести эту версию до сведения других, которые могут ее улучшить. Поиск строк имеет очень много вариаций, и мы сами не можем думать обо всех, где эта идея может принести пользу. (Исправлен текст и изменение шаблона, фиксированный шаблон, различный текст, возможная/невозможная предварительная обработка, параллельное выполнение, поиск подмножеств в больших текстах, допускать ошибки, близкие совпадения и т.д. И т.д.)

Ответ 5

Самый быстрый алгоритм поиска подстроки будет зависеть от контекста:

  • размер алфавита (например, ДНК против английского)
  • длина иглы

В статье 2010 "Точная проблема сопоставления строк: комплексная экспериментальная оценка" представлены таблицы с временем выполнения для 51 алгоритма (с разными размерами алфавита и длиной иглы), поэтому вы можете выбрать лучший алгоритм для вашего контекста.

Все эти алгоритмы имеют реализации C, а также набор тестов:

http://www.dmi.unict.it/~faro/smart/algorithms.php

Ответ 6

Хороший вопрос. Просто добавьте несколько маленьких бит...

  • Кто-то говорил о согласовании последовательности ДНК. Но для последовательности ДНК то, что мы обычно делаем, это построить структуру данных (например, суффикс-массив, дерево суффиксов или FM-индекс) для стога сена и сопоставить с ним множество игл. Это другой вопрос.

  • Было бы здорово, если бы кто-то захотел сравнить различные алгоритмы. Есть очень хорошие тесты сжатия и построения массивов суффиксов, но я не видел теста на сопоставление строк. Потенциальные кандидаты в сенату могут быть из тест SACA.

  • Несколько дней назад я тестировал реализацию Boyer-Moore от рекомендуемую страницу (EDIT: мне нужна функция вызовите memmem(), но это не стандартная функция, поэтому я решил реализовать ее). В моей тестовой программе используется случайный стог сена. Похоже, что реализация Boyer-Moore на этой странице быстрее, чем glibc memmem() и Mac strnstr(). Если вы заинтересованы, реализация здесь, а код сравнения . Это определенно не реалистичный бенчмарк, но это начало.

Ответ 7

Я знаю, что это старый вопрос, но самые плохие таблицы сдвига - один символ. Если это имеет смысл для вашего набора данных (например, особенно если оно написано словами), и если у вас есть свободное пространство, вы можете получить резкое ускорение, используя таблицу плохого сдвига, состоящую из n-граммов, а не отдельных символов.

Ответ 8

Здесь Реализация поиска на Python, используемая по всему ядру. В комментариях указано, что он использует сжатую таблицу дельта-1 боре-лор.

Я провел довольно обширное экспериментирование с самим поиском строк, но это было для нескольких строк поиска. Сложные реализации Horspool и Bitap могут часто придерживайтесь своих алгоритмов, таких как Aho-Corasick для низкого количества шаблонов.

Ответ 9

Более быстрый алгоритм "Поиск одного совпадающего символа" (ala strchr).

Важные замечания:

  • В этих функциях используется "число/кол-во (ведущих | конечных) нулей" gcc compiler intrinsic- __builtin_ctz. Эти функции, вероятно, будут выполняться только на машинах, которые имеют инструкцию (ы), которые выполняют эту операцию (то есть, x86, ppc, arm).

  • Эти функции предполагают, что целевая архитектура может выполнять 32 и 64-разрядные несвязанные нагрузки. Если ваша целевая архитектура не поддерживает это, вам нужно будет добавить некоторую логику запуска, чтобы правильно выровнять показания.

  • Эти функции нейтраль процессора. Если целевой ЦП имеет векторные инструкции, вы можете сделать (намного) лучше. Например, функция strlen ниже использует SSE3 и может быть тривиально модифицирована для XOR байтов, отсканированных для поиска байта, отличного от 0, Тесты, выполненные на ноутбуке Core 2,66 ГГц, работающем под управлением Mac OS X 10.6 (x86_64):

    • 843,433 МБ/с для strchr
    • 2656,742 МБ/с для findFirstByte64
    • 13094.479 МБ/с для strlen

... 32-разрядная версия:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u)   ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu);                    (__builtin_ctz(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
  uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
  byteMask32 |= byteMask32 << 16;
  while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
  return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}

... и 64-разрядная версия:

#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full);                    (__builtin_ctzll(_x) + 1) >> 3; })
#endif

unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
  uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
  byteMask64 |= byteMask64 << 16;
  byteMask64 |= byteMask64 << 32;
  while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
  return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}

Редактировать 2011/06/04 OP указывает в комментариях, что это решение имеет "непреодолимую ошибку":

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

Это технически верно, но применимо практически к любому алгоритму, который работает с кусками, большими, чем один байт, включая метод, предложенный ОП в комментариях:

Типичная реализация strchr не наивна, но довольно эффективна, чем то, что вы дали. См. Конец этого для наиболее широко используемого алгоритма: http://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord

Это также не имеет никакого отношения к выравниванию per se. Правда, это может потенциально вызвать поведение, обсуждаемое на большинстве используемых общих архитектур, но это больше связано с деталями реализации микроархитектуры - если неизгладимое чтение ограничивает границу 4K (опять же, типично), то это чтение вызовет программу если следующая граница страницы 4K не отображается.

Но это не является "ошибкой" в алгоритме, указанном в ответе, - что поведение связано с тем, что такие функции, как strchr и strlen, не принимают аргумент length, чтобы связать размер поиска. Поиск char bytes[1] = {0x55};, который для целей нашего обсуждения, как раз так, размещается в самом конце границы страницы VM 4K, а следующая страница не отображается, с strchr(bytes, 0xAA) (где strchr является байтовым, a-time) приведет к сбою точно так же. То же самое для strchr родственника-кузена strlen.

Без аргумента length невозможно определить, когда вы должны переключиться с алгоритма высокой скорости и вернуться к байтовому алгоритму. Скорее всего, "ошибка" будет состоять в том, чтобы читать "за размер выделения", что технически приводит к undefined behavior в соответствии с различными стандартами языка C и будет помечено как ошибка чем-то вроде valgrind.

В общем, все, что работает на больших байтах, чтобы идти быстрее, поскольку это отвечает на код, и код, указанный OP, но должен иметь байт-точную семантику чтения, скорее всего, будет "багги", если есть no length аргумент для управления угловым (ыми) корпусом "последнего чтения".

Код в этом ответе - это ядро ​​для быстрого поиска первого байта в куске размера естественного процессора, если целевой процессор имеет быструю инструкцию ctz. Тривиально добавлять такие вещи, как убедиться, что он работает только с правильно выровненными естественными границами или какой-либо формой length bound, что позволит вам переключиться с высокоскоростного ядра и на более медленную байтовую байтовую проверку.

OP также заявляет в комментариях:

Что касается оптимизации ctz, это только влияет на работу хвоста O (1). Это может улучшить производительность с помощью крошечных строк (например, strchr("abc", 'a');, но, конечно, не со строками любого основного размера.

Является ли это утверждение истинным, во многом зависит от микроархитектуры. Используя каноническую 4-ступенчатую модель трубопровода RISC, это почти наверняка верно. Но очень сложно сказать, верно ли это для современного сверхсказкового CPU с супер-супервариантом, где максимальная скорость ядра может полностью затмить скорость потоковой передачи данных. В этом случае это не только правдоподобно, но и довольно часто, поскольку существует большой разрыв в "количестве инструкций, которые могут быть удалены" относительно "количества байтов, которые могут быть потоковыми", так что у вас есть "количество инструкций, которые могут быть удалены для каждого байта, который может быть потоковым". Если это достаточно велико, команда ctz + shift может быть выполнена "бесплатно".

Ответ 10

Просто найдите "самую быструю strstr", и если вы видите что-то интересное, просто спросите меня.

На мой взгляд, вы накладываете слишком много ограничений на себя (да, мы все хотим, чтобы линейные линейные в max искателя), однако для этого требуется реальный программист, до тех пор я думаю, что хэш-подход - limbo (хорошо усиленный BNDM для более коротких 2,16 образцов).

Просто быстрый пример:

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) как однострочное... Skip-Performance (больше-лучше): 3041%, 6801754 пропусков/итераций Railgun_Quadruplet_7Hasherezade_hits/Railgun_Quadruplet_7Hasherezade_clocks: 0/58 Функция Railgun_Quadruplet_7Hasherezade: 3483 КБ/часы

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) как однострочное... Skip-Performance (больше-лучше): 1554%, 13307181 пропусков/итераций Boyer_Moore_Flensburg_hits/Boyer_Moore_Flensburg_clocks: 0/83 Производительность Boyer_Moore_Flensburg: 2434 КБ/часы

Выполнение поиска шаблона (32 байта) в строку (206908949 байтов) как однострочное... Skip-Performance (больше-лучше): 129%, 160239051 пропусков/итераций Two-Way_hits/Two-Way_clocks: 0/816 Двусторонняя производительность: 247 КБ/часы

Sanmayce,
Отношения

Ответ 11

Двусторонний алгоритм, который вы упоминаете в своем вопросе (который, кстати, невероятен!), недавно был улучшен, чтобы эффективно работать на многобайтовых словах за раз: Оптимальное соответствие пакетных строк.

Я не читал всю статью, но, похоже, они полагаются на пару новых специальных команд процессора (включая, например, SSE 4.2), которые являются O (1) для заявки на временную сложность, хотя, если они не являются доступны они могут имитировать их в O (log log w) время для w-бит слов, который не звучит слишком плохо.

Ответ 12

Можно реализовать, скажем, 4 разных алгоритма. Каждые M минут (определяется эмпирически) запускают все 4 по текущим реальным данным. Накопите статистику по N прогонам (также TBD). Затем используйте только победителя в течение следующих M минут.

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

Ответ 13

Недавно я обнаружил хороший инструмент для измерения производительности различных доступных альгос: http://www.dmi.unict.it/~faro/smart/index.php

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

Ответ 14

Вы также можете иметь различные тесты с несколькими типами строк, поскольку это может иметь большое влияние на производительность. Алгоны будут выполнять различия на основе поиска естественного языка (и даже здесь могут быть мелкозернистые различия из-за разных морфологий), строк ДНК или случайных строк и т.д.

Размер алфавита будет играть роль во многих альгосах, так же как и размер иглы. Например, Horspool делает добро на английском тексте, но плохо на ДНК из-за различного размера алфавита, что делает жизнь трудной для правила плохого характера. Введение в хороший-суффикс все это очень усложняет.

Ответ 15

Используйте stdlib strstr:

char *foundit = strstr(haystack, needle);

Это было очень быстро, мне потребовалось всего 5 секунд.

Ответ 16

Я не знаю, насколько это лучше, но у меня был хороший опыт работы с Boyer-Moore.

Ответ 17

Это не дает прямого ответа на вопрос, но если текст очень большой, как насчет того, чтобы разделить его на перекрывающиеся разделы (перекрывающиеся по длине шаблона), а затем одновременно искать разделы, используя потоки. Что касается самого быстрого алгоритма, Бойер-Мур-Хорспул, я думаю, является одним из самых быстрых, если не самый быстрый среди вариантов Бойер-Мур. Я разместил пару вариантов Бойера-Мура (я не знаю их названия) в этой теме Алгоритм быстрее, чем поиск BMH (Бойера-Мура-Хорспула).