Rekursion und EBNF Flashcards
Was bedeutet es, wenn eine Funktion rekursiv definiert ist?
Ist eine Funktion rekursiv definiert, so erscheint die Funktion in ihrer eigenen Definition, z.B.:
πππ πππππππππ(πππ π) { ππ (π <= π·) ππππππ π·; ππππ ππππππ π * πππππππππ(π-π·); }
Was ist ein mΓΆgliches Problem rekursiv definierter Funktionen und wie lΓ€sst sich dieses umgehen?
Rekursive Funktionen kΓΆnnen in einer unendlichen Rekursion enden, z.B.: ππππ π() {π()}.
Man braucht also einen garantierten Fortschritt in Richtung einer Abbruchbedingung, z.B. bei folgendem Codeβ¦
πππ πππππππππ(πππ π) {
ππ (π <= π·)
ππππππ π·;
ππππ
ππππππ π * πππππππππ(π-π·);
}
β¦endet die Rekursion, falls n β€ 1 und es entsteht ein neuer rekursiver Aufruf mit einem Argument kleiner n.
Die Abbruchbedingung wird daher garantiert erreicht.
Wie wird eine rekursive Funktion ausgewertet?
Bei jedem Funktionsaufruf kommt der Wert des Aufrufarguments auf einen Stapel und es wird immer mit dem obersten Wert des Stapels gearbeitet.
Am Ende eines Aufrufs wird der oberste Wert wieder vom Stapel gelΓΆscht.
(Gutes Bild in PPT-Slides von 05.11.2019!)
Betrachte folgenden Codeausschnitt:
ππππππππ πππ πππ(ππππππππ πππ π) { ππ (π == 0) ππππππ 0; ππ (π == π·) ππππππ π·; ππππππ πππ(π-π·) + πππ(π-πΈ); }
Wo liegt hier das Problem und wie kann es behoben werden?
Der Code funktioniert gut fΓΌr kleine Zahlen, fΓΌr grosse Zahlen jedoch dauert die Berechnung viel zu lange.
FΓΌr πππ(π»0) z.B. wird Fββ 2-mal, Fββ 3-mal, Fββ 5-mal, Fββ
8-mal, Fββ 13-mal, Fββ 21-mal und Fβ eine Milliarde mal berechnet!
Um dies zu verbessern, schaut man, dass jede Zahl nur einmal berechnet wird und jeweils nur die letzten beiden berechneten Werte gespeichert werden.
Eine LΓΆsung wΓ€re z.B. folgender Codeausschnitt:
ππππππππ πππ πππ(ππππππππ πππ π) {
ππ (π == 0) ππππππ 0;
ππ (π == π·) ππππππ π·;
ππππππππ πππ π = 0;
ππππππππ πππ π = π·;
πππ (ππππππππ πππ π = πΈ; π <= π; ++π) { ππππππππ πππ π_πππ = π; π = π; π += π_πππ; } ππππππ π; }
Welche anderen zwei βMethodenβ kann man anstelle einer rekursiven Funktion verwenden?
Eine Rekursion kann immer simuliert werden durchβ¦
(1) β¦ Iteration (Schleifen).
(2) β¦ expliziten βAufrufstapelnβ (z.B. mit einem Vektor).
Betrachte folgenden Codeausschnitt zur Berechnung einfacher arithmetischer Operationen:
ππππππ ππππ;
πππ::πππ»_space; ππππ;
ππππ ππ; π ππππ (πππ::πππ >> ππ && ππ != β=β) { ππππππ ππππ; πππ::πππ >> ππππ; ππ (ππ == β+β) ππππ += ππππ; ππππ ππ (ππ == β*β) ππππ *= ππππ; ππππ ... } πππ::ππππ << "π΄πππππππ " << ππππ << "\π";
Wo kommt es hier zum Problem und weshalb ist es nicht so einfach, dieses Problem zu lΓΆsen?
Die PrΓ€zedenzen werden nicht eingehalten: Der Code lΓ€sst mehrfache Operationen hintereinander zu (durch die while-Schlaufe), z.B. 3 + 2 * 4 oder 1 + 1 * 2 + 2 * β¦ .
Jedoch wird das Ergebnis falsch sein, wenn Punkt- und Strichrechnung gemischt wird, da lediglich die LinksprΓ€zedenz eingehalten wird mit diesem Code.
Da die LΓΆsung dieses Problems erfordert, dass der Code alle noch folgenden PrΓ€zedenzen kennt (*), braucht man ein neues formales (von C++ unabhΓ€ngiges) Handwerkszeug (= Grammatiken).
(*) z.B. muss das Programm, um 13 + 4 * (15 - 7 * 3) zu berechnen, bis zur "3" am Ende der Rechnung alle Werte zwischenspeichern, da es aufgrund der Teilrechnungen nie weiss, ob noch ein Schritt folgt oder nicht: 13 + β¦? 13 + 4 * β¦? 13 + 4 * (15 - β¦? 13 + 4 * (15 - 7 * β¦? 13 + 4 * (15 - 7 * 3) !
Was ist ein terminales Symbol bzw. ein nicht terminales Symbol und wo werden diese Symbole gebraucht?
Ein terminales Symbol ist ein Symbol, z.B. β8β, welches nicht weiter gekΓΌrzt werden kann.
Ein nicht-terminales Symbol kann durch erneute Berechnung eines rekursiven Funktionsaufrufs immer wieder verwendet werden und entsprechend gekΓΌrzt werden.
Man braucht diese Symbole folglich in rekursiven Funktionen.
Wie kann man den Begriff βZahlβ mit Hilfe des Begriffs βZifferβ definieren, und zwarβ¦
(1) β¦ rekursiv?
(2) β¦ nicht rekursiv?
(1) Eine Zahl ist eine Folge von Ziffern. Eine Folge von Ziffern ist:
- nur eine Ziffer (z.B. β2β), oder
- eine Ziffer gefolgt von einer Folge von Ziffern (z.B. β2019β).
Man sieht hier, dass βFolge von Ziffernβ mit βFolge von Ziffernβ definiert wird, also rekursiv.
(2) Eine Zahl ist eine Folge von Ziffern. Eine Folge von Ziffern ist:
- nur eine Ziffer (z.B. β2β), oder
- eine Ziffer gefolgt von beliebig vielen Ziffern (z.B. β2019β).
Man sieht hier, dass βFolge von Ziffernβ nur mit der Definition von βZifferβ auskommt, also nicht rekursiv.
Was ist die EBNF?
Die Extended Backus-Naur Form (EBNF) ist eine formale Metasprache (also eine Sprache ΓΌber eine Sprache), die verwendet wird, um formale Grammatiken zu definieren. Man kann also ganze Programmiersprachen (z.B. C++) in EBNF umschreiben.
(Gutes Beispiel: https://de.wikipedia.org/wiki/Erweiterte_Backus-Naur-Form#Beispiel_Programmiersprache).
Was bedeutet Parsing und was ist ein Parser? Wozu braucht man das Parsing?
Parsing ist per Definition die Syntaxanalyse einer Sprache, um danach auf die Semantik schliessen zu kΓΆnnen.
(Bsp. in einer βmenschlichenβ Sprache: Man wΓΌrde einen Satz in seine grammatikalischen Bestandteile zerlegen, was z.B. ein wichtiger Bestandteil der Linguistik ist.)
In C++ wird folglich mit dem Parsing festgestellt, ob ein Satz nach der EBNF gΓΌltig ist.
Dazu braucht man einen Parser, also das Programm zum Parsen.
Wie generiert man aus der EBNF einen Parser, um zu ΓΌberprΓΌfen, ob sein Satz in C++ gΓΌltig ist?
Man konvertiert EBNF-Elemente zu C++-Elementen:
- Regeln werden zu Funktionen.
- Alternativen und Optionen werden zu if-Anweisungen.
- Nichtterminale Symbole auf der rechten Seite werden zu Funktionsaufrufen.
- Optionale Repetitionen werden zu while-Anweisungen.
Beispiel:
Man definiert durch EBNF:
ππππππ = ππππππππ_ππππππ | β(β ππ‘ππππππππ β)β | β-β ππππππ.
ππππ = ππππππ { β*β ππππππ | β/β ππππππ }
ππ‘ππππππππ = ππππ {β+β ππππ | β-β ππππ}
Hier wurde βunsigned_numberβ schon vordefiniert. Weiterhin bedeuten β|β Alternativen, geschweifte Klammern Wiederholungen, und in AnfΓΌhrungszeichen gestellte Symbole Terminalsymbole.
Es wird also z.B. ein Faktor definiert als eine natΓΌrliche Zahl ODER einen Ausdruck in Klammern ODER eine natΓΌrliche Zahl mit einem Minus vorne dran (was ja dann eine ganze Zahl wird).
In C++ wird dann eine Funktion definiertβ¦
ππππ ππππππ (πππ::πππππππ& ππ_ππππππ);
β¦ um zu ΓΌberprΓΌfen, ob es sich beim Eingabestrom tatsΓ€chlich um einen βFaktorβ handelt, und falls ja, wird dieser extrahiert.
(FΓΌr genauere Angaben siehe PPT-Slides vom 12.11.2019)