Esiste un buon algoritmo di ricerca per un singolo personaggio?


23

Conosco diversi algoritmi di corrispondenza delle stringhe di base come KMP o Boyer-Moore, ma tutti quelli analizzano il modello prima di cercare. Tuttavia, se uno ha un singolo carattere, non c'è molto da analizzare. Quindi esiste un algoritmo migliore della ricerca ingenua di confrontare ogni carattere del testo?


13
Puoi lanciare istruzioni SIMD, ma non otterrai nulla di meglio di O (n).
Codici A Caos

7
Per una singola ricerca o più ricerche nella stessa stringa?
Christophe

KMP non è sicuramente qualcosa che definirei un algoritmo "di base" per la corrispondenza delle stringhe ... Non sono nemmeno sicuro che sia così veloce, ma è storicamente importante. Se vuoi qualcosa di base prova l'algoritmo Z.
Mehrdad,

Supponiamo che ci sia una posizione di carattere che l'algoritmo di ricerca non ha osservato. Quindi non sarebbe in grado di distinguere tra stringhe con il carattere dell'ago in quella posizione e stringhe con un carattere diverso in quella posizione.
user253751

Risposte:


29

Resta inteso che il caso peggiore è che O(N)ci sono alcune microottimizzazioni molto belle.

Il metodo ingenuo esegue un confronto tra caratteri e un confronto di fine testo per ciascun carattere.

L'uso di una sentinella (ovvero una copia del carattere di destinazione alla fine del testo) riduce il numero di confronti a 1 per carattere.

A livello di bit twiddling c'è:

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

per sapere se un byte in una parola ( x) ha un valore specifico ( n).

La sottoespressione restituisce v - 0x01010101ULun bit alto impostato in qualsiasi byte ogni volta che il byte corrispondente in vè zero o maggiore di 0x80.

La sottoespressione ~v & 0x80808080ULvaluta i bit alti impostati in byte in cui il byte di vnon ha il bit alto impostato (quindi il byte era inferiore a 0x80).

Andando su queste due sottoespressioni ( haszero) il risultato sono i bit alti impostati in cui i byte verano zero, poiché i bit alti impostati a causa di un valore maggiore rispetto 0x80alla prima sottoespressione vengono mascherati dal secondo (27 aprile, 1987 di Alan Mycroft).

Ora possiamo XOR il valore di test ( x) con una parola che è stata riempita con il valore di byte a cui siamo interessati ( n). Poiché XORing di un valore con se stesso comporta un byte zero e un valore diverso da zero in caso contrario, possiamo passare il risultato a haszero.

Questo è spesso usato in strchrun'implementazione tipica .

(Stephen M Bennet lo ha suggerito il 13 dicembre 2009. Ulteriori dettagli nel noto Bit Twiddling Hacks ).


PS

questo codice è rotto per qualsiasi combinazione di 1111"a"0

L'hack ha superato il test della forza bruta (basta essere pazienti):

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

Molti voti positivi per una risposta che rende l'assunzione un carattere caratteristico = un byte, che al giorno d'oggi non è più lo standard

Grazie per l'osservazione.

La risposta doveva essere tutt'altro che un saggio sulle codifiche multi-byte / a larghezza variabile :-) (in tutta onestà non è la mia area di competenza e non sono sicuro che sia ciò che l'OP stava cercando).

Ad ogni modo, mi sembra che le idee / i trucchi di cui sopra possano essere in qualche modo adattati a MBE (in particolare le codifiche auto-sincronizzanti ):

  • come osservato nel commento di Johan, l'hack può essere "facilmente" esteso per funzionare con doppi byte o altro (ovviamente non si può allungare troppo);
  • una funzione tipica che individua un carattere in una stringa di caratteri multibyte:
  • la tecnica sentinella può essere utilizzata con un po 'di lungimiranza.

1
Questa è la versione di un uomo povero dell'operazione SIMD.
Ruslan,

@Ruslan Absolutely! Questo è spesso il caso di efficaci hack di manipolazione dei bit.
manlio

2
Bella risposta. Da un aspetto della leggibilità, non capisco perché scrivi 0x01010101ULin una riga e ~0UL / 255in quella successiva. Dà l'impressione che debbano essere valori diversi, poiché altrimenti, perché scriverlo in due modi diversi?
hvd

3
Questo è interessante perché controlla 4 byte contemporaneamente, ma richiede più istruzioni (8?), Poiché la #defines si espanderebbe a ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL ). Il confronto a byte singolo non sarebbe più veloce?
Jed Schaaf,

1
@DocBrown, il codice può essere facilmente fatto funzionare per doppi byte (ad esempio mezzitoni) o stuzzichini o altro. (tenendo conto dell'avvertenza che ho citato).
Johan - ripristina Monica il

20

Qualsiasi algoritmo di ricerca di testo che cerca ogni ricorrenza di un singolo carattere in un dato testo deve leggere ogni carattere del testo almeno una volta, questo dovrebbe essere ovvio. E poiché questo è sufficiente per una ricerca singola, non può esserci un algoritmo migliore (se si pensa in termini di ordine di runtime, che in questo caso viene chiamato "lineare" o O (N), dove N è il numero di caratteri per cercare).

Tuttavia, per implementazioni reali, ci sono sicuramente molte micro-ottimizzazioni possibili, che non cambiano l'ordine del tempo di esecuzione nel suo complesso, ma riducono il tempo di esecuzione effettivo. E se l'obiettivo non è quello di trovare ogni ricorrenza di un singolo personaggio, ma solo il primo, puoi fermarti alla prima occorrenza, ovviamente. Tuttavia, anche in quel caso, il caso peggiore è che il personaggio che stai cercando è l'ultimo carattere nel testo, quindi l'ordine di esecuzione del caso peggiore per questo obiettivo è ancora O (N).


8

Se il tuo "pagliaio" viene cercato più di una volta, un approccio basato sull'istogramma sarà estremamente veloce. Dopo aver creato l'istogramma, devi solo cercare un puntatore per trovare la tua risposta.

Se hai solo bisogno di sapere se è presente il modello cercato, un semplice contatore può aiutarti. Può essere esteso per includere le posizioni in cui si trova ogni personaggio nel pagliaio o la posizione della prima occorrenza.

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

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

1

Se è necessario cercare caratteri in questa stessa stringa più di una volta, un possibile approccio consiste nel dividere la stringa in parti più piccole, possibilmente in modo ricorsivo, e utilizzare i filtri di fioritura per ciascuna di queste parti.

Poiché un filtro bloom può dirti con certezza se un carattere non si trova nella parte della stringa "rappresentata" dal filtro, puoi saltare alcune parti durante la ricerca di caratteri.

Ad esempio: per la seguente stringa è possibile dividerlo in 4 parti (ciascuna lunga 11 caratteri) e riempire per ciascuna parte un filtro bloom (forse grande 4 byte) con i caratteri di quella parte:

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

Puoi velocizzare la tua ricerca, ad es. Per il personaggio a: usando buone funzioni hash per i filtri bloom, ti diranno che - con alta probabilità - non devi cercare né nella prima, nella seconda né nella terza parte. In questo modo ti risparmi di controllare 33 caratteri e invece devi solo controllare 16 byte (per i 4 filtri bloom). Questo è ancora O(n), solo con un fattore costante (frazionario) (e affinché questo sia efficace dovrai scegliere parti più grandi, per ridurre al minimo il sovraccarico del calcolo delle funzioni hash per il carattere di ricerca).

L'uso di un approccio ricorsivo a forma di albero dovrebbe farti avvicinare 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)

In questa configurazione è necessario (di nuovo, supponendo che siamo stati fortunati e non abbiamo ottenuto un falso positivo da uno dei filtri) da controllare

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

per arrivare alla parte finale (dove è necessario controllare 3 caratteri fino a trovare il a).

Usando uno schema di suddivisione buono (meglio come sopra), dovresti ottenere risultati piuttosto carini con quello. (Nota: i filtri di fioritura alla radice dell'albero dovrebbero essere più grandi che vicini alle foglie, come mostrato nell'esempio, per ottenere una bassa probabilità di falsi positivi)


Caro downvoter, ti preghiamo di spiegare perché pensi che la mia risposta non sia utile.
Daniel Jour,

1

Se la stringa verrà cercata più volte (tipico problema di "ricerca"), la soluzione può essere O (1). La soluzione è costruire un indice.

Per esempio :

Mappa, dove Chiave è il carattere e Valore è un elenco di indici per quel carattere nella stringa.

Con questo, una singola ricerca della mappa può fornire la risposta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.