10. Kapitel: Syntaxanalyse (Parsing) Es wurden verschiedene grundlegende Algorithmen entwickelt, um zulässige (korrekte ) Programme zu erkennen und sie in einer Weise zu zerlegen, die eine weitere Verarbeitung ermöglicht. Diese Operation, die Syntaxanalyse oder Parsing genannt wird, besitzt auch außerhalb der Informatik Anwendungen, da sie unmittelbar mit der Untersuchung der Struktur von Sprachen im Allgemeinen zusammenhängt. Z. B. spielt die Syntaxanalyse in Systemen, die versuchen, natürliche ( menschliche ) Sprachen zu verstehen, eine ebenso wichtige Rolle wie in Systemen für die Übersetzung einer Sprache in eine andere. Von Interesse ist auch der Spezialfall der Übersetzung aus einer höheren Programmiersprache, wie etwa C ( geeignet für die Benutzung durch den Anwender), in eine Assembler- oder Maschinensprache (geeignet für die Ausführung durch den Computer). Ein Programm für die Realisierung einer solchen Übersetzung wird Compiler genannt. G.Heyer 1 Algorithmen und Datenstrukturen II Zwei grundsätzliche Vorgehensweisen werden für die Syntaxanalyse genutzt. Top-Down-ablaufende Methoden überprüfen ein Programm auf Zulässigkeit, indem sie zuerst die Teile eines zulässigen Programms bestimmen, dann Teile von Teilen usw., bis die Teile klein genug sind, um direkt auf Übereinstimmung mit den Eingabedaten geprüft werden können. Bottom-up-Methoden setzen Teile der Eingabedaten in einer strukturierten Weise zusammen, dass immer größere Teilstücke entstehen, bis ein zulässiges Programm konstruiert worden ist. Im Allgemeinen sind Top-Down-Methoden rekursiv, sie lassen sich leichter implementieren, Bottom-up-Methoden dagegen sind iterativ, sie gelten als effizienter. G.Heyer 2 Algorithmen und Datenstrukturen II Kontextfreie Grammatiken Bevor man ein Programm zur Bestimmung der Zulässigkeit eines in einer gegebenen Sprache erstellten Programmes schreiben kann, benötigt man eine Beschreibung, die angibt, woraus genau ein zulässiges Programm zusammengesetzt ist. Diese Beschreibung wird Grammatik genannt. Programmiersprachen werden oft durch einen speziellen Grammatiktyp beschrieben, der kontextfreie Grammatik genannt wird. Als Beispiel wird die kontextfreie Grammatik angegeben, die die Menge aller zulässigen regulären Ausdrücke definiert. <Ausdruck> ::= <Term> | <Term> + <Ausdruck> <Term> ::= <Faktor> | <Faktor> <Term> <Faktor> ::= (<Ausdruck>) | v | (<Ausdruck>)* | v* G.Heyer 3 Algorithmen und Datenstrukturen II Diese Grammatik beschreibt reguläre Ausdrücke wie etwa ( 1 + 01 )*(0 + 1 ) oder ( A*B + AC)D. Jede Zeile in der Grammatik wird eine Produktion (production) oder Regel genannt. Die Produktionen bestehen aus den in der beschriebenen Sprache benutzten terminalen Symbolen ( , ) , + und * („v“, ein spezielles Symbol, steht für einen beliebigen Buchstaben oder eine beliebige Ziffer ), weiterhin aus den nichtterminalen Symbolen <Ausdruck>, <Term> und <Faktor>, die interne Symbole der Grammatik sind, sowie aus den Metasymbolen ::= und |, die verwendet werden, um die Bedeutung der Produktionen zu beschreiben. Das Symbol ::= , das „ist ein“ gelesen werden kann, definiert die linke Seite der Produktion mit Hilfe der rechten Seite, und das Symbol | , welches als „oder“ gelesen werden kann, gibt mögliche Alternativen an. G.Heyer 4 Algorithmen und Datenstrukturen II Die verschiedenen Produktionen entsprechen trotz dieser knappen Schreibweise auf einfache Weise einer intuitiven Beschreibung der Grammatik. Beispielsweise könnte die zweite Produktion im angebenen Beispiel einer Grammatik wie folgt gelesen werden: „ Ein <Term> ist eine <Faktor> oder ein <Faktor>, dem ein <Term> folgt“. Ein nichtterminales Symbol, in diesem Falle <Ausdruck>, ist in dem Sinne herausragend, dass eine Folge von terminalen Symbolen dann und nur dann der durch die Grammatik beschriebenen Sprache angehört, wenn es eine Möglichkeit gibt, unter Anwendung der Produktionen diese Folge aus dem herausragenden nichtterminalen Symbol abzuleiten, indem man (in beliebig vielen Schritten ) ein nichtterminales Symbol durch irgendeine der Alternativen auf der rechten Seite einer Produktion für dieses nichtterminale Symbol ersetzt. G.Heyer 5 Algorithmen und Datenstrukturen II Ein natürlicher Weg zur Beschreibung des Ergebnisses dieses Ableitungsprozesses ist ein Syntaxbaum (parse tree), ein Diagramm der vollständigen grammatischen Struktur der Zeichenfolge, für die die Syntaxanalyse vorgenommen wird. Beispielsweise zeigt der unten dargestellte Syntaxbaum, dass die Zeichenfolge (A*B + AC)D in der durch die obige Grammatik beschriebenen Sprache enthalten ist. Syntaxbäume dieser Art werden manchmal für die deutsche Sprache benutzt, um einen Satz in Subjekt, Prädikat, Objekt usw. zu zerlegen. G.Heyer 6 Algorithmen und Datenstrukturen II Syntaxbaum für (A*B +AC)D Ausdruck Term Faktor Term Ausdruck ( Term ) + Faktor Ausdruck Term Faktor D A * Term Faktor Faktor Term B A Faktor C G.Heyer 7 Algorithmen und Datenstrukturen II Die Hauptaufgabe eines Parsers besteht darin, Zeichenfolgen, die auf diese Weise abgeleitet werden können, anzunehmen bzw. die, die bei denen das nicht möglich ist, zurückzuweisen, indem er versucht, für eine beliebige gegebene Zeichenfolge einen Syntaxbaum zu konstruieren. Das bedeutet, dass der Parser erkennen kann, ob eine Zeichenfolge in der durch die Grammatik beschriebenen Sprache enthalten ist, indem er feststellt, ob für die Zeichenfolge ein Syntaxbaum existiert oder nicht. Top-Down-Parser tun dies, indem sie den Baum in der Weise aufbauen, dass sie mit dem herausragenden nichtterminalen Symbol oben beginnen und dann abwärts in Richtung auf die unten befindliche zu erkennende Zeichenfolge vorgehen. Bottom-up-Parser arbeiten, indem sie mit der Zeichenfolge unten beginnen und rückwärts nach oben in Richtung auf das herausragende nichtterminale Symbol vorgehen. G.Heyer 8 Algorithmen und Datenstrukturen II Ein weiteres Beispiel für eine kontextfreie Grammatik beschreibt zulässige C-Programme. Die betrachteten Prinzipien für die Erkennung und Verwendung zulässiger Ausdrücke lassen sich unmittelbar auf die komplexe Aufgabe der Compilierung und Ausführung von C-Programmen anwenden. Z. B. beschreibt die folgende Grammatik eine sehr kleine Teilmenge von C, nämlich arithmetische Ausdrücke, in denen Addition und Multiplikation vorkommen: <Ausdruck> ::= <Term> | <Term> + <Ausdruck> <Term> ::= <Faktor> | <Faktor> * <Term> <Faktor> ::= (<Ausdruck>) | v G.Heyer 9 Algorithmen und Datenstrukturen II Diese Regeln legen fest, woraus „zulässige“ arithmetische Ausdrücke bestehen. Auch hier ist v ein spezielles Symbol, das für einen beliebigen Buchstaben steht, doch in dieser Grammatik bezeichnen die Buchstaben gewöhnliche Variablen, die Zahlenwerte annehmen. Beispiele zulässige Zeichenfolgen für diese Grammatik sind A + ( B * C ) und A* (((B + C) * (D * E )) + F. Gemäß der Definition sind gewisse Zeichenfolgen sowohl als arithmetische Ausdrücke als auch als reguläre Ausdrücke absolut zulässig. Z. B. könnte A*(B+C) bedeuten „addiere B zu C und multipliziere das Ergebnis mit A“ oder „nehme eine beliebige Anzahl von As, denen entweder B oder C folgt“. Dies verdeutlicht die Tatsache, dass die Prüfung der Zulässigkeit einer Zeichenfolge eine Angelegenheit ist, die Interpretation ihrer Bedeutung jedoch eine ganz andere. G.Heyer 10 Algorithmen und Datenstrukturen II Jeder reguläre Ausdruck ist selbst ein Beispiel für eine kontextfreie Grammatik: Jede Sprache, die durch einen regulären Ausdruck beschrieben werden kann, kann auch durch eine kontextfreie Grammatik beschrieben werden. Die Umkehrung gilt nicht: z. B. kann die Forderung des „Ausgleichens“ von Klammern mit regulären Ausdrücken nicht erfasst werden. Andere Arten von Grammatiken können Sprachen beschreiben, die kontextfreie Grammatiken nicht beschreiben können, z. B. sind kontextsensitive Grammatiken eben solche Grammatiken wie die obigen, mit dem Unterschied, dass die linken Seiten von Produktionen nicht einzelne nichtterminale Symbole sein müssen. G.Heyer 11 Algorithmen und Datenstrukturen II Der rekursive Abstieg (Top-Down-Syntaxanalyse) Jede Produktion entspricht einer Prozedur, die nach dem nichtterminalen Symbol auf der linken Seite benannt ist. Nichtterminale Symbole auf der rechten Seite der Eingabe entsprechen ( möglicherweise rekursiv ) Prozeduraufrufen; terminale Symbole entsprechen dem Durchlaufen der eingegebenen Zeichenfolge. Z. B. ist die folgende Prozedur Teil eines Top-Down-Parsers für unsere Grammatik der regulären Ausdrücke. G.Heyer 12 Algorithmen und Datenstrukturen II Prozedur als Teil eines Top-Down-Parsers expression () { term (); if ( p [j] == „+“ ) ( j++; expression() ; ) } Eine Zeichenfolge p enthält den regulären Ausdruck, der Gegenstand der Syntaxanalyse ist, mit einem Index j, der auf das Zeichen zeigt, dessen Untersuchung gerade beginnt. Um die Syntaxanalyse für einen gegebenen regulären Ausdruck p vorzunehmen, setzt man j auf 0 und ruft expression (Ausdruck) auf. Wenn dies dazu führt, dass j auf M gesetzt wird, so ist der reguläre Ausdruck in der durch die Grammatik beschriebenen Sprache enthalten . G.Heyer 13 Algorithmen und Datenstrukturen II Das erste, was Ausdruck bewirkt, ist ein Aufruf der Prozedur Term, deren Implementation etwas komplizierter ist. term() { faktor (); if ( ( p [j] == „(„ ) || letter( p[j] )) term(); } Da nach der vorgestellten Grammatik ein Term entweder ein Faktor oder ein von einem Term gefolgter Faktor ist, muss die Prozedur Term einen anderen Weg als die Prozedur Ausdruck einschlagen, um zu überprüfen, welche beiden Alternativen bei der Ableitung zu wählen ist. Bei der Prozedur Ausdruck stand hierfür das terminale Symbol + zur Verfügung. G.Heyer 14 Algorithmen und Datenstrukturen II Die Prozedur Term hingegen muss einen Teil der Aufgabe der Prozedur Faktor übernehmen und überprüfen, ob nach dem Aufruf von Faktor zu Beginn ein weiterer Faktor folgt (der laut der angegebenen Grammatik mit einer öffnenden Klammer oder einem Buchstaben v beginnen muss ). Dieser Prozess der Prüfung des nächsten Zeichens ohne Inkrementieren von j zwecks Entscheidung, was zu tun ist, wird look-ahead (Vorausschau) genannt. Die Implementation von Faktor ergibt sich nun unmittelbar aus der Grammatik. Wenn das Eingabezeichen, das gerade durchlaufen wird, keine Klammer „(„ und kein Buchstabe ist, wird eine Prozedur error aufgerufen, um die Fehlerbedingung zu behandeln. G.Heyer 15 Algorithmen und Datenstrukturen II Prozedur Faktor factor () { if ( p[j] == „(„ ) { j++ ; expression() ; if ( p[j] == „)“ ) j++; else error(); } else if ( letter ( p[j] ) ) j++ ; else error(); if ( p[j] == „* “ ) j++ ; } Eine weitere Fehlerbedingung tritt auf, wenn eine Klammer „)“ fehlt. G.Heyer 16 Algorithmen und Datenstrukturen II Syntaxanalyse von ( A * B + AC ) D Ausdruck Term Faktor ( Ausdruck Term Faktor A * Term Faktor B + Ausdruck Term Faktor A Term Faktor C ) Term Faktor D G.Heyer 17 Algorithmen und Datenstrukturen II Die Funktionen Ausdruck, Term und Faktor sind offensichtlich rekursiv; tatsächlich sind sie derart miteinander verflochten, dass es keine Möglichkeit gibt, sie so aufzuzählen, dass jede Funktion deklariert wird, bevor sie benutzt wird (für manche Programmiersprachen ist dies ein Problem). Der Syntaxbaum für eine gegebene Zeichenfolge gibt die Struktur der rekursiven Aufrufe während der Syntaxanalyse an. Die Abbildung stellt die Arbeitsweise der obigen drei Prozeduren dar, wenn p (A * B + AC) D enthält und Ausdruck mit j=1 aufgerufen wird. Außer für das Pluszeichen wird das gesamte „Durchsuchen“ in Faktor realisiert. Zur Verbesserung der Lesbarkeit wurden die von der Prozedur Faktor durchlaufenen Zeichen, mit Ausnahme der Klammern, in der gleichen Zeile wie der Aufruf Faktor dargestellt. G.Heyer 18 Algorithmen und Datenstrukturen II Der Top-Down-Ansatz führt nicht für alle möglichen kontextfreien Grammatiken zum Ziel, z. B. würde man bei der Produktion <Ausdruck> ::= v | <Ausdruck> + <Term> ein unerwünschtes Ergebnis erhalten, wenn man der mechanischen Übersetzung in C wie oben folgen würden: badexpression( ); { if (letter ( p[j] )) j++ ; else { badexpression ( ) ; if ( p[j] == „+“ ) { j++ ; term( ) ; } else error ( ) ; } } G.Heyer 19 Algorithmen und Datenstrukturen II Wenn diese Prozedur mit einem p[j] aufgerufen würde, das kein Buchstabe ist (wie im Beispiel für j = 1 ), so würde sie in eine rekursive Endlosschleife einmünden. Die Vermeidung solcher Schleifen ist eine der Hauptschwierigkeiten bei der Implementation von rekursiv absteigenden Parsern. G.Heyer 20 Algorithmen und Datenstrukturen II Bottom-up Analyse Zur Syntaxanalyse gehört mehr als die einfache Prüfung, ob die eingegebene Zeichenfolge zulässig ist; die Hauptsache besteht in der Produktion des Syntaxbaumes für die weitere Verarbeitung (selbst wenn dies wie beim Top-Down-Parser nur implizit erfolgt). Ein Typ eines Parsers, der in dieser Weise abläuft, ist der sogenannte shift-reduce-Parser. Die Idee besteht darin, einen Stapel zu verwenden, der terminale und nichtterminale Symbole aufnimmt. Jeder Schritt der Syntaxanalyse ist entweder ein verschiebender (shift) Schritt, bei dem das nächste eingegebene Zeichen einfach im Stapel abgelegt wird, oder ein reduzierender (reduce) Schritt, bei dem die im Stapel oben befindlichen Zeichen auf Übereinstimmung mit der rechten Seite einer Produktion in der Grammatik geprüft und auf das auf der linken Seite dieser Produktion befindliche nichtterminale Symbol „reduziert“ (d. h. durch dieses ersetzt ) werden. G.Heyer 21 Algorithmen und Datenstrukturen II Verbindungen zum Pattern Matching Oft ist es wünschenswert, eine Suche nach einem nur unvollständigen Muster vorzunehmen, z. B. kann es vorkommen, dass Benutzer eines Texteditors nur einen Teil eines Musters vorgeben wollen, oder ein Muster, welches zu mehreren verschiedenen Wörtern passen würde, oder dass sie festlegen wollen, dass eine beliebige Anzahl bestimmter spezifischer Zeichen ignoriert werden soll. Die bisher untersuchten Algorithmen sind entscheidend von der vollständigen Vorgabe des Musters abhängig, so dass man jetzt andere Methoden benutzen muss. Die grundlegenden Mechanismen, die betrachtet werden, ermöglichen die Entwicklung eines sehr leistungsfähigen Verfahrens zur Suche in Zeichenfolgen, welches komplizierte Muster aus M Zeichen an Text-Zeichenfolgen aus N Zeichen anpassen kann, in einer Zeit, die im ungünstigsten Fall proportional zu MN2 ist, in typischen Anwendungsfällen jedoch wesentlich schneller abläuft. G.Heyer 22 Algorithmen und Datenstrukturen II Zunächst muss man eine Methode entwickeln, um die Muster zu beschreiben; eine „Sprache“, die benutzt werden kann, um die oben genannten Probleme der partiellen Suche in Zeichenfolgen exakt zu spezifizieren. Diese Sprache wird leistungsfähigere elementare Operationen umfassen als die bisher verwendete einfache Operation „Überprüfen“, ob das i-te Zeichen der Text-Zeichenfolge mit dem j-ten Zeichen des Musters übereinstimmt. G.Heyer 23 Algorithmen und Datenstrukturen II Beschreibung von Mustern Die hier betrachteten Musterbeschreibungen erfolgen mit Hilfe von Symbolen, welche mittels der folgenden drei grundlegenden Operationen miteinander verknüpft sind: 1) Verkettung (Concatenation): Dies ist die Operation, die bisher benutzt wurde. Falls zwei Zeichen im Muster benachbart sind, so liegt genau dann und nur dann eine Übereinstimmung vor, wenn die im Text gleichen beiden Zeichen benachbart sind, z. B. bedeutet AB, dass B auf A folgt. 2) Oder (Or): Dies ist die Operation, die uns die Möglichkeit gibt, Alternativen im Muster vorzugeben. Wenn sich zwischen zwei Zeichen ein oder befindet, so liegt dann und nur dann eine Übereinstimmung vor, wenn eines der Zeichen im Text auftritt. Diese Operation wird durch die Benutzung des Symbols + bezeichnet und es werden Klammern verwendet, um sie mit der Verkettung in beliebig komplexer Weise zu kombinieren, z. B. bedeutet A+B „entweder A oder B“; C( AC+B)D bedeutet „entweder CACD oder CBD“; (A+C)((B+C)D) bedeutet „entweder ABD oder CBD oder ACD oder CCD“ . G.Heyer 24 Algorithmen und Datenstrukturen II 3) Hüllenbildung (Closure): Diese Operation gestattet es, Teile des Musters beliebig oft zu wiederholen. Bei der Hülle des Symbols liegt dann und nur dann eine Übereinstimmung vor, wenn das Symbol beliebig oft (einschließlich 0 mal ) erscheint. Die Hüllenbildung soll bezeichnet werden, indem nach dem zu wiederholenden Zeichen oder der zu wiederholenden , in Klammern eingeschlossenen Gruppe ein * gesetzt wird. Z. B. stimmt AB* mit Zeichenfolgen überein, die aus einem A bestehen, dem eine beliebige Anzahl (oder kein ) B folgt, während (AB)* mit Zeichenfolgen übereinstimmt, die aus sich abwechselnden A und B bestehen. G.Heyer 25 Algorithmen und Datenstrukturen II Eine Folge von Symbolen, die unter Verwendung dieser drei Operationen aufgebaut ist, wird regulärer Ausdruck genannt. Jeder reguläre Ausdruck beschreibt viele spezielle Textmuster. Das Ziel besteht darin, einen Algorithmus zu entwickeln, mit dem bestimmt werden kann, ob irgendeines der Muster, die durch einen gegebenen regulären Ausdruck beschrieben werden, in einer gegebenen Text-Zeichenfolge auftritt. G.Heyer 26 Algorithmen und Datenstrukturen II Der Pattern Matching-Algorithmus kann als eine Verallgemeinerung der groben Methode der Suche in Zeichenfolgen von links nach rechts angesehen werden. Der Algorithmus sucht nach der am weitesten links stehenden Teil-Zeichenkette innerhalb der TextZeichenfolge, die mit der Beschreibung des Musters übereinstimmt, indem er die Text-Zeichenfolge von links nach rechts durchläuft und bei jeder Position prüft, ob eine bei dieser Position beginnende Teil-Zeichenfolge existiert, die mit der Beschreibung des Musters übereinstimmt. G.Heyer 27 Algorithmen und Datenstrukturen II Automaten für das Pattern Matching Der Algorithmus von Knuth-Morris-Pratt kann als ein auf der Grundlage des Suchmusters konstruierter endlicher Automat betrachtet werden, der den Text durchsucht. Die Methode, die man für das Pattern Matching regulärer Ausdrücke anwenden kann, ist eine Verallgemeinerung dieser Sichtweise. Der endliche Automat für den Algorithmus von Knuth-MorrisPratt geht von einem Zustand in den anderen über, indem er ein Zeichen aus der Text-Zeichenfolge untersucht und dann in einen Zustand übergeht, wenn eine Übereinstimmung vorliegt, und in einen anderen, wenn das nicht der Fall ist. Eine Nichtübereinstimmung an irgendeiner Stelle bedeutet, dass das Muster in dem an dieser Stelle beginnenden Text nicht auftreten kann. Der Algorithmus selbst kann als eine Simulation des Automaten angesehen werden. Die Eigenschaft des Automaten, die bewirkt, dass er leicht simuliert werden kann, ist, dass er deterministisch ist: Jeder Übergang von einem Zustand in einen anderen wird vollständig durch das nächste eingegebene Zeichen bestimmt. G.Heyer 28 Algorithmen und Datenstrukturen II Um reguläre Ausdrücke zu behandeln, ist es erforderlich, einen leistungsfähigeren abstrakten Automaten zu betrachten. Aufgrund der Operation oder kann der Automat nicht durch Untersuchung von nur einem Zeichen feststellen, ob das Muster am einer gegebenen Stelle auftreten kann oder nicht; aufgrund der Hüllenbildung kann er noch nicht einmal feststellen, wie viele Zeichen betrachtet werden müssten, bevor eine Nichtübereinstimmung entdeckt wird. Der natürlichste Weg zur Überwindung dieser Probleme besteht darin, den Automaten mit der Fähigkeit eines nichtdeterministischen Vorgehens auszustatten: Bei Vorhandensein von mehreren Wegen, auf denen man versuchen kann, das Muster anzupassen, sollte der Automat den richtigen erraten. G.Heyer 29 Algorithmen und Datenstrukturen II Ein nichtdeterministischer Mustererkennungs-Automat für (A*B + AC)D 7 6 A C 8 9 5 D 0 1 2 A 3 B 4 Dieser Automat kann aus einem mit einem Zeichen markierten Zustand in den Zustand übergehen, auf den dieser Zustand „zeigt“, indem er dieses Zeichen in der Text-Zeichenfolge anpasst (und durchläuft). Was den Automat nichtdeterministisch macht, ist, dass es einige Zustände gibt (Nullzustände genannt), die nicht nur mit keinem Zeichen markiert sind, sondern auch auf zwei unterschiedliche Nachfolge-Zustände „zeigen“ können. Zustand 9 ist ein Nullzustand ohne Ausgänge, der den Automaten anhält. G.Heyer 30 Algorithmen und Datenstrukturen II Der Automat hat einen eindeutigen Anfangszustand (dargestellt mit der frei beginnenden Linie links) und einen eindeutigen Endzustand (kleines Quadrat rechts). Wenn der Automat im Anfangszustand gestartet wird, ist er in der Lage, jede durch das Muster beschriebene Zeichenfolge zu „erkennen“, indem er Zeichen liest und seinen Zustand gemäß seinen Regeln verändert, wobei er am Ende zum „Endzustand“ gelangt. Wenn der Automat auf einem gewöhnlichen Computer simuliert werden soll, muss man alle Möglichkeiten ausprobieren, z. B. bei einer Musterbeschreibung (A*B + AC)D in der Textzeichenfolge CDAABCAAABDDACDAAC um die Zeichenfolge AAABD zu erkennen. G.Heyer 31 Algorithmen und Datenstrukturen II Konstruktion eines Zustandsautomaten: Für einen gegebenen Ausdruck kann man den Automaten konstruieren, indem man für die einzelnen Komponenten des Ausdrucks Teilautomaten herstellt und für jede der drei Operationen Verkettung, oder und Hüllenbildung die Art und Weise definiert, wie zwei Teilautomaten zu einem größeren Automaten zusammengesetzt werden: 1.) Automat mit zwei Zuständen zur Erkennung eines Zeichens: A Anfangszustand, der auch das Zeichen erkennt und Endzustand. G.Heyer 32 Algorithmen und Datenstrukturen II 2.) Konstruktion eines Zustandsautomaten: Verkettung Um den Automaten für die Verkettung von zwei Ausdrücken aus den Automaten für die einzelnen Ausdrücke zu bilden, vereinigt man den Endzustand des ersten mit dem Anfangszustand des zweiten Automaten. Automat 1 Automat 1 Automat 2 Automat 2 G.Heyer 33 Algorithmen und Datenstrukturen II 3.) Konstruktion eines Zustandsautomaten: oder Konstruktion in ähnlicher Weise, Hinzufügen eines neuen Nullzustandes, der auf die beiden Anfangszustände zeigt, und indem man einen Endzustand auf den anderen zeigen lässt, der dann zum Endzustand des kombinierten Automaten wird. G.Heyer Automat 1 Automat 1 Automat 2 Automat 2 34 Algorithmen und Datenstrukturen II 4.) Konstruktion eines Automaten: Hüllenbildung Der Endzustand wird zum Anfangszustand und zeigt zurück zum alten Anfangszustand sowie zu einem neuen Endzustand. Durch sukzessive Anwendung dieser Regeln kann für jeden beliebigen regulären Ausdruck ein ihm entsprechender Automat gebildet werden. Die Zustände werden entsprechend der Reihenfolge ihrer Erzeugung nummeriert (wenn der Automat konstruiert wird, indem das Muster von links nach rechts durchlaufen wird), so dass die Konstruktion des Automaten mit Hilfe der obigen Regeln leicht verfolgt werden kann. G.Heyer 35 Algorithmen und Datenstrukturen II Darstellung des Automaten Da man oft nur über die Nummer auf die Zustände zugreifen will, ist die zweckmäßigste Organisation für den Automaten eine Darstellung als Feld. Es werden die drei mit dem Index state (Zustand) versehenen parallelen Felder ch, next1 und next2 verwendet, um den Automaten darzustellen. state 0 1 2 3 4 5 6 7 8 9 ch[state] A B A C D next1[state] 5 2 3 4 8 6 7 8 9 0 next2[state] 5 2 1 4 8 2 7 8 9 0 Die mit dem Index state versehenen Einträge können als Anweisungen für den nichtdeterministischen Automaten von der Art „Wenn man sich in state befindet und ch[state] sieht, dann soll man das Zeichen durchlaufen und in den Zustand next1[state] (oder next2[state]) übergehen“ interpretiert werden. Zustand 9 ist der Endzustand in diesem Beispiel, und Zustand 0 ist ein Pseudo-Anfangszustand, dessen Einträge next die Nummer des eigentlichen Anfangszustandes sind. G.Heyer 36 Algorithmen und Datenstrukturen II