Tek bir karakter için iyi bir arama algoritması var mı?


23

KMP veya Boyer-Moore gibi birkaç temel dizi eşleştirme algoritmasını biliyorum, ancak bunların tümü aramadan önce modeli analiz ediyor. Ancak, tek bir karakter varsa, analiz edilecek çok fazla bir şey yok. Öyleyse, metnin her karakterini karşılaştırmanın saf arayışından daha iyi bir algoritma var mı?


13
SIMD komutlarını ona atabilirsiniz, ancak O (n) 'den daha iyi bir şey elde edemezsiniz.
CodesInChaos

7
Tek bir arama veya aynı dizede birden fazla arama için mi?
Christophe

KMP kesinlikle "temel" bir dize eşleme algoritması diyeceğim bir şey değil ... O kadar hızlı olduğundan da emin değilim, ama tarihsel olarak önemli. Temel bir şey istiyorsanız, Z algoritmasını deneyin.
Mehrdad

Arama algoritmasının bakmadığı bir karakter konumu olduğunu varsayalım. O zaman, iğne karakteriyle o konumda bulunan dizeleri ve bu konumda farklı bir karaktere sahip dizileri ayırt edemezdi.
kullanıcı253751

Yanıtlar:


29

En kötü durum olduğu anlaşılıyor ki, O(N)bazı çok güzel mikro optimizasyonlar var.

Naif yöntemi, her karakter için karakter karşılaştırması ve metin sonu karşılaştırması gerçekleştirir.

Sentinel kullanmak (yani metnin sonundaki hedef karakterin bir kopyası), karakter sayısını karşılaştırma sayısını 1'e düşürür.

Biraz twiddling düzeyinde var:

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

( x) kelimesindeki herhangi bir baytın belirli bir değeri ( n) olup olmadığını bilmek .

Alt ifade v - 0x01010101UL, karşılık gelen bayt vsıfır veya daha büyük olduğunda, herhangi bir byte'ta ayarlanan yüksek bir bit olarak değerlendirilir 0x80.

Alt ifade ~v & 0x80808080UL, baytın vyüksek bit kümesine sahip olmadığı baytlarda ayarlanan yüksek bitleri değerlendirir (bu nedenle bayt daha küçüktü 0x80).

Bu iki alt ifadenin ( haszero) AND sonucu, sonuç, baytların vsıfır olduğu yüksek bitlerdir çünkü 0x80ilk alt ifadedeki değerden daha büyük bir değere göre ayarlanan yüksek bitler , ikinci tarafından maskelenir (27 Nisan, 1987, Alan Mycroft).

Şimdi test etmeyi ( x) ilgilendiğimiz bayt değeriyle doldurulmuş bir sözcükle ( ) XOR yapabiliriz n. XORing kendisi ile bir değer sıfır bayt ile sonuçlanır, aksi halde sıfırla sonuçlanmadığından sonucu geçebiliriz haszero.

Bu genellikle tipik bir strchruygulamada kullanılır.

(Stephen M Bennet bunu 13 Aralık 2009'da önerdi. İyi bilinen Bit Twiddling Hack'lerinde daha ayrıntılı bilgi var ).


PS

Bu kod 1111bir yanındaki herhangi bir kombinasyonu için bozulur.0

Hack, kaba kuvvet testini geçer (sadece sabırlı olun):

#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;
}

Bu varsayımı bir karakter niteliğinde kılan bir cevap için çok fazla teklif = bir bayt, günümüzde artık standart değil

Açıklama için teşekkürler.

Cevap, çok baytlı / değişken genişlikli kodlamalar :-) üzerine yapılan bir yazıdan başka bir şey değildi, (tüm adaletlerde bu benim uzmanlık alanım değil ve OP'nin aradığı şey olduğundan emin değilim).

Her neyse bana, yukarıdaki fikirlerin / püf noktaların bir şekilde MBE'ye uyarlanabileceğini (özellikle kendiliğinden senkron olan kodlamaları ):

  • Johan'ın yorumunda belirtildiği gibi , kesmek 'kolayca' çift bayt veya herhangi bir şey için çalışmaya uzatılabilir (elbette çok fazla uzatamazsınız);
  • çok baytlı karakter dizesindeki bir karakteri bulan tipik bir işlev:
  • Sentinel tekniği biraz öngörü ile kullanılabilir.

1
Bu, fakir bir adamın SIMD operasyonunun bir versiyonudur.
Ruslan

@Ruslan Kesinlikle! Bu, genellikle etkili bit döndürme korsanları için geçerlidir.
manlio

2
Güzel cevap Okunabilirlik açısından, neden 0x01010101ULbir satırda ve diğerinde yazdığınızı anlamıyorum ~0UL / 255. Farklı değerler olduğu izlenimini veriyor, aksi halde neden iki farklı şekilde yazıyorlar?
hvd

3
Bu iyidir, çünkü bir kerede 4 baytı kontrol eder, ancak #defines'nin genişleyeceği için çoklu (8?) Komut gerektirir ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL ). Tek baytlık karşılaştırma daha hızlı olmaz mıydı?
Jed Schaaf

1
@DocBrown, kod kolayca çift bayt (örneğin, halfwords) veya nibbles veya herhangi bir şey için çalışmak için yapılabilir. (Bahsettiğim ihtar dikkate alınarak).
Johan - Monica

20

Belirli bir metinde tek bir karakterin her bir oluşumunu arayan herhangi bir metin arama algoritması, metnin her karakterini en az bir kez okumak zorundadır, bu açık olmalıdır. Ve bu, bir kerelik bir arama için yeterli olduğundan, daha iyi bir algoritma olamaz (bu, çalışma zamanı sırası açısından "doğrusal" veya O (N) olarak adlandırılan, N'nin karakter sayısı olduğu düşünüldüğünde) aramak için).

Bununla birlikte, gerçek uygulamalar için, çalışma süresi sırasını bir bütün olarak değiştirmeyen, ancak gerçek çalışma süresini düşüren pek çok mikro optimizasyon vardır. Ve eğer amaç, tek bir karakterin her oluşumunu bulmak değil, yalnızca birincisini bulmaksa, elbette ilk oluşumunda durabilirsiniz. Bununla birlikte, bu durumda bile, en kötü durum, aradığınız karakterin metindeki son karakter olmasıdır, bu nedenle bu amaç için en kötü durum çalışma zamanı sırası hala O (N) 'dir.


8

"Samanlık" bir kereden fazla aranırsa, histogram tabanlı bir yaklaşım son derece hızlı olacaktır. Histogram oluşturulduktan sonra, cevabınızı bulmak için yalnızca işaretçi aramaya ihtiyacınız vardır.

Sadece aranan desenin olup olmadığını bilmeniz gerekiyorsa, basit bir sayaç yardımcı olabilir. Her karakterin samanlıkta bulunduğu konumları veya ilk oluşumun pozisyonunu içerecek şekilde genişletilebilir.

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

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

1

Aynı dizideki karakterleri bir defadan fazla aramanız gerekirse, diziyi daha küçük parçalara, muhtemelen tekrarlayarak bölmek ve bu parçaların her biri için çiçek filtreleri kullanmak olası bir yaklaşımdır.

Bir çiçek filtresi size bir karakterin olup olmadığını kesin olarak söyleyebildiğinden filtre tarafından "temsil" oluyor dize parçasındaki karakter ararken, bazı parçaları atlayabilirsiniz.

Örneğin: Aşağıdaki dize için biri 4 parçaya bölünebilir (her biri 11 karakter uzunluğunda) ve her parça için bu bölümün karakterleriyle bir çiçek filtresi (belki de 4 bayt büyüklüğünde) doldurabilir:

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

Aramanızı hızlandırabilirsiniz, örneğin karakter için a: Çiçek filtreleri için iyi karma fonksiyonlar kullanmak, size şunu söyleyecektir - yüksek olasılıkla - ne birinci, ikinci ne de üçüncü bölümde arama yapmak zorunda değilsiniz. Böylece, 33 karakter kontrol etmekten kurtarıyorsunuz ve bunun yerine sadece 16 baytı kontrol etmeniz gerekiyor (4 çiçeklenme filtresi için). Bu yine de O(n), sadece sabit (kesirli) bir faktördür (ve bunun etkili olması için, arama karakteri için karma fonksiyonlarını hesaplama ek yükünü en aza indirmek için daha büyük parçalar seçmeniz gerekir).

Özyinelemeli, ağaç benzeri bir yaklaşım kullanarak sizi yanınıza almalısınız 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)

Bu yapılandırmada, bir kişinin (yine, şanslı olduğumuzu ve filtrelerden birinden yanlış bir pozitif almadığı varsayılarak) kontrol edilmesi gerekir.

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

Son bölüme ulaşmak için (bulanana kadar 3 karakterin kontrol edilmesi gereken yer a).

İyi (yukarıdaki gibi daha iyi) bir alt bölüm şeması kullanarak bununla oldukça iyi sonuçlar almalısınız. (Not: Ağacın kökündeki çiçek filtreleri, düşük bir yanlış pozitif olasılığı elde etmek için örnekte gösterildiği gibi yapraklara yakın olmalıdır.)


Sevgili downvoter, lütfen cevabımın neden yardımcı olmadığını düşündüğünüzü açıklayınız.
Daniel Jour

1

Dize birden çok kez aranacaksa (tipik "arama" sorunu), çözüm O (1) olabilir. Çözüm bir dizin oluşturmaktır.

Örneğin :

Anahtarın Karakter ve Değer olduğu Harita, dizedeki o karakter için bir dizin listesidir.

Bununla, tek bir harita araması cevap verebilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.