Boyer-Moore-Algorithmus

Einleitung

Der naive Algorithmus wie auch der Knuth-Morris-Pratt-Algorithmus setzen darauf erfolgreich ein Suchmuster zu finden. Das scheint sinnvoll, wenn das Alphabet klein ist und damit die Chance hoch ist, bei der Suche erfolg zu haben. Bei genauerer Betrachtung ist die Chance einen Treffer zu landen jedoch eher gering. Bei einem Unicode-Alphabet können 65336 Zeichen auftreten, die Chance das richtige zu finden ist also 1:65335. In einem ASCII-Text ist die Chance 1:255. Nimmt man nur kleine Buchstaben ist die Chance den richtigen Buchstaben 1:25. Beschränkt man sich auf binäre Zeichen (true oder false) ist die Chance 1:1. Damit die Chance das richtige Zeichen zu finden größer ist, als ein falsches, darf es keine falschen Zeichen gehen.

Hier setzen Boyer-Moore und optimieren die Textsuche für den Regelfall: Sie nutzen die hohe Chance ein falsches Zeichen zu finden, das garantiert, dass man an dieser Stelle das Suchmuster nicht finden wird.

Die Idee

Nehmen wir uns wie beim Knuth-Morris-Pratt-Algorithmus das Wort 'ananas' vor und suchen es im Text „anabellmagananas.“ (Anabell mag Ananas.):

Schritt a n a b e l l m a g a n a n a n a s
1 a n a n a s

Wenn wir jetzt davon ausgehen, dass wir das Richtige Wort gefunden haben, werden solange wir 'ana' analysiert haben, auch davon überzeugt bleiben. Beim zweiten 'n' werden wir im Text aber das 'b' finden und entsprechend enttäuscht werden. Boyer-Moore macht eine andere Annahme: Ich will das Suchmuster nicht finden und daher schnellst möglich weiter kommen. Hierfür beginnt der Vergleich nicht vorne, sondern hinten:

Schritt a n a b e l l m a g a n a n a n a s
1 a n a n a s

Nun spielt es zunächst keine Rolle, ob der Vergleich vorne oder hinten fehlschlägt. Wir müssten das Suchmuster nun verschieben, um zu sehen, ob der nächste Buchstabe ein 's' ist:

Schritt a n a b e l l m a g a n a n a n a s
1 a n a n a s
2 a n a n a s

Aber auch hier können wir uns zunächst das Suchmuster ansehen und schauen, ob wir daraus Erkenntnisse ziehen können. Der erste Vergleich schlägt bei einem 'l' fehl. Das Suchmuster 'ananas' enthält aber kein 'l'. Das bedeutet, dass das Suchmuster vor dem 'l' einfach nicht reinpasst und vor dem 'l' auch kein Anfang des Suchmusters mehr zu finden sein wird, weil sonst das 'l' ja im Suchmuster auftauchen müsste. Und das bedeutet, dass bei einem Mismatch mit einem 'l', das Suchmuster komplett verschoben werden darf:

Schritt a n a b e l l m a g a n a n a n a s
1 a n a n a s
2 a n a n a s

Wir sind hier mit einem einzigen Vergleich also gleich an Position 6 gelandet. Der nächste Vergleich findet mit einem 'n' statt. Um mit dem nächsten 'n' zu vergleichen muss das Suchmuster zwei Buchstaben verrückt werden:

Schritt a n a b e l l m a g a n a n a n a s
1 a n a n a s
2 a n a n a s
3 a n a n a s
4 a n a n a s
5 a n a n a s

Zufälligerweise finden wir zwei weitere 'n', können uns also wie beim naiven Algorithmus oder Knuth-Morris-Pratt-Algorithmus nicht nur einen Schritt weiterbewegen, sondern gleich in Zweierschritten durch den Text gehen. Nach 5 Schritten haben wir einen Treffer mit dem 's' gelandet. Nun müssen wir den Vergleich starten.

Schritt a n a b e l l m a g a n a n a n a s
5 a n a n a s
6 a n a n a s
7 a n a n a s
8 a n a n a s
9 a n a n a s
10 a n a n a s

Würden wir die Ananas hier doch nicht finden, so müssten wir das Suchmuster um eins nach Rechts verschieben und es würde wieder mit dem 's' verglichen werden.

Das Bespiel

Wir suchen das Suchmuster „ananas“ im Text „anabellmagananasananabolika.“ (Anabell mag Ananas an Anabolika).

Die Präfix-Untersuchung

Erklärung

Ähnlich des Knuth-Morris-Pratt-Algorithmus muss zunächst das Suchmuster untersucht werden. Damit wir hier nicht mit tausenden Argumenten jonglieren müssen, packen wir zunächst alles, was zum Suchmuster gehört in eine eigene kleine Struktur:

struct Pattern
{
    char const   * Pattern;
    unsigned int   Length;
    int          * Table;
};

Hier können wir die Tabelle speichern, die die Verschiebung im Suchmuster speichert und uns die Länge des Suchmusters merken, so dass wir die sie nicht laufend neu ermitteln müssen.

Wir erzeugen nun ein Suchmuster, indem wir eine Pattern-Struktur anlegen und die Adresse des Suchmusters in die Struktur schreiben:

struct Pattern pat;
pat.Pattern = "ababcabab";

Diesmal sieht unsere Tabelle allerdings anders aus, wir müssen nicht auf die Position im Pattern zugreifen, sondern wir brauchen eine Tabelle, die für alle Zeichen im Alphabet mit der Länge des Patterns initialisiert wird. Anschließend gehen wir die das Suchmuster von links nach rechts durch und tragen für das jeweilige Zeichen die Entfernung zum 's' ein, also die Differenz aus Länge des Patterns und Position des Buchstabens im Pattern.

Schritt Position Buchstabe Wert
1 0 a 6-1 Wird ein 'a' gefunden, so wird das Pattern um 5 Buchstaben verschoben.
2 1 n 6-2 Wird ein 'n' gefunden, so wird das Pattern um 4 Buchstaben verschoben.
3 2 a 6-3 Wird ein 'a' gefunden, so wird das Pattern um 3 Buchstaben verschoben, der alte Wert(6) wird damit überschrieben.
4 3 n 6-4 Wird ein 'a' gefunden, so wird das Pattern um 2 Buchstaben verschoben, der alte Wert(5) wird damit überschrieben.
5 4 a 6-5 Wird ein 'a' gefunden, so wird das Pattern um 1 Buchstaben verschoben, der alte Wert(6) wird damit überschrieben.
6 5 s 6-6 Treffer!

Implementierung

 

Die Suche

Erklärung

Implementierung

Weiterführendes

Quelltext