Dieses Kapitel gehoert zum Kurs Programmieren in Oberon des Fachbereichs Informatik.

Immer das gleiche - Rekursionen Bearbeiten

Also schön, die Grundlagen des Programmierens sitzen schon fest, die Selbstsicherheit pulsiert in den Adern, und dann kommt so ein Ding - Rekursionen. Obwohl wir (hoffentlich) alle diese Dinger in der Mathematik schon mal gesehen haben, fällt es immer noch schwer, sie zu verstehen.

Wir wollen also eine berühmte Rekursion, das Euklidsche Verfahren zur Bestimmung eines ggTs (fuer die, die in der Mathemaitik gepennt haben, das ist der groesste gemeinsame Teiler), durch eine rekursive Prozedur ausdrucken. Es soll ein Modul namens MeinModul mit einer Prozedur namens ggT, welche die Zahlen n und m einliest und den grössten gemeinsamen Teiler zurückgibt erzeugt werden.

Aus dem Leben gegriffen Bearbeiten

Wie gesagt, haben wir alle diese Dinger schon mal gesehen, und zwar in der Mathematik. Dort ist die Fibonacci-Reihe z.B. so beschrieben:

     
     
     

Wenn wir also selber eine Fibonacci-Prozedur schreiben, welche n als Parameter erhält und eine Zahl zurückgibt, muss diese Prozedur, falls n > 1, sich selber aufrufen. Dies ist kein Problem.

PROCEDURE Fib ( n : INTEGER ) : INTEGER;
    BEGIN
        IF n < 2 THEN
            RETURN 1;
        ELSE
            RETURN Fib(n-1) + Fib(n-2);
        END;
    END Fib;

Das ganze sieht erstaunlich einfach aus und ist es sogar auch. Eine erste Bedingung muss dafür sorgen, dass die Prozedur sich nicht endlos selber aufruft, ist diese nicht erfüllt, so ruft die Prozedur sich selber auf.

Beim Entwurf der Prozedur ist immer darauf zu achten, dass die Abbruchbedingung (in unserem Fall n < 2) irgendwann erfüllt wird. Bei uns muss n immer kleiner als ein gewisser Wert sein. Die Prozedur ruft sich immer entweder mit n-1 oder n-2 selbst auf, also wird n immer kleiner, die Rekursion hört irgendwann auf.

Same shit, new Variables Bearbeiten

Das Beispiel der Fibonacci-Reihe ist ziemlich einfach, da die Prozedur keine Variablen enthält, und sich darum nie die Frage stellt, was mit lokalen Variablen passiert. Nehmen wir ein etwas komplizierteres Beispiel, nämlich ein Fahrzeug, welches ein Feld (m * n) von links oben nach rechts unten traversieren muss. Das Terrain auf dem Feld ist nicht immer gleich, sondern variiert von 1 bis 10 (1 schnell, 10 langsam). Er muss den schnellsten Weg finden und melden, wie lange dieser war.

Wir müssen nun eine Prozedur schreiben, die für ein Fahrzeug an der Position x,y entscheidet, ob er besser nach rechts oder nach unten geht, und diesen Weg, plus den Wert des Feldes an der Position x,y zurückgibt. Grob ausgelegt gibt es so was

PROCEDURE Fahr ( x, y : INTEGER ) : INTEGER;
    VAR
        rechts, runter : INTEGER;
    BEGIN
        IF x,y nicht im feld (also nicht in n oder m) THEN
            RETURN MAX(INTEGER);
        END; (* dieser weg soll nicht gewaehlt werden *)
        IF position ist unten rechts THEN
            RETURN feld[x,y];
        END (* man ist am Ende gelangt *)
        rechts := Fahr(x+1, y);
        runter := Fahr(x,y+1);
        IF rechts > runter THEN
            RETURN feld[x,y] + runter;
        ELSE
            RETURN feld[x,y] + rechts;
        END; (* nimm den besseren Weg *)
    END Fahr;

Was passiert nun mit den Werten von rechts und runter wenn die Prozedur sich selber aufruft? Werden sie von der zweiten Instanz von Fahr überschrieben?

Nee! Man kann es so sehen, dass wenn eine rekursive Prozedur sich selber aufruft, wird so getan, als ob sie eine andere Prozedur, halt mit dem gleichen Namen aber separate Variablen, aufrufen würde. Die Variablen in jeder Instanz von Fahr sind ihr eigen und werden bei den verschachtelten Prozeduraufrufen jedesmal neu angelegt, die Werte der aufrufenden Instanz bleiben erhalten.

Bauen wir also nun unsere Prozedur Fahr so aus, dass sie Funktioniert. Da wir schon daran sind, bauen wir auch ein Modul rund herum.

MODULE Fahrzeug;

    IMPORT
        In, Out;

    VAR
        feld : ARRAY 20,20 OF INTEGER;
        m, n : INTEGER;

    PROCEDURE Einlesen;
        VAR
            i, j : INTEGER;
        BEGIN
            In.Open;
            In.Int(m); In.Int(n);
            FOR i := 0 TO m-1 DO
                FOR j := 0 TO n-1 DO
                    In.Int(feld[i,j]);
                END;
            END;
        END Einlesen;

    PROCEDURE Fahr ( x, y : INTEGER ) : INTEGER;
        VAR
            rechts, runter : INTEGER;
        BEGIN
            IF (x >= m) OR (x < 0) OR (y >= n) OR (y < 0) THEN
                RETURN MAX(INTEGER);
            END;
            IF (x = m-1) & (y = n-1) THEN
                RETURN feld[x,y];
            END;
            rechts := Fahr(x+1,y);
            runter := Fahr(x,y+1);
            IF rechts > runter THEN
                RETURN runter + feld[x,y];
            ELSE
                RETURN rechts + feld[x,y];
            END;
        END Fahr;

    PROCEDURE Los*;
        BEGIN
            Einlesen;
            Out.String("Der kuerzeste Weg hat die Laenge: ");
            Out.Int(Fahr(0,0),2); Out.Ln;
        END Los;

    BEGIN
        Out.Open;
    END Fahrzeug.

So, wenn wir jetzt die Prozedur Fahr uns anschauen, haben wir zwei Abbruchbedingungen: wenn das Fahrzeug am Ziel ist und wenn es aus dem Feld raus ist. Wir berechnen den Weg nach unten und den nach rechts und nehmen den besseren, addieren unser gegenwärtiges Feld dazu und springen zurück.

Euklid meets Oberon Bearbeiten

Also gut, wenn ihr das Beispiel mit dem Fahrzeug nicht hundertprozentig verstanden habt, ist es nicht so schlimm: Hauptsache ist, dass ihr die wesentlich einfachere Euklidische Rekursion versteht. In der Mathematik sind folgende Regeln bestimmt:

     
     
     
     

Wir haben also zwei Abbruchbedingungen und zwei mögliche Berechnungen. Wenn man sich diese Berechnungen anschaut, kann man sie beide in einem Schritt erledigen: ist y grösser als x, so ist x MOD y gleich x, es ändert sich aber nichts. Ist hingegen y kleiner als x, so wird x MOD y kleiner sein als y. Damit wir garantieren, das der ggT nie gleich ist, und die Rekursion somit nie in eine endlos-Schleife gerät, können wir gleichzeitig MOD-rechnen und umkehren, und zwar so:

 

Wenn wir jetzt das ganze so verpacken, dass es Oberon versteht, kriegen wir sowas:

MODULE MeinModul;

    IMPORT
        In, Out;

    PROCEDURE Euklid ( x, y : INTEGER ) : INTEGER;
        BEGIN
            IF (y = 0) OR (x = y) THEN
                RETURN x;
            ELSE
                RETURN Euklid(y,x MOD y);
            END;
        END Euklid;

    PROCEDURE ggT*;
        VAR
            x, y : INTEGER;
        BEGIN
            In.Open; In.Int(x); In.Int(y);
            Out.String("der ggT von "); Out.Int(x,2);
            Out.String(" und "); Out.Int(y,2);
            Out.String(" ist: "); Out.Int(Euklid(x,y),2);
            Out.Ln;
        END ggT;

    BEGIN
        Out.Open;
    END MeinModul.