it-swarm.com.de

Wie schreibe ich korrekte Schleifen?

Die meiste Zeit schreibe ich beim Schreiben von Schleifen normalerweise falsche Randbedingungen (z. B. falsches Ergebnis) oder meine Annahmen über Schleifenabschlüsse sind falsch (z. B. Endlos laufende Schleife). Obwohl ich meine Annahmen nach einigem Ausprobieren richtig gemacht habe, war ich zu frustriert, weil ich kein korrektes Rechenmodell in meinem Kopf hatte.

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

Die Fehler, die ich anfangs gemacht habe, waren:

  1. Anstelle von j> = 0 habe ich es j> 0 behalten.
  2. Ich war verwirrt, ob Array [j + 1] = Wert oder Array [j] = Wert.

Was sind Werkzeuge/mentale Modelle, um solche Fehler zu vermeiden?

64
CodeYogi

Prüfung

Nein, im Ernst, testen.

Ich programmiere seit über 20 Jahren und vertraue mir immer noch nicht, beim ersten Mal eine Schleife richtig zu schreiben. Ich schreibe und führe Tests durch, die beweisen, dass es funktioniert, bevor ich vermute, dass es funktioniert. Testen Sie jede Seite jeder Randbedingung. Zum Beispiel sollte ein rightIndex von 0 was tun? Wie wäre es mit -1?

Halte es einfach

Wenn andere auf einen Blick nicht sehen können, was es tut, machen Sie es zu schwer. Bitte ignorieren Sie die Leistung, wenn Sie etwas leicht verständliches schreiben können. Machen Sie es nur in dem unwahrscheinlichen Fall schneller, den Sie wirklich brauchen. Und selbst dann nur einmal, wenn Sie absolut sicher sind, dass Sie genau wissen, was Sie verlangsamt. Wenn Sie eine tatsächliche Big O Verbesserung erzielen können, ist diese Aktivität möglicherweise nicht sinnlos, aber machen Sie Ihren Code auch dann so lesbar wie möglich.

Um eins aus

Kennen Sie den Unterschied zwischen dem Zählen Ihrer Finger und dem Zählen der Zwischenräume zwischen Ihren Fingern. Manchmal sind die Räume das, was eigentlich wichtig ist. Lass dich nicht von deinen Fingern ablenken. Wissen Sie, ob Ihr Daumen ein Finger ist. Wissen Sie, ob die Lücke zwischen Ihrem kleinen Finger und Ihrem Daumen als Leerzeichen zählt.

Bemerkungen

Bevor Sie sich im Code verlieren, versuchen Sie zu sagen, was Sie auf Englisch meinen. Geben Sie Ihre Erwartungen klar an. Erklären Sie nicht, wie der Code funktioniert. Erklären Sie, warum Sie es tun lassen, was es tut. Halten Sie Implementierungsdetails fern. Es sollte möglich sein, den Code umzugestalten, ohne den Kommentar ändern zu müssen.

Der beste Kommentar ist ein guter Name.

Wenn Sie alles, was Sie zu sagen haben, mit einem guten Namen sagen können, sagen Sie es NICHT noch einmal mit einem Kommentar.

Abstraktionen

Objekte, Funktionen, Arrays und Variablen sind Abstraktionen, die nur so gut sind wie die Namen, die sie erhalten. Geben Sie ihnen Namen, die sicherstellen, dass Menschen, die in sie hineinschauen, nicht überrascht werden, was sie finden.

Kurznamen

Verwenden Sie kurze Namen für kurzlebige Dinge. i ist ein guter Name für einen Index in einer schönen engen Schleife in einem kleinen Bereich, der seine Bedeutung offensichtlich macht. Wenn i lange genug lebt, um sich Zeile für Zeile mit anderen Ideen und Namen zu verteilen, die mit i verwechselt werden können, ist es Zeit, i einen schönen langen erklärenden Namen zu geben .

Lange Namen

Kürzen Sie einen Namen niemals nur aus Gründen der Zeilenlänge. Finden Sie einen anderen Weg, um Ihren Code zu gestalten.

Leerzeichen

Fehler lieben es, sich in unlesbarem Code zu verstecken. Wenn Sie in Ihrer Sprache Ihren Einrückungsstil auswählen können, müssen Sie zumindest konsistent sein. Lassen Sie Ihren Code nicht wie einen Rauschstrom aussehen. Code sollte so aussehen, als würde er in Formation marschieren.

Schleifenkonstrukte

Lernen und überprüfen Sie die Schleifenstrukturen in Ihrer Sprache. Beobachten, wie ein Debugger eine for(;;) - Schleife hervorhebt kann sehr lehrreich sein. Lerne alle Formen. while, do while, while(true), for each. Verwenden Sie die einfachste, mit der Sie durchkommen können. Nachschlagen Pumpe vorbereiten . Erfahren Sie, was break und continue tun, wenn Sie sie haben. Kennen Sie den Unterschied zwischen c++ Und ++c. Haben Sie keine Angst, früh zurückzukehren, solange Sie immer alles schließen, was geschlossen werden muss. Endlich blockiert oder vorzugsweise etwas, das es zum automatischen Schließen beim Öffnen markiert: sing-Anweisung / Try with Resources .

Schleifenalternativen

Lassen Sie etwas anderes die Schleife machen, wenn Sie können. Es ist augenschonender und bereits debuggt. Diese gibt es in vielen Formen: Sammlungen oder Streams, die map(), reduce(), foreach() und andere solche Methoden zulassen, die ein Lambda anwenden. Suchen Sie nach Spezialfunktionen wie Arrays.fill(). Es gibt auch eine Rekursion, aber erwarten Sie nur, dass dies in besonderen Fällen die Dinge einfacher macht. Verwenden Sie im Allgemeinen keine Rekursion, bis Sie sehen, wie die Alternative aussehen würde.

Oh, und teste.

Test, Test, Test.

Habe ich das Testen erwähnt?

Es gab noch eine Sache. Ich kann mich nicht erinnern. Begonnen mit einem T ...

206
candied_orange

Bei der Programmierung ist Folgendes zu beachten:

und wenn man Neuland erkundet (wie das Jonglieren mit Indizes), kann es sehr, sehr nützlich sein, nicht nur über diese nachzudenken, sondern sie tatsächlich im Code mit Behauptungen explizit zu machen .

Nehmen wir Ihren Originalcode:

/**
 * Inserts the given value in proper position in the sorted subarray i.e. 
 * array[0...rightIndex] is the sorted subarray, on inserting a new value 
 * our new sorted subarray becomes array[0...rightIndex+1].
 * @param array The whole array whose initial elements [0...rightIndex] are 
 * sorted.
 * @param rightIndex The index till which sub array is sorted.
 * @param value The value to be inserted into sorted sub array.
 */
function insert(array, rightIndex, value) {
    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];
    }   
    array[j + 1] = value; 
};

Und überprüfen Sie, was wir haben:

  • voraussetzung: array[0..rightIndex] ist sortiert
  • nachbedingung: array[0..rightIndex+1] wird sortiert
  • invariant: 0 <= j <= rightIndex, aber es scheint ein bisschen überflüssig zu sein; oder wie @Jules in den Kommentaren am Ende einer "Runde" vermerkt, for n in [j, rightIndex+1] => array[j] > value.
  • invariant: Am Ende einer "Runde" wird array[0..rightIndex+1] sortiert

Sie können also zuerst eine is_sorted - Funktion sowie eine min - Funktion schreiben, die an einem Array-Slice arbeitet, und dann behaupten:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

Es gibt auch die Tatsache, dass Ihre Schleifenbedingung etwas kompliziert ist; Vielleicht möchten Sie es sich leichter machen, indem Sie die Dinge aufteilen:

function insert(array, rightIndex, value) {
    assert(is_sorted(array[0..rightIndex]));

    for (var j = rightIndex; j >= 0; j--) {
        if (array[j] <= value) { break; }

        array[j + 1] = array[j];

        assert(min(array[j..rightIndex+1]) > value);
        assert(is_sorted(array[0..rightIndex+1]));
    }   
    array[j + 1] = value; 

    assert(is_sorted(array[0..rightIndex+1]));
};

Jetzt ist die Schleife einfach (j geht von rightIndex zu 0).

Schließlich muss dies nun getestet werden:

  • denken Sie an Randbedingungen (rightIndex == 0, rightIndex == array.size - 2)
  • stellen Sie sich vor, value ist kleiner als array[0] oder größer als array[rightIndex]
  • stellen Sie sich vor, value ist gleich array[0], array[rightIndex] oder einem mittleren Index

Unterschätzen Sie auch nicht Fuzzing. Sie haben Zusicherungen, um Fehler zu erkennen. Generieren Sie also ein zufälliges Array und sortieren Sie es mit Ihrer Methode. Wenn eine Zusicherung ausgelöst wird, haben Sie einen Fehler gefunden und können Ihre Testsuite erweitern.

73
Matthieu M.

Verwenden Sie Unit Testing/TDD

Wenn Sie wirklich über eine for -Schleife auf Sequenzen zugreifen müssen, können Sie die Fehler durch nit-Test und insbesondere testgetriebene Entwicklung vermeiden.

Stellen Sie sich vor, Sie müssen eine Methode implementieren, die die Werte, die über Null liegen, in umgekehrter Reihenfolge annimmt. Welche Testfälle könnten Sie sich vorstellen?

  1. Eine Sequenz enthält einen Wert, der Null überlegen ist.

    Tatsächlich: [5]. Erwartet: [5].

    Die einfachste Implementierung, die die Anforderungen erfüllt, besteht darin, die Quellsequenz einfach an den Aufrufer zurückzugeben.

  2. Eine Sequenz enthält zwei Werte, die beide über Null liegen.

    Tatsächlich: [5, 7]. Erwartet: [7, 5].

    Jetzt können Sie die Sequenz nicht einfach zurückgeben, sondern sollten sie umkehren. Würden Sie eine for (;;) - Schleife verwenden, spielt ein anderes Sprachkonstrukt oder eine Bibliotheksmethode keine Rolle.

  3. Eine Sequenz enthält drei Werte, von denen einer Null ist.

    Tatsächlich: [5, 0, 7]. Erwartet: [7, 5].

    Jetzt sollten Sie den Code ändern, um die Werte zu filtern. Dies kann wiederum durch eine if -Anweisung oder einen Aufruf Ihrer bevorzugten Framework-Methode ausgedrückt werden.

  4. Abhängig von Ihrem Algorithmus (da es sich um White-Box-Tests handelt, ist die Implementierung von Bedeutung) müssen Sie möglicherweise speziell den Fall der leeren Sequenz [] → [] Behandeln oder auch nicht. Oder Sie können sicherstellen, dass der Edge-Fall, in dem alle Werte negativ sind [-4, 0, -5, 0] → [], Richtig behandelt wird, oder dass die negativen Grenzwerte wie folgt lauten: [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]. In vielen Fällen haben Sie jedoch nur die drei oben beschriebenen Tests: Bei jedem zusätzlichen Test werden Sie Ihren Code nicht ändern und sind daher irrelevant.

Arbeiten Sie auf einer höheren Abstraktionsebene

In vielen Fällen können Sie jedoch die meisten dieser Fehler vermeiden, indem Sie auf einer höheren Abstraktionsebene mit vorhandenen Bibliotheken/Frameworks arbeiten. Diese Bibliotheken/Frameworks ermöglichen das Zurücksetzen, Sortieren, Teilen und Verknüpfen der Sequenzen, das Einfügen oder Entfernen von Werten in Arrays oder doppelt verknüpften Listen usw.

Normalerweise kann foreach anstelle von for verwendet werden, wodurch die Überprüfung der Randbedingungen irrelevant wird: Die Sprache erledigt dies für Sie. Einige Sprachen wie Python haben nicht einmal das Konstrukt for (;;), sondern nur for ... in ....

In C # ist LINQ besonders praktisch, wenn Sie mit Sequenzen arbeiten.

var result = source.Skip(5).TakeWhile(c => c > 0);

ist viel besser lesbar und weniger fehleranfällig als die Variante for:

for (int i = 5; i < source.Length; i++)
{
    var value = source[i];
    if (value <= 0)
    {
        break;
    }

    yield return value;
}
28

Ich stimme anderen Leuten zu, die sagen, testen Sie Ihren Code. Es ist jedoch auch schön, es überhaupt richtig zu machen. Ich habe in vielen Fällen die Tendenz, Randbedingungen falsch zu machen, deshalb habe ich mentale Tricks entwickelt, um solche Probleme zu verhindern.

Mit einem 0-indizierten Array sind Ihre normalen Bedingungen:

for (int i = 0; i < length; i++)

oder

for (int i = length - 1; i >= 0; i--)

Diese Muster sollten zur zweiten Natur werden, über die Sie überhaupt nicht nachdenken müssen.

Aber nicht alles folgt genau diesem Muster. Wenn Sie sich nicht sicher sind, ob Sie es richtig geschrieben haben, ist hier Ihr nächster Schritt:

Stecken Sie Werte ein und bewerten Sie den Code in Ihrem eigenen Gehirn. Machen Sie es sich so einfach wie möglich. Was passiert, wenn die relevanten Werte Nullen sind? Was passiert, wenn sie 1s sind?

for(var j = rightIndex; j >= 0 && array[j] > value; j--) {
    array[j + 1] = array[j];
}   
array[j + 1] = value;

In Ihrem Beispiel sind Sie sich nicht sicher, ob es [j] = Wert oder [j + 1] = Wert sein soll. Zeit, es manuell zu bewerten:

Was passiert, wenn Sie eine Array-Länge von 0 haben? Die Antwort wird offensichtlich: rightIndex muss (Länge - 1) == -1 sein, also beginnt j bei -1. Um also bei Index 0 einzufügen, müssen Sie 1 hinzufügen.

Wir haben also bewiesen, dass die endgültige Bedingung korrekt ist, aber nicht das Innere der Schleife.

Was passiert, wenn Sie ein Array mit 1 Element, 10, haben und wir versuchen, eine 5 einzufügen? Bei einem einzelnen Element sollte rightIndex bei 0 beginnen. Beim ersten Durchlaufen der Schleife ist also j = 0, also "0> = 0 && 10> 5". Da wir die 5 bei Index 0 einfügen möchten, sollte die 10 auf Index 1 verschoben werden, also Array [1] = Array [0]. Da dies geschieht, wenn j 0 ist, ist Array [j + 1] = Array [j + 0].

Wenn Sie versuchen, sich ein großes Array vorzustellen und was passiert, wenn Sie es an einem beliebigen Ort einfügen, wird Ihr Gehirn wahrscheinlich überfordert sein. Wenn Sie sich jedoch an einfache Größenbeispiele von 0/1/2 halten, sollte es einfach sein, einen schnellen mentalen Durchlauf durchzuführen und zu sehen, wo Ihre Randbedingungen zusammenbrechen.

Stellen Sie sich vor, Sie haben noch nie von dem Zaunpfostenproblem gehört und ich sage Ihnen, ich habe 100 Zaunpfosten in einer geraden Linie, wie viele Segmente zwischen ihnen. Wenn Sie versuchen, sich 100 Zaunpfosten in Ihrem Kopf vorzustellen, werden Sie einfach überwältigt sein. Was sind die wenigsten Zaunpfosten, um einen gültigen Zaun zu erstellen? Sie benötigen 2, um einen Zaun zu erstellen. Stellen Sie sich also 2 Pfosten vor, und das mentale Bild eines einzelnen Segments zwischen den Pfosten macht dies sehr deutlich. Sie müssen nicht dort sitzen und Beiträge und Segmente zählen, weil Sie das Problem zu etwas gemacht haben, das für Ihr Gehirn intuitiv offensichtlich ist.

Sobald Sie denken, dass es richtig ist, ist es gut, es durch einen Test zu führen und sicherzustellen, dass der Computer das tut, was Sie denken, dass es sollte, aber an diesem Punkt sollte es nur eine Formalität sein.

15
Bryce Wagner

Ich war zu frustriert wegen des Mangels an korrektem Rechenmodell in meinem Kopf.

Ist ein sehr interessanter Punkt zu dieser Frage und es hat diesen Kommentar generiert: -

Es gibt nur einen Weg: Verstehen Sie Ihr Problem besser. Aber das ist so allgemein wie Ihre Frage. - Thomas Junk

... und Thomas hat recht. Wenn Sie keine klare Absicht für eine Funktion haben, sollte dies eine rote Fahne sein - ein klarer Hinweis darauf, dass Sie sofort STOPPEN, einen Bleistift und Papier nehmen, sich von der IDE entfernen und das Problem ordnungsgemäß beheben sollten. oder zumindest überprüfen Sie, was Sie getan haben.

Ich habe so viele Funktionen und Klassen gesehen, die zu einem völligen Durcheinander geworden sind, weil die Autoren versucht haben, die Implementierung zu definieren, bevor sie das Problem vollständig definiert haben. Und es ist so einfach damit umzugehen.

Wenn Sie das Problem nicht vollständig verstehen, ist es auch unwahrscheinlich, dass Sie die optimale Lösung codieren (entweder in Bezug auf Effizienz oder Klarheit), und Sie werden auch nicht in der Lage sein, wirklich nützliche Komponententests in einer TDD-Methodik zu erstellen.

Nehmen Sie Ihren Code hier als Beispiel, er enthält eine Reihe potenzieller Fehler, die Sie noch nicht berücksichtigt haben, zum Beispiel: -

  • was ist, wenn rightIndex zu niedrig ist? (Hinweis: es wird Datenverlust beinhalten)
  • was ist, wenn rightIndex außerhalb der Array-Grenzen liegt? (Erhalten Sie eine Ausnahme oder haben Sie sich gerade einen Pufferüberlauf erstellt?)

Es gibt noch einige andere Probleme im Zusammenhang mit der Leistung und dem Design des Codes ...

  • muss dieser Code skaliert werden? Ist es die beste Option, das Array sortiert zu halten, oder sollten Sie sich andere Optionen ansehen (z. B. eine verknüpfte Liste?)
  • können Sie sich Ihrer Annahmen sicher sein? (Können Sie garantieren, dass das Array sortiert ist, und was ist, wenn dies nicht der Fall ist?)
  • erfinden Sie das Rad neu? Sortierte Arrays sind ein bekanntes Problem. Haben Sie die vorhandenen Lösungen untersucht? Gibt es in Ihrer Sprache bereits eine Lösung (z. B. SortedList<t> In C #)?
  • sollten Sie jeweils einen Array-Eintrag manuell kopieren? oder bietet Ihre Sprache allgemeine Funktionen wie JScript Array.Insert(...)? Wäre dieser Code klarer?

Es gibt viele Möglichkeiten, diesen Code zu verbessern, aber bis Sie richtig definiert haben, wozu dieser Code benötigt wird, entwickeln Sie keinen Code, sondern hacken ihn nur zusammen in der Hoffnung, dass er funktioniert. Investiere die Zeit in sie und dein Leben wird einfacher.

11
James Snell

Off-by-One-Fehler sind einer der häufigsten Programmierfehler. Sogar erfahrene Entwickler verstehen das manchmal falsch. Höhere Sprachen haben normalerweise Iterationskonstrukte wie foreach oder map, wodurch eine explizite Indizierung insgesamt vermieden wird. Manchmal benötigen Sie jedoch eine explizite Indizierung, wie in Ihrem Beispiel.

Die Herausforderung besteht darin, sich Bereiche Array-Zellen vorzustellen. Ohne ein klares mentales Modell wird es verwirrend, wann die Endpunkte ein- oder ausgeschlossen werden sollen.

Bei der Beschreibung von Array-Bereichen lautet die Konvention: Untergrenze einschließen, Obergrenze ausschließen . Zum Beispiel ist der Bereich 0..3 die Zellen 0,1,2. Diese Konventionen werden in allen 0-indizierten Sprachen verwendet. Beispielsweise gibt die Methode slice(start, end) in JavaScript das Subarray zurück, beginnend mit index start bis aber nicht einschließlich index end.

Es ist klarer, wenn Sie an Bereichsindizes denken, die die Kanten zwischen Arrayzellen beschreiben. Die folgende Abbildung ist ein Array mit der Länge 9, und die Zahlen unter den Zellen sind an den Kanten ausgerichtet und werden zur Beschreibung von Arraysegmenten verwendet. Z.B. Aus der Abbildung geht hervor, dass der Bereich 2..5 die Zellen 2,3,4 ist.

┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │   -- cell indexes, e.g array[3]
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
0   1   2   3   4   5   6   7   8   9   -- segment bounds, e.g. slice(2,5) 
        └───────────┘ 
          range 2..5

Dieses Modell stimmt damit überein, dass die Array-Länge die Obergrenze eines Arrays ist. Ein Array mit der Länge 5 hat die Zellen 0..5, was bedeutet, dass es die fünf Zellen 0,1,2,3,4 gibt. Dies bedeutet auch, dass die Länge eines Segments die Obergrenze abzüglich der Untergrenze ist, d. H. Das Segment 2..5 hat 5-2 = 3 Zellen.

Wenn Sie dieses Modell berücksichtigen, wenn Sie nach oben oder unten iterieren, wird viel klarer, wann die Endpunkte ein- oder ausgeschlossen werden müssen. Wenn Sie nach oben iterieren, müssen Sie den Startpunkt einschließen, aber den Endpunkt ausschließen. Wenn Sie nach unten iterieren, müssen Sie den Startpunkt (die obere Grenze) ausschließen, aber den Endpunkt (die untere Grenze) einschließen.

Da Sie in Ihrem Code nach unten iterieren, müssen Sie die Untergrenze 0 einschließen, damit Sie iterieren, während j >= 0.

Vor diesem Hintergrund verstößt Ihre Entscheidung, das Argument rightIndex als letzten Index im Subarray darzustellen, gegen die Konvention. Dies bedeutet, dass Sie beide Endpunkte (0 und rightIndex) in die Iteration einbeziehen müssen. Es macht es auch schwierig, das leere Segment darzustellen (das Sie benötigen, wenn Sie mit dem Sortieren beginnen). Sie müssen tatsächlich -1 als rightIndex verwenden, wenn Sie den ersten Wert einfügen. Das scheint ziemlich unnatürlich. Es erscheint natürlicher, wenn rightIndex den Index nach dem Segment angibt, sodass 0 das leere Segment darstellt.

Natürlich ist Ihr Code besonders verwirrend, da er das sortierte Subarray um eins erweitert und das Element unmittelbar nach dem ursprünglich sortierten Subarray überschreibt. Sie lesen also aus dem Index j, schreiben aber den Wert in j + 1. Hier sollte nur klar sein, dass j die Position im anfänglichen Subarray vor dem Einfügen ist. Wenn Indexoperationen zu schwierig werden, hilft es mir, sie auf einem Stück Rasterpapier darzustellen.

10
JacquesB

Die Einführung in Ihre Frage lässt mich denken, dass Sie nicht gelernt haben, richtig zu codieren. Jeder, der länger als ein paar Wochen in einer zwingenden Sprache programmiert, sollte in mehr als 90% der Fälle beim ersten Mal die richtigen Loop-Grenzen erreichen. Vielleicht beeilen Sie sich, mit dem Codieren zu beginnen, bevor Sie das Problem ausreichend durchdacht haben.

Ich schlage vor, dass Sie diesen Mangel beheben, indem Sie (wieder) lernen, wie man Schleifen schreibt - und ich würde empfehlen, einige Stunden mit Papier und Bleistift durch eine Reihe von Schleifen zu arbeiten. Nehmen Sie sich dafür einen Nachmittag frei. Dann arbeiten Sie ungefähr 45 Minuten am Tag an dem Thema, bis Sie es wirklich verstanden haben.

Es ist alles sehr gut zu testen, aber Sie sollten in der Erwartung testen, dass Sie im Allgemeinen Ihre Schleifengrenzen (und den Rest Ihres Codes) richtig machen.

Vielleicht sollte ich meinen Kommentar etwas fleischiger machen:

Es gibt nur einen Weg: Verstehen Sie Ihr Problem besser. Aber das ist so allgemein wie Ihre Frage

Ihr Punkt ist

Obwohl ich meine Annahmen nach einigem Ausprobieren richtig gemacht habe, war ich zu frustriert, weil ich kein korrektes Rechenmodell in meinem Kopf hatte.

Wenn ich lese trial and error, meine Alarmglocken läuten. Natürlich kennen viele von uns den Geisteszustand, wenn man ein kleines Problem beheben will und seinen Kopf um andere Dinge gewickelt hat und auf die eine oder andere Weise zu raten beginnt, um den Code seem dazu zu bringen, zu tun, was soll tun. Daraus ergeben sich einige hackige Lösungen - und einige davon sind rein Genie; aber um ehrlich zu sein: die meisten von ihnen sind es nicht. Ich eingeschlossen, diesen Zustand zu kennen.

Unabhängig von Ihrem konkreten Problem haben Sie Fragen gestellt, wie Sie sich verbessern können:

1) Test

Das wurde von anderen gesagt und ich hätte nichts Wertvolles hinzuzufügen

2) Problemanalyse

Es ist schwer, dazu einen Rat zu geben. Ich kann Ihnen nur zwei Hinweise geben, die Ihnen wahrscheinlich dabei helfen, Ihre Fähigkeiten zu diesem Thema zu verbessern:

  • das Offensichtliche und Trivialste ist auf lange Sicht das Effektivste: viele Probleme lösen. Während Sie üben und wiederholen, entwickeln Sie die Denkweise, die Ihnen bei zukünftigen Aufgaben hilft. Das Programmieren ist wie jede andere Aktivität, die durch hartes Üben verbessert werden muss

Code Katas sind ein Weg, der ein bisschen helfen könnte.

Wie kommst du dazu, ein großartiger Musiker zu sein? Es hilft, die Theorie zu kennen und die Mechanik Ihres Instruments zu verstehen. Es hilft, Talent zu haben. Aber letztendlich kommt Größe vom Üben; Wenden Sie die Theorie immer wieder an und verwenden Sie Feedback, um jedes Mal besser zu werden.

Code Kata

Eine Seite, die mir sehr gut gefällt: Code Wars

Erreichen Sie Meisterschaft durch Herausforderung Verbessern Sie Ihre Fähigkeiten, indem Sie mit anderen an echten Code-Herausforderungen trainieren

Dies sind relativ kleine Probleme, mit denen Sie Ihre Programmierkenntnisse verbessern können. Und was mir an Code Wars Am besten gefällt, ist, dass Sie Ihre Lösung mit einer von anderen vergleichen können.

Oder vielleicht sollten Sie sich Exercism.io ansehen, wo Sie Feedback von der Community erhalten.

  • Der andere Rat ist fast genauso trivial: Lerne, Probleme zu zerlegen Du musst dich selbst trainieren und Probleme in wirklich kleine Probleme zerlegen. Wenn Sie sagen, Sie haben Probleme beim Schreiben von Schleifen, machen Sie den Fehler, dass Sie die Schleife als ganzes Konstrukt sehen und sie nicht in Stücke zerlegen. Wenn Sie lernen, Dinge auseinander zu nehmen Schritt für Schritt, lernen Sie, solche Fehler zu vermeiden.

Ich weiß - wie ich oben sagte, manchmal sind Sie in einem solchen Zustand -, dass es schwierig ist, "einfache" Dinge in "absolut einfache" Aufgaben zu zerlegen; aber es hilft sehr.

Ich erinnere mich, als ich professionell Programmieren lernte, hatte ich riesig Probleme beim Debuggen meines Codes. Was war das Problem? Hybris - Der Fehler kann nicht in diesem und jenem Bereich des Codes liegen, weil ich weiß dass es nicht sein kann. Und in der Folge? Ich habe den Code durchgesehen, anstatt ihn zu analysieren Ich musste lernen - auch wenn es mühsam war, meinen Code aufzuschlüsseln Anweisung für Anweisung.

3) Entwickeln Sie einen Werkzeuggürtel

Neben der Kenntnis Ihrer Sprache und Ihrer Werkzeuge - ich weiß, dass dies die glänzenden Dinge sind, an die Entwickler zuerst denken - lernen Sie Algorithmen (auch bekannt als Lesen).

Hier sind zunächst zwei Bücher:

Dies ist wie das Erlernen einiger Rezepte, um mit dem Kochen zu beginnen. Zuerst weißt du nicht, was du tun sollst, also musst du schauen, was vorher Köche für dich gekocht hat. Gleiches gilt für Algortihms. Algorithmen sind wie das Kochen von Rezepten für gängige Mahlzeiten (Datenstrukturen, Sortieren, Hashing usw.). Wenn Sie sie auswendig kennen (zumindest versuchen), haben Sie einen guten Ausgangspunkt.

3a) Programmierkonstrukte kennen

Dieser Punkt ist sozusagen eine Ableitung. Kennen Sie Ihre Sprache - und besser: wissen, welche Konstrukte möglich sind in Ihrer Sprache.

Ein häufiger Punkt für schlechten oder ineffizienten Code ist manchmal, dass der Programmierer den Unterschied zwischen verschiedenen Arten von Schleifen nicht kennt (for-, while- und do- Schleifen). Sie sind irgendwie alle austauschbar; Unter bestimmten Umständen führt die Auswahl eines anderen Schleifenkonstrukts zu einem eleganteren Code.

Und da ist Duffs Gerät ...

P.:

andernfalls ist Ihr Kommentar nicht gut als Donal Trump.

Ja, wir sollten das Codieren wieder großartig machen!

Ein neues Motto für Stackoverflow.

3
Thomas Junk

Ich werde ein detaillierteres Beispiel geben, wie Pre/Post-Bedingungen und Invarianten verwendet werden, um eine korrekte Schleife zu entwickeln. Zusammen werden solche Behauptungen als Spezifikation oder Vertrag bezeichnet.

Ich schlage nicht vor, dass Sie versuchen, dies für jede Schleife zu tun. Aber ich hoffe, dass Sie es nützlich finden, den damit verbundenen Denkprozess zu sehen.

Zu diesem Zweck übersetze ich Ihre Methode in ein Tool namens Microsoft Dafny , mit dem die Richtigkeit solcher Spezifikationen nachgewiesen werden soll. Es überprüft auch die Beendigung jeder Schleife. Bitte beachten Sie, dass Dafny keine for Schleife hat, daher musste ich stattdessen eine while Schleife verwenden.

Abschließend werde ich zeigen, wie Sie solche Spezifikationen verwenden können, um eine wohl etwas einfachere Version Ihrer Schleife zu entwerfen. Diese einfachere Loop-Version hat tatsächlich die Loop-Bedingung j > 0 Und die Zuweisung array[j] = value - wie es Ihre ursprüngliche Intuition war.

Dafny wird uns beweisen, dass diese beiden Schleifen korrekt sind und dasselbe tun.

Ich werde dann aufgrund meiner Erfahrung einen allgemeinen Anspruch darauf erheben, wie man eine korrekte Rückwärtsschleife schreibt, die Ihnen vielleicht helfen wird, wenn Sie in Zukunft mit dieser Situation konfrontiert werden.

Erster Teil - Schreiben einer Spezifikation für die Methode

Die erste Herausforderung besteht darin, zu bestimmen, was die Methode eigentlich tun soll. Zu diesem Zweck habe ich Vor- und Nachbedingungen entworfen, die das Verhalten der Methode festlegen. Um die Spezifikation genauer zu machen, habe ich die Methode erweitert, damit sie den Index zurückgibt, in den value eingefügt wurde.

method insert(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  // the method will modify the array
  modifies arr
  // the array will not be null
  requires arr != null
  // the right index is within the bounds of the array
  // but not the last item
  requires 0 <= rightIndex < arr.Length - 1
  // value will be inserted into the array at index
  ensures arr[index] == value 
  // index is within the bounds of the array
  ensures 0 <= index <= rightIndex + 1
  // the array to the left of index is not modified
  ensures arr[..index] == old(arr[..index])
  // the array to the right of index, up to right index is
  // shifted to the right by one place
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  // the array to the right of rightIndex+1 is not modified
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])

Diese Spezifikation erfasst das Verhalten der Methode vollständig. Meine Hauptbeobachtung zu dieser Spezifikation ist, dass es vereinfacht wäre, wenn der Prozedur der Wert rightIndex+1 Anstelle von rightIndex übergeben würde. Da ich jedoch nicht sehen kann, woher diese Methode stammt, weiß ich nicht, welche Auswirkungen diese Änderung auf den Rest des Programms haben würde.

Teil Zwei - Bestimmen einer Schleifeninvariante

Nachdem wir eine Spezifikation für das Verhalten der Methode haben, müssen wir eine Spezifikation des Schleifenverhaltens hinzufügen, die Dafny davon überzeugt, dass die Ausführung der Schleife beendet wird und zum gewünschten Endzustand von array führt.

Das Folgende ist Ihre ursprüngliche Schleife, übersetzt in Dafny-Syntax mit hinzugefügten Schleifeninvarianten. Ich habe es auch geändert, um den Index zurückzugeben, in den der Wert eingefügt wurde.

{
    // take a copy of the initial array, so we can refer to it later
    // ghost variables do not affect program execution, they are just
    // for specification
    ghost var initialArr := arr[..];


    var j := rightIndex;
    while(j >= 0 && arr[j] > value)
       // the loop always decreases j, so it will terminate
       decreases j
       // j remains within the loop index off-by-one
       invariant -1 <= j < arr.Length
       // the right side of the array is not modified
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       // the part of the array looked at by the loop so far is
       // shifted by one place to the right
       invariant arr[j+2..rightIndex+2] == initialArr[j+1..rightIndex+1]
       // the part of the array not looked at yet is not modified
       invariant arr[..j+1] == initialArr[..j+1] 
    {
        arr[j + 1] := arr[j];
        j := j-1;
    }   
    arr[j + 1] := value;
    return j+1; // return the position of the insert
}

Dies wird in Dafny überprüft. Sie können es selbst sehen durch folgen Sie diesem Link . Ihre Schleife implementiert also die Methodenspezifikation, die ich in Teil 1 geschrieben habe, korrekt. Sie müssen entscheiden, ob diese Methodenspezifikation wirklich das gewünschte Verhalten ist.

Beachten Sie, dass Dafny hier einen Korrektheitsnachweis erbringt. Dies ist eine weitaus stärkere Garantie für die Richtigkeit, als dies möglicherweise durch Tests erreicht werden kann.

Dritter Teil - eine einfachere Schleife

Jetzt haben wir eine Methodenspezifikation, die das Verhalten der Schleife erfasst. Wir können die Implementierung der Schleife sicher ändern und gleichzeitig das Vertrauen behalten, dass wir das Schleifenverhalten nicht geändert haben.

Ich habe die Schleife so geändert, dass sie Ihren ursprünglichen Intuitionen über die Schleifenbedingung und den Endwert von j entspricht. Ich würde argumentieren, dass diese Schleife einfacher ist als die Schleife, die Sie in Ihrer Frage beschrieben haben. Es ist häufiger in der Lage, j anstelle von j+1 Zu verwenden.

  1. Starten Sie j bei rightIndex+1

  2. Ändern Sie die Schleifenbedingung in j > 0 && arr[j-1] > value

  3. Ändern Sie die Zuordnung in arr[j] := value

  4. Dekrementieren Sie den Schleifenzähler am Ende der Schleife und nicht am Anfang

Hier ist der Code. Beachten Sie, dass die Schleifeninvarianten jetzt auch etwas einfacher zu schreiben sind:

method insert2(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 0 <= rightIndex < arr.Length - 1
  ensures 0 <= index <= rightIndex + 1
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+2] == old(arr[index..rightIndex+1])
  ensures arr[rightIndex+2..] == old(arr[rightIndex+2..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex+1;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+2..] == initialArr[rightIndex+2..]
       invariant arr[j+1..rightIndex+2] == initialArr[j..rightIndex+1]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}

Vierter Teil - Ratschläge zur Rückwärtsschleife

Nachdem ich über einige Jahre viele Loops geschrieben und als richtig erwiesen habe, habe ich die folgenden allgemeinen Ratschläge zum Rückwärtsschleifen.

Es ist fast immer einfacher, über eine Rückwärtsschleife (Dekrementierungsschleife) nachzudenken und diese zu schreiben, wenn die Dekrementierung am Anfang der Schleife und nicht am Ende durchgeführt wird.

Leider macht das Konstrukt for in vielen Sprachen dies schwierig.

Ich vermute (kann aber nicht beweisen), dass diese Komplexität den Unterschied in Ihrer Intuition darüber verursacht hat, was die Schleife sein sollte und was sie tatsächlich sein musste. Sie sind es gewohnt, über Vorwärtsschleifen (inkrementelle Schleifen) nachzudenken. Wenn Sie eine Rückwärtsschleife (Dekrementierungsschleife) schreiben möchten, versuchen Sie, die Schleife zu erstellen, indem Sie versuchen, die Reihenfolge umzukehren, in der die Dinge in einer Vorwärtsschleife (Inkrementierungsschleife) geschehen. Aufgrund der Funktionsweise des Konstrukts for haben Sie es jedoch versäumt, die Reihenfolge der Zuweisung und der Aktualisierung der Schleifenvariablen umzukehren. Dies ist für eine echte Umkehrung der Reihenfolge der Operationen zwischen einer Rückwärts- und einer Vorwärtsschleife erforderlich.

Fünfter Teil - Bonus

Der Vollständigkeit halber erhalten Sie hier den Code, den Sie erhalten, wenn Sie rightIndex+1 An die Methode und nicht an rightIndex übergeben. Diese Änderung eliminiert alle +2 - Offsets, die ansonsten erforderlich sind, um über die Richtigkeit der Schleife nachzudenken.

method insert3(arr:array<int>, rightIndex:int, value:int) returns (index:int)
  modifies arr
  requires arr != null
  requires 1 <= rightIndex < arr.Length 
  ensures 0 <= index <= rightIndex
  ensures arr[..index] == old(arr[..index])
  ensures arr[index] == value 
  ensures arr[index+1..rightIndex+1] == old(arr[index..rightIndex])
  ensures arr[rightIndex+1..] == old(arr[rightIndex+1..])
{
    ghost var initialArr := arr[..];
    var j := rightIndex;
    while(j > 0 && arr[j-1] > value)
       decreases j
       invariant 0 <= j <= arr.Length
       invariant arr[rightIndex+1..] == initialArr[rightIndex+1..]
       invariant arr[j+1..rightIndex+1] == initialArr[j..rightIndex]
       invariant arr[..j] == initialArr[..j] 
    {
        j := j-1;
        arr[j + 1] := arr[j];
    }   
    arr[j] := value;
    return j;
}
2
flamingpenguin

Dies richtig einfach und fließend richtig zu machen, ist eine Frage der Erfahrung. Auch wenn die Sprache es Ihnen nicht erlaubt, es direkt auszudrücken, oder Sie einen komplexeren Fall verwenden, als das einfache integrierte Ding verarbeiten kann, was Sie denken ist eine höhere Ebene wie "Besuche jedes Element einmal in ehrwürdiger Reihenfolge" und der erfahrenere Codierer übersetzt dies sofort in die richtigen Details, weil er es so oft getan hat.

Selbst dann kann es in komplexeren Fällen leicht zu Fehlern kommen, da das, was Sie schreiben, normalerweise nicht die übliche Sache in Dosen ist. Mit moderneren Sprachen und Bibliotheken schreiben Sie nicht die einfache Sache, weil es ein vordefiniertes Konstrukt gibt oder dafür aufruft. In C++ lautet das moderne Mantra "Algorithmen verwenden, anstatt Code zu schreiben".

Um sicherzustellen, dass es richtig ist, insbesondere für diese Art von Dingen, müssen Sie sich die Randbedingungen ansehen. Verfolgen Sie den Code in Ihrem Kopf für die wenigen Fälle an den Rändern, an denen sich die Dinge ändern. Wenn das index == array-max, was geschieht? Wie wäre es mit max-1? Wenn der Code falsch abbiegt, befindet er sich an einer dieser Grenzen. Einige Schleifen müssen sich Sorgen machen, dass das erste oder letzte Element sowie das Schleifenkonstrukt die Grenzen richtig machen. z.B. wenn Sie sich auf a[I] und a[I-1] was passiert, wenn I der Minimalwert ist?

Schauen Sie sich auch Fälle an, in denen die Anzahl der (korrekten) Iterationen extrem ist: Wenn sich die Grenzen erfüllen und Sie 0 Iterationen haben, funktioniert das nur ohne Sonderfall? Und was ist mit nur einer Iteration, bei der die niedrigste Grenze gleichzeitig auch die höchste Grenze ist?

Untersuchen Sie die Edge-Fälle (beide Seiten jeder Edge), was Sie beim Schreiben der Schleife tun sollten und was Sie bei Codeüberprüfungen tun sollten.

2
JDługosz

Wenn ich das Problem richtig verstanden habe, ist Ihre Frage, wie Sie denken sollen, Schleifen vom ersten Versuch an richtig zu machen, und nicht, wie Sie sicherstellen, dass Ihre Schleife richtig ist (für die die Antwort wie in anderen Antworten erläutert getestet werden würde).

Was ich für einen guten Ansatz halte, ist das Schreiben der ersten Iteration ohne Schleife. Nachdem Sie dies getan haben, sollten Sie beachten, was zwischen den Iterationen geändert werden sollte.

Ist es eine Zahl wie eine 0 oder eine 1? Dann brauchen Sie höchstwahrscheinlich ein für und Bingo, Sie haben auch Ihren Start i. Überlegen Sie dann, wie oft Sie dasselbe ausführen möchten, und Sie haben auch Ihre Endbedingung.

Wenn Sie nicht genau wissen, wie oft es ausgeführt wird, brauchen Sie kein für, sondern eine Weile oder eine Weile.

Technisch gesehen kann jede Schleife in jede andere Schleife übersetzt werden. Der Code ist jedoch leichter zu lesen, wenn Sie die richtige Schleife verwenden. Hier einige Tipps:

  1. Wenn Sie ein if () {...; break;} in ein for schreiben, brauchen Sie eine Weile und haben bereits die Bedingung

  2. "While" ist vielleicht die am häufigsten verwendete Schleife in einer Sprache, sollte aber nicht imo sein. Wenn Sie feststellen, dass Sie bool schreiben ok = True; while (check) {mach etwas und ändere hoffentlich irgendwann ok}; Dann brauchen Sie keine Weile, sondern eine Weile, denn das bedeutet, dass Sie alles haben, was Sie zum Ausführen der ersten Iteration benötigen.

Jetzt ein bisschen Kontext ... Als ich das Programmieren lernte (Pascal), sprach ich kein Englisch. Für mich ergab "für" und "während" nicht viel Sinn, aber das Schlüsselwort "wiederholen" (in C ausführen) ist in meiner Muttersprache fast dasselbe, sodass ich es für alles verwenden würde. Meiner Meinung nach ist das Wiederholen (do while) die natürlichste Schleife, da Sie fast immer möchten, dass etwas getan wird, und dann möchten Sie, dass es immer wieder ausgeführt wird, bis ein Ziel erreicht ist. "Für" ist nur eine Verknüpfung, die Ihnen einen Iterator gibt und die Bedingung seltsamerweise am Anfang des Codes platziert, obwohl Sie fast immer möchten, dass etwas getan wird, bis etwas passiert. Außerdem ist while nur eine Verknüpfung für if () {do while ()}. Verknüpfungen sind für später gut, aber der einfachste Weg, die Schleifen vom ersten Versuch an richtig zu machen, besteht darin, sie so zu schreiben, wie Ihr Gehirn denkt. Dies ist "Wiederholen (etwas tun) bis (während) (Sie haben) zu stoppen) "und verwenden Sie dann gegebenenfalls die entsprechende Verknüpfung.

2
Andrei

Insbesondere Umkehrschleifen können schwierig zu begründen sein, da viele unserer Programmiersprachen sowohl in der gemeinsamen For-Loop-Syntax als auch durch die Verwendung von auf Null basierenden halboffenen Intervallen auf Vorwärtsiteration ausgerichtet sind. Ich sage nicht, dass es falsch ist, dass die Sprachen diese Entscheidungen getroffen haben; Ich sage nur, dass diese Entscheidungen das Nachdenken über Rückwärtsschleifen erschweren.

Denken Sie im Allgemeinen daran, dass eine for-Schleife nur syntaktischer Zucker ist, der um eine while-Schleife herum aufgebaut ist:

// pseudo-code!
for (init; cond; step) { body; }

ist äquivalent zu:

// pseudo-code!
init;
while (cond) {
  body;
  step;
}

(Möglicherweise mit einer zusätzlichen Ebene des Gültigkeitsbereichs, um die im Init-Schritt deklarierten Variablen lokal für die Schleife zu halten).

Dies ist für viele Arten von Schleifen in Ordnung, aber es ist umständlich, wenn Sie den Schritt als letztes ausführen, wenn Sie rückwärts gehen. Wenn ich rückwärts arbeite, fällt es mir leichter, den Schleifenindex mit dem Wert after zu beginnen, den ich möchte, und den Teil step wie folgt an den Anfang der Schleife zu verschieben:

auto i = v.size();  // init
while (i > 0) {  // simpler condition because i is one after
    --i;  // step before the body
    body;  // in body, i means what you'd expect
}

oder als for-Schleife:

for (i = v.size(); i > 0; ) {
    --i;  // step
    body;
}

Dies kann nervig erscheinen, da sich der Schrittausdruck eher im Körper als in der Kopfzeile befindet. Dies ist ein unglücklicher Nebeneffekt der inhärenten Forward-Bias-for-Loop-Syntax. Aus diesem Grund werden einige argumentieren, dass Sie stattdessen Folgendes tun:

for (i = v.size() - 1; i >= 0; --i) {
    body;
}

Dies ist jedoch eine Katastrophe, wenn Ihre Indexvariable ein Typ ohne Vorzeichen ist (wie in C oder C++).

In diesem Sinne schreiben wir Ihre Einfügefunktion.

  1. Da wir rückwärts arbeiten, lassen wir den Schleifenindex der Eintrag nach dem "aktuellen" Array-Slot sein. Ich würde die Funktion so gestalten, dass das Größe der Ganzzahl anstelle eines Index zum letzten Element verwendet wird, da halboffene Bereiche die natürliche Art sind, Bereiche in den meisten Programmiersprachen darzustellen, und weil sie uns einen Weg geben um ein leeres Array darzustellen, ohne auf einen magischen Wert wie -1 zurückzugreifen.

    function insert(array, size, value) {
      var j = size;
    
  2. Während der neue Wert kleiner als das Element vorheriges ist, verschieben Sie sich weiter. Natürlich kann das vorherige Element nur überprüft werden, wenn is ein vorheriges Element ist, also müssen wir zuerst überprüfen, ob wir nicht ganz am Anfang stehen:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Dies lässt j genau dort, wo wir den neuen Wert wollen.

      array[j] = value; 
    };
    

Programming Pearls von Jon Bentley gibt eine sehr klare Erklärung der Einfügungssortierung (und anderer Algorithmen), die Ihnen helfen können, Ihre mentalen Modelle für diese Art von Problemen zu erstellen.

1
Adrian McCarthy

Ich werde versuchen, mich von den bereits erwähnten Themen in Hülle und Fülle fernzuhalten.

Was sind Werkzeuge/mentale Modelle, um solche Fehler zu vermeiden?

Werkzeuge

Für mich ist das größte Werkzeug, um bessere for und while Schleifen zu schreiben, überhaupt keine for oder while Schleifen zu schreiben.

Die meisten modernen Sprachen versuchen, dieses Problem auf die eine oder andere Weise anzugehen. Zum Beispiel führte Java, obwohl es von Anfang an Iterator hatte, was früher etwas umständlich zu verwenden war, eine Verknüpfungssyntax ein, um sie in einer neueren Version einfacher zu verwenden. C # hat sie auch usw.

Meine derzeit bevorzugte Sprache, Ruby, hat den funktionalen Ansatz (.each, .map Usw.) von vornherein übernommen. Dies ist sehr mächtig. Ich habe gerade eine kurze Zählung in einigen Ruby Code-Basis, an der ich arbeite: In ungefähr 10.000 Codezeilen gibt es null for und ungefähr 5 while.

Wenn ich gezwungen wäre, eine neue Sprache auszuwählen, würde die Suche nach solchen funktionalen/datenbasierten Schleifen ganz oben auf der Prioritätenliste stehen.

Mentale Modelle

Denken Sie daran, dass while das geringste Minimum an Abstraktion ist, das Sie erhalten können, nur einen Schritt über goto. Meiner Meinung nach macht for es noch schlimmer statt besser, da es alle drei Teile der Schleife eng zusammenhält.

Wenn ich mich also in einer Umgebung befinde, in der for verwendet wird, stelle ich verdammt sicher, dass alle drei Teile absolut einfach und immer gleich sind. Das heißt, ich werde schreiben

limit = ...;
for (idx = 0; idx < limit; idx++) { 

Aber nichts sehr komplexeres. Vielleicht habe ich manchmal einen Countdown, aber ich werde mein Bestes geben, um dies zu vermeiden.

Wenn ich while verwende, halte ich mich von verschlungenen inneren Spielereien fern, die die Schleifenbedingung betreffen. Der Test innerhalb der while(...) wird so einfach wie möglich sein und ich werde break so gut ich kann vermeiden. Außerdem ist die Schleife kurz (Codezeilen zählen) und größere Codemengen werden herausgerechnet.

Insbesondere wenn die tatsächliche while-Bedingung komplex ist, werde ich eine "Bedingungsvariable" verwenden, die sehr leicht zu erkennen ist, und die Bedingung nicht in die Anweisung while selbst einfügen:

repeat = true;
while (repeat) {
   repeat = false; 
   ...
   if (complex stuff...) {
      repeat = true;
      ... other complex stuff ...
   }
}

(Oder so ähnlich, natürlich im richtigen Maße.)

Dies gibt Ihnen ein sehr einfaches mentales Modell: "Diese Variable läuft monoton von 0 bis 10" oder "Diese Schleife läuft, bis diese Variable falsch/wahr ist". Die meisten Gehirne scheinen in der Lage zu sein, mit dieser Abstraktionsebene gut umzugehen.

Ich hoffe, das hilft.

1
AnoE

Sind Sie einfach verwirrt darüber, was eine for - Schleife tatsächlich tut und wie sie funktioniert?

for(initialization; condition; increment*)
{
    body
}
  1. Zunächst wird die Initialisierung ausgeführt
  2. Dann wird der Zustand überprüft
  3. Wenn die Bedingung erfüllt ist, wird der Körper einmal ausgeführt. Wenn nicht, gehe zu # 6
  4. Der Inkrementcode wird ausgeführt
  5. Gehe zu # 2
  6. Ende der Schleife

Es kann nützlich sein, diesen Code mit einem anderen Konstrukt neu zu schreiben. Hier ist das gleiche mit einer while-Schleife:

initialization
while(condition)
{
    body
    increment
}

Hier sind einige andere Vorschläge:

  • Können Sie ein anderes Sprachkonstrukt wie eine foreach-Schleife verwenden? Dies kümmert sich um den Zustand und den Inkrementierungsschritt für Sie.
  • Können Sie eine Karten- oder Filterfunktion verwenden? Einige Sprachen haben Funktionen mit diesen Namen, die intern eine Sammlung für Sie durchlaufen. Sie liefern nur die Sammlung und den Körper.
  • Sie sollten wirklich mehr Zeit damit verbringen, sich mit for Schleifen vertraut zu machen. Sie werden sie die ganze Zeit verwenden. Ich schlage vor, dass Sie in einem Debugger eine for-Schleife durchlaufen, um zu sehen, wie sie ausgeführt wird.

* Beachten Sie, dass ich zwar den Begriff "Inkrement" verwende, es sich jedoch nur um einen Code handelt, der nach dem Body und vor der Bedingungsprüfung steht. Es kann eine Abnahme oder gar nichts sein.

0
user2023861

Versuch zusätzlicher Einsicht

Für nicht triviale Algorithmen mit Schleifen können Sie die folgende Methode ausprobieren:

  1. Erstellen Sie ein festes Array mit 4 Positionen und geben Sie einige Werte ein, m zu simulieren das Problem;
  2. Schreiben Sie Ihren Algorithmus auf lösen Sie das gegebene Problem, ohne Schleife und mit fest codierten Indexierungen;
  3. Danach ersetzen Sie fest codierte Indizierungen in Ihrem Code durch eine Variable i oder j und erhöhen/dekrementieren Sie diese Variablen nach Bedarf (jedoch immer noch ohne Schleife). ;;
  4. Schreiben Sie Ihren Code neu und setzen Sie die sich wiederholenden Blöcke in eine Schleife, wobei die Vor- und Nachbedingungen erfüllt sind;
  5. [ optional ] Schreibe deine Schleife so um, dass sie die gewünschte Form hat (für/while/do while);
  6. Das Wichtigste ist, dass Sie Ihren Algorithmus korrekt schreiben. Danach überarbeiten und optimieren Sie Ihren Code/Ihre Schleifen, falls erforderlich. (dies kann jedoch dazu führen, dass der Code für einen Leser nicht mehr trivial ist.)

Dein Problem

//TODO: Insert the given value in proper position in the sorted subarray
function insert(array, rightIndex, value) { ... };

Schreibe den Körper der Schleife mehrmals manuell

Verwenden wir ein festes Array mit 4 Positionen und versuchen, den Algorithmus manuell ohne Schleifen zu schreiben:

           //0 1 2 3
var array = [2,5,9,1]; //array sorted from index 0 to 2
var leftIndex = 0;
var rightIndex = 2;
var value = array[3]; //placing the last value within the array in the proper position

//starting here as 2 == rightIndex

if (array[2] > value) {
    array[3] = array[2];
} else {
    array[3] = value;
    return; //found proper position, no need to proceed;
}

if (array[1] > value) {
    array[2] = array[1];
} else {
    array[2] = value;
    return; //found proper position, no need to proceed;
}

if (array[0] > value) {
    array[1] = array[0];
} else {
    array[1] = value;
    return; //found proper position, no need to proceed;
}

array[0] = value; //achieved beginning of the array

//stopping here because there 0 == leftIndex

mschreiben, hartcodierte Werte entfernen

//consider going from 2 to 0, going from "rightIndex" to "leftIndex"

var i = rightIndex //starting here as 2 == rightIndex

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

if (array[i] > value) {
    array[i+1] = array[i];
} else {
    array[i+1] = value;
    return; //found proper position, no need to proceed;
}

i--;
if (i < leftIndex) {
    array[i+1] = value; //achieved beginning of the array
    return;
}

//stopping here because there 0 == leftIndex

In eine Schleife übersetzen

Mit while:

var i = rightIndex; //starting in rightIndex

while (true) {
    if (array[i] > value) { //refactor: this can go in the while clause
        array[i+1] = array[i];
    } else {
        array[i+1] = value;
        break; //found proper position, no need to proceed;
    }

    i--;
    if (i < leftIndex) { //refactor: this can go (inverted) in the while clause
        array[i+1] = value; //achieved beginning of the array
        break;
    }
}

Refactor/rewrite/optimiere die Schleife wie du willst :

Mit while:

var i = rightIndex; //starting in rightIndex

while ((array[i] > value) && (i >= leftIndex)) {
    array[i+1] = array[i];
    i--;
}

array[i+1] = value; //found proper position, or achieved beginning of the array

mit for:

for (var i = rightIndex; (array[i] > value) && (i >= leftIndex); i--) {
    array[i+1] = array[i];
}

array[i+1] = value; //found proper position, or achieved beginning of the array

PS : Code setzt voraus, dass die Eingabe gültig ist und dieses Array keine Wiederholungen enthält.

0
Emerson Cardoso