Ich habe über einige Praktiken für Entwicklungsinterviews gelesen, insbesondere über die technischen Fragen und Tests, die bei den Interviews gestellt wurden, und bin einige Male über Sprüche des Genres gestolpert. "Ok, Sie haben das Problem mit einer while-Schleife gelöst, jetzt können Sie es tun Rekursion "oder" Jeder kann dies mit einer 100-Zeilen-while-Schleife lösen, aber können sie dies mit einer 5-Zeilen-Rekursionsfunktion tun? " usw.
Meine Frage ist, ist Rekursion im Allgemeinen besser als wenn / während / für Konstrukte?
Ich habe ehrlich gesagt immer gedacht, dass Rekursion nicht zu bevorzugen ist, da sie auf den Stapelspeicher beschränkt ist, der viel kleiner als der Heap ist. Auch das Ausführen einer großen Anzahl von Funktions- / Methodenaufrufen ist vom Standpunkt der Leistung aus nicht optimal, aber ich kann falsch liegen...
quelle
Antworten:
Rekursion ist an sich nicht besser oder schlechter als Schleifen - jede hat Vor- und Nachteile, und diese hängen sogar von der Programmiersprache (und der Implementierung) ab.
Technisch gesehen passen iterative Schleifen besser zu typischen Computersystemen auf Hardwareebene: Auf Maschinencodeebene ist eine Schleife nur ein Test und ein bedingter Sprung, während die Rekursion (naiv implementiert) das Verschieben eines Stapelrahmens, das Springen, Zurückkehren und Zurückspringen umfasst vom Stapel. OTOH, viele Fälle von Rekursion (insbesondere solche, die trivial zu iterativen Schleifen äquivalent sind) können so geschrieben werden, dass der Stack-Push / Pop vermieden werden kann; Dies ist möglich, wenn der rekursive Funktionsaufruf das letzte Ereignis im Funktionskörper ist, bevor er zurückgegeben wird. Dies wird im Allgemeinen als Tail-Call-Optimierung (oder Tail-Recursion-Optimierung ) bezeichnet. Eine ordnungsgemäß tail-call-optimierte rekursive Funktion entspricht meist einer iterativen Schleife auf Maschinencodeebene.
Eine weitere Überlegung ist, dass iterative Schleifen destruktive Zustandsaktualisierungen erfordern, die sie mit der reinen (nebenwirkungsfreien) Sprachsemantik inkompatibel machen. Dies ist der Grund, warum reine Sprachen wie Haskell überhaupt keine Schleifenkonstrukte haben und viele andere funktionale Programmiersprachen sie entweder nicht vollständig oder so weit wie möglich meiden.
Der Grund, warum diese Fragen in Interviews so häufig vorkommen, ist, dass Sie zur Beantwortung vieler wichtiger Programmierkonzepte - Variablen, Funktionsaufrufe, Gültigkeitsbereiche und natürlich Schleifen und Rekursionen - genauestens verstehen müssen die mentale Flexibilität auf den Tisch zu bringen, die es Ihnen ermöglicht, ein Problem aus zwei radikal unterschiedlichen Blickwinkeln anzusprechen und zwischen verschiedenen Erscheinungsformen desselben Konzepts zu wechseln.
Erfahrungs- und Forschungsergebnisse legen nahe, dass es eine Grenze zwischen Menschen gibt, die Variablen, Zeiger und Rekursionen verstehen können, und solchen, die dies nicht tun. Fast alles andere in der Programmierung, einschließlich Frameworks, APIs, Programmiersprachen und deren Randbedingungen, kann durch Studium und Erfahrung erlangt werden. Wenn Sie jedoch keine Intuition für diese drei Kernkonzepte entwickeln können, sind Sie nicht in der Lage, Programmierer zu sein. Das Übersetzen einer einfachen iterativen Schleife in eine rekursive Version ist die schnellste Methode zum Herausfiltern von Nicht-Programmierern. Selbst ein unerfahrener Programmierer kann dies in der Regel in 15 Minuten tun, und es ist ein sehr sprachunabhängiges Problem, das der Kandidat auswählen kann eine Sprache ihrer Wahl, anstatt über Eigenheiten zu stolpern.
Wenn Sie in einem Interview eine Frage wie diese erhalten, ist dies ein gutes Zeichen: Der potenzielle Arbeitgeber sucht nach Personen, die programmieren können, und nicht nach Personen, die sich das Handbuch eines Programmierwerkzeugs auswendig gelernt haben.
quelle
Es hängt davon ab, ob.
Es ist auch erwähnenswert, dass durch die Unterstützung der Schwanzrekursion die rekursiven und iterativen Schleifen des Schwanzes gleichgesetzt werden. Das heißt, die Rekursion muss nicht immer den Stapel verschwenden.
Ein rekursiver Algorithmus kann auch immer iterativ implementiert werden, indem ein expliziter Stapel verwendet wird .
Schließlich würde ich bemerken, dass eine Lösung mit fünf Zeilen wahrscheinlich immer besser ist als eine Lösung mit 100 Zeilen (vorausgesetzt, sie sind tatsächlich gleichwertig).
quelle
Es gibt keine allgemein anerkannte Definition von "besser", wenn es um Programmierung geht, aber ich nehme es als "leichter zu warten / zu lesen".
Rekursion hat mehr Ausdruckskraft als iterative Schleifenkonstrukte: Ich sage dies, weil eine while-Schleife einer rekursiven Tail-Funktion entspricht und rekursive Funktionen nicht rekursiv sein müssen. Leistungsfähige Konstrukte sind in der Regel eine schlechte Sache, weil sie es Ihnen ermöglichen, schwer lesbare Dinge zu tun. Rekursion gibt Ihnen jedoch die Möglichkeit, Schleifen ohne Verwendung von Mutability zu schreiben, und meiner Meinung nach ist Mutability viel leistungsfähiger als Rekursion.
Von niedriger Ausdrucksstärke zu hoher Ausdrucksstärke stapeln sich Schleifen-Konstrukte wie folgt:
Im Idealfall würden Sie die am wenigsten aussagekräftigen Konstrukte verwenden, die Sie können. Wenn Ihre Sprache die Tail-Call-Optimierung nicht unterstützt, kann dies natürlich auch Ihre Wahl des Loop-Konstrukts beeinflussen.
quelle
Rekursion ist oft weniger offensichtlich. Weniger offensichtlich ist schwerer zu pflegen.
Wenn Sie
for(i=0;i<ITER_LIMIT;i++){somefunction(i);}
im Hauptablauf schreiben , stellen Sie klar, dass Sie eine Schleife schreiben. Wenn Sie schreiben, machensomefunction(ITER_LIMIT);
Sie nicht wirklich klar, was passieren wird. Nur Inhalte sehen: Diesersomefunction(int x)
Aufruf zeigt ansomefunction(x-1)
, dass es sich tatsächlich um eine Schleife handelt, die Iterationen verwendet. Außerdem können Sie eine Escape-Bedingung nicht einfachbreak;
irgendwo in der Mitte der Iterationen platzieren. Sie müssen entweder eine Bedingung hinzufügen, die auf dem gesamten Weg zurückgegeben wird, oder eine Ausnahme auslösen. (Und Ausnahmen machen nochmal komplizierter ...)Im Wesentlichen, wenn es eine offensichtliche Wahl zwischen Iteration und Rekursion ist, machen Sie die intuitive Sache. Wenn die Iteration die Aufgabe leicht erledigt, lohnt sich das Speichern von 2 Zeilen auf lange Sicht nur selten.
Wenn Sie 98 Zeilen sparen, ist das natürlich eine ganz andere Sache.
Es gibt Situationen, für die Rekursion einfach perfekt passt und die nicht wirklich ungewöhnlich sind. Durchquerung von Baumstrukturen, mehrfach verknüpften Netzwerken, Strukturen, die ihren eigenen Typ enthalten können, mehrdimensionale gezackte Arrays, im Wesentlichen alles, was weder ein einfacher Vektor noch ein Array fester Dimensionen ist. Wenn Sie einen bekannten, geraden Pfad durchlaufen, iterieren Sie. Wenn Sie in Unbekanntes eintauchen, greifen Sie zu.
Grundsätzlich sollten Sie
somefunction(x-1)
Iterationen vergessen , wenn Sie mehr als einmal pro Ebene von sich aus aufrufen möchten.... Das iterative Schreiben von Funktionen für Aufgaben, die am besten durch Rekursion erledigt werden, ist möglich, aber nicht angenehm. Wo immer Sie möchten
int
, brauchen Sie etwas wiestack<int>
. Ich habe es einmal gemacht, mehr als Übung als zu praktischen Zwecken. Ich kann Ihnen versichern, dass Sie, wenn Sie erst einmal vor einer solchen Aufgabe stehen, keine Zweifel mehr haben werden, wie Sie es ausdrückten.quelle
map
kann als rekursive Funktion definiert werden (siehe z. B. haskell.org/tutorial/functions.html ), auch wenn intuitiv klar ist, dass sie eine Liste durchläuft und auf jedes Mitglied der Liste eine Funktion anwendet.map
ist kein Schlüsselwort, es ist eine reguläre Funktion, aber das ist ein wenig irrelevant. Wenn funktionale Programmierer eine Rekursion verwenden, liegt dies normalerweise nicht daran, dass sie eine Abfolge von Aktionen ausführen möchten, sondern daran, dass das zu lösende Problem als Funktion und als Liste von Argumenten ausgedrückt werden kann. Das Problem kann dann auf eine andere Funktion und eine andere Liste von Argumenten reduziert werden. Schließlich haben Sie ein Problem, das trivial gelöst werden kann.Wie üblich ist dies im Allgemeinen nicht zu beantworten, da es zusätzliche Faktoren gibt, die in der Praxis zwischen den Fällen weitestgehend ungleich und innerhalb eines Anwendungsfalls ungleich sind. Hier sind einige der Belastungen.
quelle
Bei der Rekursion wird der Funktionsaufruf wiederholt, bei der Schleife wird der Sprung zum Speicherplatz wiederholt.
Sollte auch über Stapelüberlauf erwähnt werden - http://en.wikipedia.org/wiki/Stack_overflow
quelle
Es kommt wirklich auf die Bequemlichkeit oder die Anforderung an:
Wenn Sie die Programmiersprache Python verwenden , wird die Rekursion unterstützt. Standardmäßig ist die Rekursionstiefe jedoch auf 1000 begrenzt. Wenn es den Grenzwert überschreitet, erhalten wir einen Fehler oder eine Ausnahme. Diese Grenze kann geändert werden, aber wenn wir dies tun, kann es zu abnormalen Situationen in der Sprache kommen.
Zu diesem Zeitpunkt (Anzahl der Aufrufe mehr als Rekursionstiefe) müssen wir Schleifenkonstrukte bevorzugen. Ich meine, wenn die Stapelgröße nicht ausreicht, müssen wir die Schleifenkonstrukte bevorzugen.
quelle
Verwenden Sie das Strategieentwurfsmuster.
Wählen Sie je nach Belastung (und / oder anderen Bedingungen) eine aus.
quelle