Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 << zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch für Linux, Unix und Windows
– 2., aktualisierte und erweiterte Auflage 2006
Buch: C von A bis Z

C von A bis Z
1.116 S., mit CD, Referenzkarte, 39,90 Euro
Galileo Computing
ISBN 3-89842-643-2
gp Kapitel 24 Algorithmen
  gp 24.1 Was sind Algorithmen?
  gp 24.2 Wie setze ich Algorithmen ein?
  gp 24.3 Sortieralgorithmen
    gp 24.3.1 Selektion Sort – Sortieren durch Auswählen
    gp 24.3.2 Insertion Sort
    gp 24.3.3 Bubble Sort
    gp 24.3.4 Shellsort
    gp 24.3.5 Quicksort
    gp 24.3.6 qsort()
    gp 24.3.7 Zusammenfassung der Sortieralgorithmen
  gp 24.4 Suchalgorithmen – Grundlage zur Suche
    gp 24.4.1 Lineare Suche
    gp 24.4.2 Binäre Suche
    gp 24.4.3 Binäre (Such-)Bäume
    gp 24.4.4 Elemente im binären Baum einordnen
    gp 24.4.5 Binäre Bäume travesieren
    gp 24.4.6 Löschen eines Elements im binären Baum
    gp 24.4.7 Ein binärer Suchbaum in der Praxis
    gp 24.4.8 Binäre Suchbäume mit Eltern-Zeiger und Threads
    gp 24.4.9 Ausgeglichene Binärbäume
    gp 24.4.10 Algorithmen für ausgeglichene Bäume – eine Übersicht
  gp 24.5 Hashing (Zerhacken)
    gp 24.5.1 Wann wird Hashing verwendet?
    gp 24.5.2 Was ist für das Hashing erforderlich?
    gp 24.5.3 Hash-Funktion
    gp 24.5.4 Hashing mit direkter Adressierung
    gp 24.5.5 Vergleich von Hashing mit binären Bäumen
  gp 24.6 String-Matching
    gp 24.6.1 Brute-Force-Algorithmus
    gp 24.6.2 Der Algorithmus von Knuth/Morris/Pratt (KMP)
    gp 24.6.3 Weitere String-Matching-Algorithmen
  gp 24.7 Pattern Matching (reguläre Ausdrücke)
  gp 24.8 Backtracking
    gp 24.8.1 Der Weg durch den Irrgarten
    gp 24.8.2 Das 8-Dame-Problem


Galileo Computing - Zum Seitenanfang

24.6 String-Matching  downtop

Textverarbeitungsprogramme verwenden für die Bearbeitung von Texten Zeichenkettenfolgen in Form von einzelnen Buchstaben, Nummern oder Sonderzeichen.

Wenn Sie einen Texteditor oder Ähnliches entwickeln wollen, werden Sie auch Funktionen wie das Suchen von Strings oder Teilstrings benötigen; oder etwa die Syntaxhervorhebung einer bestimmten Programmiersprache. Eine weitere Möglichkeit ist das Pattern Matching, das Sie vielleicht aus Perl oder Shell von Linux kennen.

Für solche und weitere Anwendungsmöglichkeiten werden String-Matching-Algorithmen genutzt. Sie funktionieren nach folgendem Prinzip: In einer Textzeichenfolge, wie etwa dem Text in diesem Buch, soll mit einem Suchmuster die Häufigkeit der enthaltenen N-Zeichen und M-Zeichen verglichen werden.

Das Ziel des Kapitels ist es nicht, die Algorithmen anhand mathematischer Formeln zu erklären, sondern eher, die Algorithmen so zu erklären, dass ihr Prinzip, nach dem sie funktionieren, verstanden wird. Auf der Suche nach genaueren Beschreibungen werden Sie im Anhang dieses Buches unter Weiterführende Literatur fündig.

Als Schnittstelle zu diesen Beispielen soll eine Struktur verwendet werden, welche die Daten von der Kommandozeile entnimmt und für eventuelle Auswertungen speichert.

struct datei{
   char name[LEN];  /* Name der Datei  */
   int gefunden;    /* Anzahl gefunden */
};
typedef struct datei DATEI;

Sie können diese Struktur gern um weitere Informationen wie die Position der Fundstelle erweitern. In den Beispielen wird jeweils ein Array von Strukturen verwendet. In der Praxis können Sie auch verkettete Listen einsetzen.

Der Aufruf der Programme lautet hierbei immer:

programmname suchstring datei1 ... bis datei_n

Bei all den Zusätzen sollten Sie dennoch das Hauptaugenmerk auf die einzelnen Algorithmen richten. Alle Matching-Algorithmen suchen nach einer bestimmten Textfolge. Sofern Sie an ganzen Wörtern interessiert sind, können Sie den Algorithmus entsprechend anpassen. Dabei sollten Sie darauf achten, dass vor und nach dem Suchstring alle Whitespace-Zeichen beachtet werden (Newline, Tabulator und Space).


Galileo Computing - Zum Seitenanfang

24.6.1 Brute-Force-Algorithmus  downtop

Der einfachste Algorithmus liegt auf der Hand. Es werden alle infrage kommenden Positionen des Musters in einem Text überprüft, bis das Muster mit dem Text übereinstimmt oder das Ende des Texts gekommen ist. Das komplette Muster wird also beim Vergleich des Texts um eine Position nach vorn gezählt. Dies ist ein Brute-Force-Algorithmus (oder auch grober Algorithmus oder naiver Algorithmus). Hier das simple Beispiel:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.30   Ablauf des Brute-Force-Algorithmus

Jetzt der Quellcode zu diesem einfachen String-Matching-Algorithmus:

/* bruteforce.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define LEN 255
#define MAX_DAT 10
#define MAX_LINE 4096
#define LINE "---------------------------------------\n"
struct datei{
   char name[LEN];  /* Name der Datei  */
   int gefunden;    /* Anzahl gefunden */
};
typedef struct datei DATEI;
/* int i = der Textzählerstand
 * int j = der Musterzählerstand */
int BruteForce(char *muster, char *text) {
   int i = 0, j, cnt = 0;
   int m=strlen(muster); /* Länge Muster */
   int n=strlen(text);   /* Länge Text   */
   while (i<=n-m) {      /* Solange i kleiner als n-m zum Ende */
      /* Solang Muster und Text gleich j++ */
      for(j=0; j<m && muster[j]==text[i+j]; j++);
      if(j==m) { /* Ist die Länge von j gleich der vom Muster? */
         printf("Pos. %3d, ",i);
         cnt++;
      }
      i++; /* Im Text eine Position weiter */
   }
   return cnt;
}
int main(int argc, char **argv) {
   DATEI suche[MAX_DAT];
   char suchstring[LEN];
   char read_line[MAX_LINE];
   FILE *f;
   int i, j , ret, zeile;
   if(argc < 3) {
      fprintf(stderr, "Verwendung: %s suchstring datei1"
         " <datei2>  – <datei%d>\n",argv[0],MAX_DAT);
      return EXIT_FAILURE;
   }
   strncpy(suchstring, argv[1], LEN);
   /* Kommandozeilen-Argumente auswerten */
   for(i=2,j=0; j < MAX_DAT && i < argc; i++,j++) {
      strncpy(suche[j].name, argv[i], LEN);
      suche[j].gefunden = 0;
   }
   for(i = 0; i < argc-2; i++) {
      f = fopen(suche[i].name, "r");
      if(f == NULL) {
         perror(NULL);
         continue;
      }
      zeile = 0;
      printf("\nDatei \"%s\": \n",suche[i].name);
      while( fgets(read_line, MAX_LINE, f) != NULL) {
         zeile++;
         ret = BruteForce(suchstring, read_line);
         if(ret != 0) {
            suche[i].gefunden+=ret;
            printf(" in Zeile %d\n",zeile);
            ret = 0;
         }
      }
      printf("Suchergebnisse in \"%s\": %d\n",
         suche[i].name, suche[i].gefunden);
      printf(LINE);
      fclose(f);
   }
   return EXIT_SUCCESS;
}

Um als Beispiel den Suchstring "ex" und als Muster "a example text" zu verwenden: Die innere for-Schleife wird dabei nur dreimal inkrementiert, und zwar bei jedem Vorkommen des Buchstabens 'e'. Zweimal wird davon ein Ergebnis gefunden. Die Laufzeit des Algorithmus ist natürlich abhängig vom Suchmuster, aber im Durchschnitt hat der Brute-Force-Algorithmus immer ein lineares Zeitverhalten.


Galileo Computing - Zum Seitenanfang

24.6.2 Der Algorithmus von Knuth/Morris/Pratt (KMP)  downtop

Der Nachteil des Brute-Force-Algorithmus‘ ist der, dass dieser stur Zeichen für Zeichen, Position um Position vergleicht. Die Programmierer Knuth, Morris und Pratt, nach denen dieser Algorithmus auch benannt ist, haben diesen Algorithmus verbessert (verfeinert). Sie hatten die Idee, die Fehlvergleiche (so genanntes Missmatch) in den weiteren Algorithmus mit einzubeziehen. Als Beispiel sei diese Textfolge gegeben (text):

lu lalalala lule lulalalas

Der Suchstring (suchmuster) lautet alalas. Der Vorgang beginnt wie beim Brute-Force-Algorithmus:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.31   Auf der Suche nach dem Suchstring »alalas« – Start

Hier haben Sie zwischen text[i] und suchmuster[j] keine Übereinstimmung. Daher kann suchmuster um eine Position weitergeschoben werden:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.32   Keine Übereinstimmung – Suchstring eine Postion weiter

Dies wird so lange wiederholt, bis zwei gleiche Zeichen aufeinander treffen:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.33   Erstes Zeichen stimmt überein

Solange text[i] jetzt gleich mit suchmuster[j] ist, werden i und j inkrementiert:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.34   Nach fünf Zeichen tritt ein Fehlvergleich auf

An Position text[9] und suchmuster[5] tritt hier eine Ungleichheit auf. Beim Brute-Force-Algorithmus würde jetzt das Muster wieder um eine Position nach vorn gesetzt werden. Und genau hier greift der Algorithmus von Knuth, Morris und Pratt ein. Die kleinstmögliche Verschiebung, bei der »alalas« sich mit sich selbst deckt, ist um zwei Stellen:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.35   Kleinstmögliche Verschiebung des Suchstrings selbst

Im nächsten Schritt werden dabei auch zwei Stellen weitergeschoben, da Sie ja nun wissen, dass sich der Anfang nicht überlappt. Genauer gesagt, Sie wissen es jetzt, weil ich es Ihnen gesagt habe, aber nicht wie dies programmtechnisch geschieht. Hierfür ist eine Vorlaufphase erforderlich. Aber dazu gleich mehr.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 24.36   Verschiebung um zwei Stellen anstatt um eine

In der Theorie hört sich das alles natürlich recht interessant an. Aber es in die Praxis umzusetzen, ist wesentlich komplizierter. Wie realisieren Sie eine kleinstmögliche Verschiebung um den Suchstring (suchmuster) selbst?

Da die Berechnung einer solchen Verschiebung nicht vom Text abhängig ist, in dem die Suche stattfindet, sondern nur vom Suchtext (suchmuster), kann sie schon vor der eigentlichen Suche erstellt werden. Es wird dabei von einer Vorlaufphase (Preprocessing) gesprochen. Hier der Algorithmus, welcher eine Sprungtabelle aus dem Suchstring selbst für den eigentlichen Suchalgorithmus erstellt.

void init_next(char *suchmuster, int m) {
   int i, j;
   i = 0;
   j = next[0] = –1;
   /* Solange i kleiner als der Suchstring ist */
   while (i < m) {
      while (j > –1 && suchmuster[i] != suchmuster[j])
         j = next[j];
      i++;
      j++;
      if (suchmuster[i] == suchmuster[j])
         next[i] = next[j];
      else
         next[i] = j;
   }
}

Um nochmals zum Szenario zu kommen: Sie verwenden gerade einen Brute-Force-Algorithmus und vergleichen einzelne Zeichen. Findet jetzt ein Missmatch (suchmuster!=text) statt, wird in den bisher gefundenen und übereinstimmenden Zeichen von hinten ein String mit maximaler (L) Länge gesucht, der zugleich Anfang eines weiteren Strings ist. Danach wird das Zeichen mit L+1 (=next[j]) und dem i-ten Zeichen im Text verglichen. Zwei Möglichkeiten gibt es dafür:

gp  Das Zeichen next[j] des Suchstrings stimmt nicht mit dem i-ten Zeichen des Texts überein. Somit wird entweder ganz normal wie beim Brute-Force-Algorithmus fortgefahren, bis der ganze String gefunden wurde oder erneut ein Missmatch auftritt. In diesem Fall wird genauso fortgefahren wie beim ersten Missmatch.
gp  Das Zeichen next[j] des Suchstrings stimmt nicht mit dem i-ten Zeichen des Texts überein. next[j] wird somit um den Wert 1 reduziert, und der Vergleich geht weiter. Tritt wieder ein Missmatch auf, wird next[j] dekrementiert, bis das erste Zeichen des Suchstrings erreicht wurde. In diesem Fall wird i inkrementiert und das Ganze beginnt von vorne.

Mit next[i] = j stellen Sie sicher, dass j–1 die Länge des größten Endstücks, aber auch das Anfangsstück des Suchstrings ist. Ist suchmuster[i] gleich suchmuster[j], wird mit next[++i]=++j der Wert zugewiesen, da das next-Array immer das nächste Zeichen beinhaltet. Ist dies nicht der Fall, wird der Anfangsteil und das längste Endteil mit dem Muster verglichen, bis es zu einem positiven Vergleich zwischen muster[i] und muster[j] kommt.

Nun noch der eigentliche Algorithmus mit dem Suchstring und dem Textbeispiel, welches eben verwendet wurde (alalas).

/* kmp.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 4096
void init_next(char *, int);
void kmpSearch(char *, char *);
int next[MAX];
/* i = Position im Text */
/* j = Position im Muster */
void kmpSearch(char *muster, char *text) {
   int i=0, j=0;
   int m=strlen(muster);  /* Länge Muster */
   int n=strlen(text);    /* Länge Text   */
   init_next(muster, m);  /* Tabelle für next berechnen */
   while (i<n) {  /*Solange wir nicht am Ende vom Text sind*/
      while (j>=0 && text[i]!=muster[j])j=next[j];
      i++; j++;
      if (j==m) {
         printf("Gefunden an Pos. %d\n", i-j);
         j = next[j];
      }
   }
}
void init_next(char *muster, int m) {
   int i, j;
   i = 0;
   j = next[0] = –1;
   /* Solange i kleiner als der Suchstring ist */
   while (i < m) {
      while (j > –1 && muster[i] != muster[j])
         j = next[j];
      i++;
      j++;
      (muster[i] == muster[j]) ? (next[i]=next[j]) : (next[i]=j);
   }
}
int main(void)  {
   kmpSearch("alalas", "lu lalalala lule lulalalas");
   return EXIT_SUCCESS;
}

Das vollständige Listing des »Brute-Force-Algorithmus« umgeschrieben auf das Programmbeispiel:

/* kmpsearch.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LEN 255
#define MAX_DAT 10
#define MAX_LINE 4096
#define MAX 255
#define LINE "_______________________________________\n"
struct datei{
   char name[LEN];  /* Name der Datei  */
   int gefunden;    /* Anzahl gefunden */
};
typedef struct datei DATEI;
int next[MAX];
int kmp_Search(char *, char *);
void init_next(char *, int);
int kmp_Search(char *muster, char *text) {
   int i=0, j=0, cnt=0;
   int m=strlen(muster);  /* Länge Muster */
   int n=strlen(text);    /* Länge Text */
   init_next(muster, m);  /* Tabelle für next berechnen */
   while (i<n) {   /* Solange wir nicht am Ende vom Text sind */
      while (j>=0 && text[i]!=muster[j])j=next[j];
      i++; j++;
      if (j==m) {
         printf("Gefunden an Pos. %d\n", i-j);
         cnt++;
         j=next[j];
      }
   }
   return cnt;
}
void init_next(char *muster, int m) {
   int i, j;
   i = 0;
   j = next[0] = –1;
   /* Solange i kleiner als der Suchstring ist */
   while (i < m) {
      while (j > –1 && muster[i] != muster[j])
         j = next[j];
      i++;
      j++;
      (muster[i]==muster[j]) ? (next[i]=next[j]) : (next[i]=j);
   }
}
int main(int argc, char **argv) {
   DATEI suche[MAX_DAT];
   char suchstring[LEN];
   char read_line[MAX_LINE];
   FILE *f;
   int i, j , ret, zeile;
   if(argc < 3) {
      fprintf(stderr, "Verwendung: %s suchstring datei1 "
         "<datei2> ... <datei%d>\n",argv[0],MAX_DAT);
      return EXIT_FAILURE;
   }
   strncpy(suchstring, argv[1], LEN);
   /* Kommandozeilen-Argumente auswerten */
   for(i=2,j=0; j < MAX_DAT && i < argc; i++,j++) {
      strncpy(suche[j].name, argv[i], LEN);
      suche[j].gefunden = 0;
   }
   for(i = 0; i < argc-2; i++) {
      f = fopen(suche[i].name, "r");
      if(f == NULL) {
         perror(NULL);
         continue;
      }
      zeile = 0;
      printf("\nDatei \"%s\": \n",suche[i].name);
      while( fgets(read_line, MAX_LINE, f) != NULL) {
         zeile++;
         ret = kmp_Search(suchstring, read_line);
         if(ret != 0) {
            suche[i].gefunden+=ret;
            printf(" in Zeile %d\n",zeile);
            ret=0;
         }
      }
      printf("Suchergebnisse in \"%s\": %d\n",
         suche[i].name, suche[i].gefunden);
      printf(LINE);
      fclose(f);
   }
   return EXIT_SUCCESS;
}

Der eine oder andere wird mir jetzt entgegnen, dass Kapitel sei viel zu schwer für ein Einsteiger-Buch. Im Prinzip muss ich dem zustimmen. Allerdings möchte ich zum Knuth-Morris-Pratt-Algorithmus sagen, dass hierbei versucht wurde, diesen Algorithmus möglichst einfach zu erklären, ohne viele technische Begriffe aus der Welt der Mathematik und Informatik. Für Hardcore-Programmierer, die mehr Details benötigen, sei auf die weiterführenden Links und die Literatur im Anhang verwiesen.

Außerdem soll nicht unerwähnt bleiben, dass der Knuth-Morris-Pratt-Algorithmus immer noch einer der leichteren String-Matching-Algorithmen ist.


Galileo Computing - Zum Seitenanfang

24.6.3 Weitere String-Matching-Algorithmen  toptop

Boyer Moore

Der Boyer-Moore-Algorithmus ähnelt dem Brute-Force und ist eine weitere Verbesserung gegenüber dem Knuth-Morris-Pratt-Algorithmus. Auch mit diesem Algorithmus werden Verschiebungen vorgenommen, und nicht mehr infrage kommende Verschiebungen werden übersprungen. Hierfür sind gleich zwei Vorlaufphasen (Preprocessing) notwendig. Genauer gesagt zwei Heuristiken, welche die Schrittweite bei der nächsten Verschiebung vorschlagen:

gp  Schlechter-Buchstabe-Heuristik – Dabei wird untersucht, ob ein Zeichen im Text, welches nicht mehr mit dem Pattern übereinstimmt, an einer anderen Stelle im Pattern vorkommt, und dann wird eine entsprechende Schrittweite vorgeschlagen.
gp  Gutes-Suffix-Heuristik – Dabei wird untersucht, ob das Suffix des Patterns, welches mit dem Text übereinstimmt, an einer anderen Stelle vorkommt, und dann wird auch hier eine entsprechende Schrittweite vorgeschlagen.

Karp Rabin

Jeder mögliche String des Musters wird in eine Hash-Tabelle eingetragen. Dafür wird eine spezielle Hash-Funktion geschrieben, welche die Eigenschaft hat, dass sie bei dem Text für Startindex i effizient aus dem vorhergehenden Hashwert (Startindex = i-1) berechnet werden kann. Sind dabei zwei Hashwerte gleich, wird wie beim Brute-Force-Algorithmus vorgegangen. Dies funktioniert nach folgendem Pseudocode:

hash_pattern = hash_wert_des_pattern
hash_text    = hash_wert_der_ersten_m_Zeichen_im_text
do {
   if(hash_pattern == hash_text)
      bruteforce_vergleich
   hash_text = hash_wert_der_nächsten_m_Zeichen_des_Textes
} while(Text_zu_Ende || bruteforce == wahr);
 << zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z
C von A bis Z
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Shell-Programmierung






 Shell-Programmierung


Zum Katalog: Linux-UNIX-Programmierung






 Linux-UNIX-Programmierung


Zum Katalog: C/C++






 C/C++


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Reguläre Ausdrücke






 Reguläre Ausdrücke


Zum Katalog: Linux






 Linux


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de