Чи є хороший алгоритм пошуку для одного символу?


23

Я знаю кілька основних алгоритмів відповідності рядків, таких як KMP або Boyer-Moore, але всі вони аналізують схему перед пошуком. Однак, якщо в них є один символ, аналізувати не так вже й багато. То чи є кращий алгоритм, ніж наївний пошук порівняння кожного символу тексту?


13
Ви можете кинути на нього SIMD інструкції, але нічого кращого за O (n) не вийде.
CodesInChaos

7
За один пошук або кілька пошукових запитів у тій самій рядку?
Крістоф

KMP - це точно не те, що я б назвав "базовим" алгоритмом відповідності рядків ... Я навіть не впевнений, що він такий швидкий, але це важливо історично. Якщо ви хочете щось базове, спробуйте алгоритм Z.
Мехрдад

Припустимо, було місце символів, на яке алгоритм пошуку не дивився. Тоді він не зможе розрізнити між рядками з символом голки в цьому положенні та рядками з іншим символом у цій позиції.
користувач253751

Відповіді:


29

Зрозуміло, що найгірший випадок - O(N)це дуже приємні мікрооптимізації.

Наївний метод виконує порівняння символів та порівняння кінця тексту для кожного персонажа.

Використання дозорного (тобто копії цільового символу в кінці тексту) зменшує кількість порівнянь до 1 на символ.

На рівні біт-подвійного змісту є:

#define haszero(v)      ( ((v) - 0x01010101UL) & ~(v) & 0x80808080UL )
#define hasvalue(x, n)  ( haszero((x) ^ (~0UL / 255 * (n))) )

знати, чи має байт у слові ( x) певне значення ( n).

Підвираз v - 0x01010101UL, оцінюється на високий набір бітів у будь-якому байті, коли відповідний байт у vнулі або більше, ніж 0x80.

Підвираз ~v & 0x80808080ULвираховує високі біти, встановлені в байтах, де байт vне має свого високого біта (тому байт був меншим 0x80).

За допомогою ANDing цих двох підвиразів ( haszero) результат - це високі біти, встановлені там, де байти в vнулі, оскільки високі біти, встановлені через значення, більше, ніж 0x80у першому підвиразі, маскуються другим (27 квітня, 1987 р. Алан Мікрофт).

Тепер ми можемо XOR значення тестувати ( x) за допомогою слова, яке було заповнене значенням байта, в якому нас цікавить ( n). Оскільки XORing значення з самим собою призводить до нульового байта і ненульового значення, інакше ми можемо передати результат haszero.

Це часто використовується в типовому strchrвиконанні.

(Стівен М Беннет запропонував це 13 грудня 2009 року. Докладніші відомості у відомих хай-бит-хайдінгах ).


PS

цей код порушений для будь-якої комбінації 1111'поряд із a0

Хак проходить тест на грубу силу (просто будьте терплячі):

#include <iostream>
#include <limits>

bool haszero(std::uint32_t v)
{
  return (v - std::uint32_t(0x01010101)) & ~v & std::uint32_t(0x80808080);
}

bool hasvalue(std::uint32_t x, unsigned char n)
{
  return haszero(x ^ (~std::uint32_t(0) / 255 * n));
}

bool hasvalue_slow(std::uint32_t x, unsigned char n)
{
  for (unsigned i(0); i < 32; i += 8)
    if (((x >> i) & 0xFF) == n)
      return true;

  return false;
}

int main()
{
  const std::uint64_t stop(std::numeric_limits<std::uint32_t>::max());

  for (unsigned c(0); c < 256; ++c)
  {
    std::cout << "Testing " << c << std::endl;

    for (std::uint64_t w(0); w != stop; ++w)
    {
      if (w && w % 100000000 == 0)
        std::cout << w * 100 / stop << "%\r" << std::flush;

      const bool h(hasvalue(w, c));
      const bool hs(hasvalue_slow(w, c));

      if (h != hs)
        std::cerr << "hasvalue(" << w << ',' << c << ") is " << h << '\n';
    }
  }

  return 0;
}

Багато відповідей на відповідь, що робить припущення одним символом = один байт, який на сьогоднішній день вже не є стандартом

Дякую за зауваження.

Відповідь мала на увазі будь-що, окрім есе про багатобайтові кодування з змінною шириною :-) (якщо чесно, це не моя область знань, і я не впевнений, що це шукало ОП).

Так чи інакше, мені здається, що вищезазначені ідеї / хитрощі можна було дещо адаптувати до MBE (особливо самосинхронізованих кодувань ):

  • як зазначається в коментарі Йохана, хак можна «легко» розширити, щоб він працював на подвійні байти або що завгодно (звичайно, ви не можете занадто сильно розтягнути його);
  • типова функція, яка розміщує символ у багатобайтовій рядку символів:
    • містить виклики до strchr/ strstr(наприклад, GNUlib coreutils mbschr )
    • розраховує, що вони будуть добре налаштовані.
  • техніку дозорного можна використовувати з невеликим передбаченням.

1
Це погана версія SIMD-операції.
Руслан

@Руслан Абсолютно! Це часто трапляється для ефективних хайд-бит-хайдингів.
manlio

2
Гарна відповідь. З боку читабельності я не розумію, чому ви пишете 0x01010101ULв одному рядку і ~0UL / 255в наступному. Створюється враження, що вони повинні бути різними значеннями, оскільки в іншому випадку, навіщо писати це двома різними способами?
hvd

3
Це класно, тому що він перевіряє 4 байти одночасно, але для цього потрібні кілька (8?) Інструкцій, оскільки #defines буде розширено на ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL ). Хіба однобайтне порівняння не було б швидшим?
Джед Шаф

1
@DocBrown, код легко може бути змушений працювати для подвійних байтів (тобто півслова), або гризків чи будь-чого іншого. (з урахуванням зазначеного мною застереження).
Йохан - відновити Моніку

20

Будь-який алгоритм пошуку тексту, який шукає кожне виникнення одного символу в заданому тексті, повинен прочитати кожен символ тексту хоча б один раз, що має бути очевидним. А оскільки цього достатньо для одноразового пошуку, не може бути кращого алгоритму (якщо думати з точки зору порядку виконання часу, який у цьому випадку називається "лінійним" або O (N), де N - кількість символів для пошуку).

Однак для реальних реалізацій, безумовно, можливе безліч мікрооптимізацій, які не змінюють порядок виконання часу в цілому, але зменшують фактичний час виконання. І якщо мета - не знайти кожне виникнення одного персонажа, а лише перше, можна, звичайно, зупинитися на першому виникненні. Тим не менш, навіть у такому випадку найгіршим є те, що персонаж, якого ви шукаєте, є останнім символом у тексті, тому найгіршим випадком виконання часу для цієї мети є все ще O (N).


8

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

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

string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
     ++histogram[character];

if(histogram['a'])
    // a belongs to haystack

1

Якщо вам потрібно шукати символи в цій самій самій рядку не один раз, то можливим підходом є поділ рядка на менші частини, можливо, рекурсивно, та використання фільтрів розцвітання для кожної з цих частин.

Оскільки фільтр цвітіння може вам точно сказати, якщо символ не знаходиться в тій частині рядка, яка "представлена" фільтром, ви можете пропустити деякі частини під час пошуку символів.

Наприклад: для наступного рядка можна розділити його на 4 частини (кожна по 11 символів) і заповнити для кожної частини фільтр розквіту (можливо, 4 байти великим розміром) символами цієї частини:

The quick brown fox jumps over the lazy dog 
          |          |          |          |

Ви можете прискорити пошук, наприклад, за персонажем a: Використовуючи хороші хеш-функції для фільтрів цвітіння, вони скажуть вам, що з великою часткою ймовірності вам не доведеться шукати ні в першій, ні в другій, ні в третій частині. Таким чином, ви вбережете себе від перевірки 33 символів, а замість цього потрібно лише перевірити 16 байт (для 4 фільтрів розквіту). Це все-таки O(n), лише з постійним (дробовим) фактором (і для того, щоб це було ефективно, вам потрібно буде вибрати більші частини, щоб мінімізувати накладні витрати на обчислення хеш-функцій для пошукового персонажа).

Використовуючи рекурсивний, деревоподібний підхід, вам слід наблизитись O(log n):

The quick brown fox jumps over the lazy dog 
   |   |   |   |   |   |   |   |---|-X-|   |  (1 Byte)
       |       |       |       |---X---|----  (2 Byte)
               |               |-----X------  (3 Byte)
-------------------------------|-----X------  (4 Byte)
---------------------X---------------------|  (5 Byte)

У цій конфігурації потрібно (знову ж таки, припустивши, що нам пощастило і не отримали помилковий позитив від одного з фільтрів), щоб перевірити

5 + 2*4 + 3 + 2*2 + 2*1 bytes

щоб дістатися до заключної частини (де потрібно знайти 3 символи до знаходження a).

Використовуючи хорошу (краще як вище) схему підрозділу, ви повинні отримати досить хороші результати з цим. (Примітка. Фільтри цвітіння в корені дерева повинні бути більше, ніж близько до листя, як показано в прикладі, щоб отримати низьку ймовірність помилкових позитивних результатів)


Шановний долю, поясніть, будь ласка, чому ви вважаєте, що моя відповідь не корисна.
Даніель Жур

1

Якщо рядок буде шукати кілька разів (типова проблема "пошуку"), рішенням може бути O (1). Рішення - побудувати індекс.

Наприклад:

Карта, де Key - символ та значення - це перелік індексів цього символу в рядку.

Завдяки цьому один пошук карт може дати відповідь.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.