Forum: PC-Programmierung Rekursion als Schleifen darstellen (java)


von Jochen (Gast)


Lesenswert?

Hallo,

ich suche nach einem Schema, um Rekursionen als Schleifen umzuschreiben.

Für

static void down1 (int n)
{
 if ( n <= 0)
  return;

System.out.println( n + ",");
down1( n - 1 );
}

habe ich schon gedacht:
1. If Answieung negieren und in die Bedingung der while schleife 
schreiben
2. Rekursionsaufruf n -1 zieht von n eins ab und macht damit alles von 
forne, ist also die Zählvariable der Schleife

das wäre dann  also:

while (n>0)
{ System.out.println ( n + ", ");
n = n - 1
}

Ist das so effektiv, wie würdet ihr das machen? Geht es so, dass man 
nicht negieren muss?

Wie würde das aussehen, wenn man in einer Rekursion 2 Rekusrsionsaufrufe 
hat:

static void down1 (auto auto)
{
 if ( n == 0 )
  return;
System.out.println(auto)
down1(auto.links)
down1(auto.rechts)
}

wie würde man das enstprechend umschreiben, hier fällt mir nichts zu 
ein...

Danke !

von Jochen (Gast)


Lesenswert?

Zu meinem ersten Beispiel: mir fällt grad auf, dass die nicht äquivalent 
sind,
da der Wert von n nach der Rekursion gleich ist, bei der Schleife aber 
verändert ist.
Kann man das umschreiben, damit es wirklich gleich ist, ohne neue 
Variablen einzuführen?

von Jemand (Gast)


Lesenswert?

>Ist das so effektiv, wie würdet ihr das machen? Geht es so, dass man
>nicht negieren muss?

Bis auf dass des öfteren ';' fehlt, sollte das funktionieren. Als 
effektiv würde ich auch nur die Schleife bezeichnen wollen.

Was willst du nicht negieren? Die Schleifenbedingung? Was sollte das 
bringen?
Wenn es nur um ein Schema geht, dann nimm halt 'while(!(n <= 0))'.

>Wie würde das aussehen, wenn man in einer Rekursion 2 Rekusrsionsaufrufe
>hat:

Hat man das? Meines Wissens nach nicht.
Weiter ist das Beispiel im speziellen sinnlos, da keine Variable 
mitübergeben wird, bzw. die Abbruchbedingung nicht zur Variablen passt.

Wenn man das wirklich iterativ darstellen wollte, so müsste man in einer 
Schleife jeweils den ersten Aufruf abarbeiten und danach in einer 
zweiten Schleife rückwärts den zweiten.
Vllt. einfach mal ne einfache Rekursion nach dem Beispiel aufstellen und 
durchspielen.

>Kann man das umschreiben, damit es wirklich gleich ist, ohne neue
>Variablen einzuführen?

Die Rekursion oder die Schleife? Falls letzteres: Fiele mir jetzt nichts 
dazu ein. Allerdings ist zu erwähnen, dass die Rekursion haufenweise 
neue Variablen einführt, wieso sollte das die Schleife dann nicht auch 
dürfen?

Weiter müsste man sich auch überlegen, dass man die Schleife als 
Rekursionsersatz in eine Funktion packen sollte, denn dann sieht der 
Aufruf aus dem eigentlich Code heraus gleich aus, und das n des 
eigentlichen Codes ändert sich auch nicht.

von Karl H. (kbuchegg)


Lesenswert?

Deine erste Rekursion ist von einer speziellen Gestalt: Tail-Rekursion.
Sowas lässt sich immer in eine Schleife umwandeln. Ob dein
vorgeschlagenes Schema immer funktioniert kann ich so auf
die Schnelle nicht sagen, klingt aber nicht schlecht.

Im Allgemeinenen, wenn ich mich recht erinnere, wird eine
Rekursion so in eine iterative Lösung umgeformt, indem man
einen Stack einführt auf dem die Rekursionsstufen samt
aktuellem Aufrufstatus zwischengelagert werden und sowas wie
eine Statemachine eingeführt wird, die die Ablaufsteuerung
beim 'rekrusiven return' übernimmt.
Im Prinzip ist diese Lösung daher immer noch rekursiv, nur macht
man dann selbst die Arbeit, die ansonsten der Compiler bzw. die
CPU mit ihrem Callstack macht.

Schön ist das aber nicht. Hab das mal vor Jahren auf der Uni
mit dem bekannten Hanoi-Beispiel gesehen:
Die rekursive Lösung war kurz, die iterative Lösung lang.
Von der Laufzeit her, waren beide gleich.

von yalu (Gast)


Lesenswert?

Endrekursionen wie im ersten Beispiel löst man etwas allgemeiner
folgendermaßen auf:

1. Der gesamte Funktionsrumpf wird in eine Endlosschleife (for(;;)
   oder while(true)) gepackt.

2. Der rekursive Aufruf der Funktion in der letzten Zeile wird -
   zusammen mit einem evtl. davor stehenden return - weggelassen und
   die an die Funktion übergebenen Argumente explizit den
   Funktionsparametern zugewiesen.

Das ist alles. Ein oder mehrere bedingte Returns innerhalb der
Schleife stören nicht, sie tun das, was sie auch sollen. Wenn man
will, kann man natürlich nachträglich den entstandenen Code noch etwas
verschönern.

Viele Compiler lösen übrigens Endrekursionen automatisch auf. So bspw.
der GCC, bei den meisten Funktionalsprachen muss das Compiler sogar
tun. Wie es mit Java aussieht, müsstest du mal ausprobieren.

Nicht-Endrekursionen, darunter auch Mehrfachrekursionen, können
mittels Kellerautomaten rekursionsfrei ausgeführt werden. Such mal
nach dem Begriff "Entrekursivierung", da gibt es ziemlich viel zu
lesen.

von Jochen (Gast)


Lesenswert?

Ich danke für die vielen Antworten ;)
Könntet ihr mir dann vllt. mal schreiben, wie mein erstes Beispiel mit 
der Anleitung von yalu aussehen würde? Ich versteh das noch nicht 
richtig, und das ist genau das Rezept was ich gesucht habe :D

von yalu (Gast)


Lesenswert?

Im Prinzip schon so ähnlich, wie du es beschrieben hast, nur mache ich
mir zunächst noch keine Gedanken zur Negierung von irgendwelchen
Bedingungen. Die bleiben erst einmal so stehen, wie sie sind und
werden höchstens im Rahmen von Schönheitskorrekturen geändert:
1
static void down1 (int n)
2
{
3
  while(true)
4
  {
5
    if ( n <= 0)
6
      return;
7
8
    System.out.println( n + ",");
9
    n = n - 1;
10
  }
11
}

von Jochen (Gast)


Lesenswert?

OK, danke. Ich find das nämlich besser, wenn ich weiß, wie das "sauber" 
geht und nicht irgendetwas zusammenwürfel.

Wie macht man das, wenn eine Rekursion innerhalb einer Methode zweimal 
aufgerufen wird?

Als beispiel habe ich den Tiefendurchlauf von einem Baum:
1
public void inorder(ZahlNode node) {
2
    if (node == null) return ;
3
    inorder(node.left);
4
    do_something(node.z);
5
       inorder(node.right);

(Zur erklärung, node ist ein Objekt einer Klasse mit einem left, right 
Feld, wo der Verweis zum nächsten drin ist. z ist der Wert, der im 
Knoten gespeichert ist.)

von Christoph _. (chris)


Lesenswert?

Eine Standard-Methode in der Theorie der Programmiersprachen ist das 
ganze in Continuation Passing Style umzuwandeln. Damit kann man jede 
rekursive Funktion durch relativ simple Regeln in eine endrekursive (und 
damit praktisch eine iterative) Funktion verwandeln. Das Problem ist 
nur, dass Continuation Passing Style in C++ nicht sonderlich gut 
funktioniert, da C++ keine Closures unterstuetzt.

Continuation Passing Style basiert auf dem Konzept der "Continuation". 
Eine Continuation ist ein Parameter einer rekursiven Funktion. Eine 
Continuation ist aber auch selbst eine Funktion, die anstelle return 
aufgerufen wird. Eine Continuation stellt "den Rest der Berechnung" dar, 
also alles das, was nach einem rekursiven Aufruf noch stattfinden soll.
Das grosse Ziel ist dabei rekursive Aufrufe in End-Position zu bringen. 
Daher fasst man alle Berechnungen, die "nach" dem rekursiven Aufruf 
stattfinden sollen, in ein Funktions-Objekt zusammen und uebergibt 
dieses beim rekursiven Aufruf.

Eine Simulation dieses Stils koennte in C++ zum Beispiel so aussehen 
(ungetestet):
1
struct Continuation {
2
  Continuation* next;
3
  Node* current;
4
5
  void eval() {
6
    if(current) {
7
      do_something(current);
8
      inorder(current->right, next);
9
    }
10
    else if(next)
11
      next->eval();
12
  };
13
};
14
15
void inorder(Node* root, Continuation* c) {
16
  if(!root)
17
    c->eval();
18
  else {
19
    Continuation c2 = { c, root };
20
    inorder(root->left, &c2);
21
  }
22
}
23
24
// aufgerufen wird das ganze so:
25
Continuation c = { 0, 0 };
26
inorder(root, &c);
Durch diese Umschreibung stehen saemtliche Aufrufe von rekursiven 
Funktionen nun am Ende, direkt beim return. Dadurch haben wir also eine 
end-rekursive Funktion bekommen.
Continuation Passing Style ist in Sprachen wie C++ allerdings ziemlich 
unueblich und umstaendlich.

In funktionalen Sprachen wie Haskell geht das ganze viel eleganter. 
Zuerst die klassische rekursive Variante:
1
inOrder :: Node -> m ()
2
inOrder Empty = return ()
3
inOrder node  = inOrder (left node) >> doSomething node >> inOrder (right node)
In Continuation Passing Style:
1
inOrder :: Node -> m () -> m ()
2
inOrder Empty cont = cont
3
inOrder node  cont = inOrder (left node) (doSomething node >> inOrder (right node) cont)
Der Unterschied ist minimal, dennoch ist die zweite Version in 
Continuation Passing Style geschrieben und endrekursiv.

Die Sprache fuer Continuation Passing Style ist uebrigens Scheme, denn 
im Grunde ist das die einzige nicht-esoterische Sprache die echte 
Continuations unterstuetzt.

Wenn du mehr ueber Rekursion lernen moechtest, kann ich dir Scheme sehr 
nahelegen.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.