Come scrivere loop corretti?


65

La maggior parte delle volte durante la scrittura di loop di solito scrivo condizioni al contorno errate (ad esempio: esito errato) o le mie ipotesi sulle terminazioni del loop sono errate (ad esempio: loop a funzionamento continuo). Anche se ho avuto i miei presupposti corretti dopo qualche prova ed errore, ma sono stato troppo frustrato a causa della mancanza di un modello di calcolo corretto nella mia testa.

/**
 * 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; 
};

Gli errori che ho fatto inizialmente sono stati:

  1. Invece di j> = 0 l'ho tenuto j> 0.
  2. Confuso se array [j + 1] = valore o array [j] = valore.

Quali sono gli strumenti / modelli mentali per evitare tali errori?


6
In quali circostanze ritieni che j >= 0sia un errore? Sarei più cauto del fatto che tu stia accedendo array[j]e array[j + 1]senza prima verificarlo array.length > (j + 1).
Ben Cottrell,

5
simile a quello che ha detto @LightnessRacesinOrbit, probabilmente stai risolvendo problemi che sono già stati risolti. In generale, qualsiasi looping che è necessario eseguire su una struttura di dati esiste già in alcuni moduli o classi core ( Array.prototypenell'esempio di JS). Ciò impedisce di incontrare condizioni limite poiché qualcosa di simile mapfunziona su tutti gli array. Puoi risolvere quanto sopra usando slice e concat per evitare il looping tutti insieme: codepen.io/anon/pen/ZWovdg?editors=0012 Il modo più corretto per scrivere un loop è non scriverne affatto uno.
Jed Schneider,

13
In realtà, vai avanti e risolvi i problemi risolti. Si chiama pratica. Non preoccuparti di pubblicarli. Cioè, a meno che non trovi un modo per migliorare le soluzioni. Detto questo, reinventare la ruota coinvolge più della ruota. Implica un sistema di controllo della qualità dell'intera ruota e l'assistenza clienti. Tuttavia, i cerchi personalizzati sono belli.
candied_orange,

53
Temo che stiamo andando nella direzione sbagliata qui. Dare CodeYogi merda perché il suo esempio fa parte di un noto algoritmo è piuttosto infondato. Non ha mai affermato di aver inventato qualcosa di nuovo. Sta chiedendo come evitare alcuni errori al contorno molto comuni quando si scrive un ciclo. Le biblioteche hanno fatto molta strada, ma vedo ancora un futuro per le persone che sanno scrivere i loop.
candied_orange,

5
In generale, quando si ha a che fare con loop e indici, si dovrebbe imparare che gli indici puntano tra gli elementi e si familiarizzano con intervalli semiaperti (in realtà, sono due facce degli stessi concetti). Una volta ottenuti questi fatti, molti dei loop / indici che graffiano la testa scompaiono completamente.
Matteo Italia,

Risposte:


208

Test

No, sul serio, prova.

Ho programmato per oltre 20 anni e ancora non mi fido di me stesso di scrivere un ciclo correttamente la prima volta. Scrivo ed eseguo test che dimostrano che funziona prima di sospettare che funzioni. Prova ogni lato di ogni condizione al contorno. Ad esempio uno rightIndexdi 0 dovrebbe fare cosa? Che ne dici di -1?

Mantienilo semplice

Se gli altri non riescono a vedere cosa fa a colpo d'occhio, lo stai rendendo troppo difficile. Non esitate a ignorare le prestazioni se ciò significa che è possibile scrivere qualcosa di facile da capire. Renderlo più veloce solo nell'improbabile evento in cui è davvero necessario. E anche allora solo una volta che sei assolutamente sicuro di sapere esattamente cosa ti sta rallentando. Se riesci a ottenere un effettivo miglioramento di Big O , questa attività potrebbe non essere inutile, ma anche in questo caso, rendi il tuo codice il più leggibile possibile.

Off da uno

Conosci la differenza tra contare le dita e contare gli spazi tra le dita. A volte gli spazi sono ciò che è realmente importante. Non lasciare che le dita ti distraggano. Scopri se il pollice è un dito. Scopri se il divario tra il mignolo e il pollice conta come uno spazio.

Commenti

Prima di perderti nel codice prova a dire cosa intendi in inglese. Dichiara chiaramente le tue aspettative. Non spiegare come funziona il codice. Spiega perché lo stai facendo fare quello che fa. Tieni fuori i dettagli dell'implementazione. Dovrebbe essere possibile riformattare il codice senza dover cambiare il commento.

Il miglior commento è un buon nome.

Se riesci a dire tutto ciò che devi dire con un buon nome, NON ripeterlo con un commento.

astrazioni

Gli oggetti, le funzioni, le matrici e le variabili sono tutte astrazioni valide solo come i nomi che vengono loro assegnati. Dai loro nomi che assicurino che quando le persone guardano dentro di loro non saranno sorpresi da ciò che trovano.

Nomi brevi

Usa nomi brevi per cose di breve durata. iè un bel nome per un indice in un bel loop stretto in un piccolo ambito che rende ovvio il suo significato. Se la ivita è abbastanza lunga da diffondersi riga dopo riga con altre idee e nomi che possono essere confusi, iallora è il momento di dare iun bel nome esplicativo lungo.

Nomi lunghi

Non abbreviare mai un nome semplicemente a causa di considerazioni sulla lunghezza della linea. Trova un altro modo per disporre il tuo codice.

Lo spazio bianco

I difetti amano nascondersi nel codice illeggibile. Se la tua lingua ti consente di scegliere il tuo stile di rientro almeno essere coerente. Non rendere il tuo codice simile a un flusso di rumore. Il codice dovrebbe apparire come in marcia in formazione.

Costrutti Loop

Scopri e rivedi le strutture del loop nella tua lingua. Guardare un debugger evidenziare un for(;;)loop può essere molto istruttivo. Scopri tutti i moduli. while, do while, while(true), for each. Usa quello più semplice con cui puoi cavartela. Cercare l' adescamento della pompa . Scopri cosa breake continuefai se li hai. Conosci la differenza tra c++e ++c. Non aver paura di tornare presto finché chiudi sempre tutto ciò che deve essere chiuso. Infine, blocca o preferibilmente qualcosa che lo contrassegna per la chiusura automatica quando lo apri: usando statement / Try with Resources .

Alternative ad anello

Lascia che qualcos'altro faccia il loop se puoi. È più facile per gli occhi e già eseguito il debug. Questi sono disponibili in molte forme: collezioni o torrenti che permetterà map(), reduce(), foreach(), e altri tali metodi che si applicano un lambda. Cerca funzioni speciali come Arrays.fill(). C'è anche una ricorsione, ma ci si aspetta che ciò renda le cose facili in casi speciali. Generalmente non usare la ricorsione fino a quando non vedi come sarebbe l'alternativa.

Oh, e prova.

Test, test, test.

Ho già parlato di test?

C'era un'altra cosa. Non ricordo. Iniziato con un T ...


36
Buona risposta, ma forse dovresti menzionare i test. Come si fa a gestire il loop infinito nel test unitario? Tale ciclo non "interrompe" i test ???
GameAlchemist,

139
@GameAlchemist Questo è il test della pizza. Se il mio codice non si interrompe nel tempo che mi serve per fare una pizza, inizio a sospettare che qualcosa non vada. Certo non è una cura per il problema di arresto di Alan Turing ma almeno ottengo una pizza fuori dall'affare.
candied_orange,

12
@CodeYogi - in realtà, può avvicinarsi molto . Inizia con un test che opera su un singolo valore. Implementa il codice senza un ciclo. Quindi scrivere un test che opera su due valori. Implementa il ciclo. È molto improbabile che si verifichi una condizione al contorno errata nel loop se lo si fa in questo modo, perché in quasi tutte le circostanze l'uno o l'altro di questi due test fallirà se si commette un errore del genere.
Jules,

15
@CodeYogi Amico, tutto merito a TDD ma Test >> TDD. Emettere un valore può essere test, ottenere un secondo set di occhi sul tuo codice sta testando (puoi formalizzare questo come una revisione del codice ma spesso afferro qualcuno per una conversazione di 5 minuti). Un test è ogni possibilità che tu esprima la tua intenzione di fallire. Diavolo puoi testare il tuo codice parlando attraverso un'idea con tua madre. Ho trovato dei bug nel mio codice quando fissavo la piastrella sotto la doccia. TDD è un'efficace disciplina formalizzata che non trovi in ​​tutti i negozi. Non ho mai codificato una volta da nessuna parte dove le persone non hanno testato.
candied_orange,

12
Ho programmato e testato anni e anni prima di aver mai sentito parlare di TDD. È solo ora che realizzo la correlazione di quegli anni con gli anni trascorsi a scrivere codice senza indossare pantaloni.
candied_orange,

72

Durante la programmazione è utile pensare a:

e nell'esplorazione di territori inesplorati (come la giocoleria con gli indici) può essere molto, molto , utile non solo pensare a quelli ma renderli espliciti nel codice con affermazioni .

Prendiamo il tuo codice originale:

/**
 * 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; 
};

E controlla cosa abbiamo:

  • pre-condizione: array[0..rightIndex]è ordinata
  • post-condition: array[0..rightIndex+1]è ordinato
  • invariante: 0 <= j <= rightIndexma sembra un po 'ridondante; o come ha osservato @Jules nei commenti, al termine di un "giro", for n in [j, rightIndex+1] => array[j] > value.
  • invariante: alla fine di un "round", array[0..rightIndex+1]viene ordinato

Quindi puoi prima scrivere una is_sortedfunzione e una minfunzione che lavora su una porzione di array, quindi asserire:

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]));
};

C'è anche il fatto che la condizione del tuo loop è un po 'complicata; potresti voler renderti più semplice dividendo le cose:

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]));
};

Ora, il ciclo è semplice ( jva da rightIndexa 0).

Infine, ora questo deve essere testato:

  • pensare alle condizioni al contorno ( rightIndex == 0, rightIndex == array.size - 2)
  • pensa di valueessere più piccolo array[0]o più grande diarray[rightIndex]
  • pensare valueessendo uguale a array[0], array[rightIndex]o qualche indice medio

Inoltre, non sottovalutare il fuzzing . Sono presenti affermazioni per rilevare errori, quindi genera un array casuale e ordinalo usando il tuo metodo. Se viene generata un'affermazione, hai trovato un bug e puoi estendere la tua suite di test.


8
@CodeYogi: con ... test. Il fatto è che può essere poco pratico esprimere tutto nelle asserzioni: se l'asserzione semplicemente ripete il codice, allora non porta nulla di nuovo (la ripetizione non aiuta con la qualità). Questo è il motivo per cui qui non ho affermato nel ciclo che , 0 <= j <= rightIndexo array[j] <= value, sarebbe semplicemente ripetere il codice. D'altra parte, is_sortedporta una nuova garanzia, quindi è preziosa. Successivamente, ecco a cosa servono i test. Se chiami insert([0, 1, 2], 2, 3)per la tua funzione e l'output non lo [0, 1, 2, 3]è, hai trovato un bug.
Matthieu M.,

3
@MatthieuM. non scartare il valore di un'asserzione semplicemente perché duplica il codice. Infatti, quelle possono essere affermazioni molto preziose da avere se decidi di riscrivere il codice. I test hanno tutto il diritto di essere duplicativi. Piuttosto considera se l'asserzione è così accoppiata a una singola implementazione di codice che qualsiasi riscrittura invaliderebbe l'asserzione. Questo è quando stai sprecando il tuo tempo. Bella risposta a proposito.
candied_orange,

1
@CandiedOrange: duplicando il codice intendo letteralmente array[j+1] = array[j]; assert(array[j+1] == array[j]);. In questo caso, il valore sembra molto basso (è una copia / incolla). Se duplichi il significato ma lo esprimi in un altro modo, diventa più prezioso.
Matthieu M.,

10
Le regole di Hoare: aiutare a scrivere loop corretti dal 1969. "Tuttavia, anche se queste tecniche sono note da più di un decennio, la maggior parte dei porgrammers non ne ha mai sentito parlare".
Joker_vD,

1
@MatthieuM. Sono d'accordo che ha un valore molto basso. Ma non penso che sia causato da una copia / incolla. Supponiamo che volessi effettuare il refactoring in insert()modo che funzionasse copiando da un vecchio array a un nuovo array. Questo può essere fatto senza rompere gli altri assert. Ma non quest'ultimo. Mostra solo quanto bene assertsono stati progettati gli altri.
candied_orange,

29

Usa test unitario / TDD

Se hai davvero bisogno di accedere alle sequenze attraverso un forciclo, puoi evitare gli errori attraverso i test unitari e in particolare lo sviluppo guidato dai test .

Immagina di dover implementare un metodo che accetta i valori superiori a zero, in ordine inverso. A quali casi di test potresti pensare?

  1. Una sequenza contiene un valore superiore a zero.

    Actual: [5]. Previsto: [5].

    L'implementazione più semplice che soddisfa i requisiti consiste semplicemente nel restituire la sequenza di origine al chiamante.

  2. Una sequenza contiene due valori, entrambi superiori a zero.

    Actual: [5, 7]. Previsto: [7, 5].

    Ora, non puoi semplicemente restituire la sequenza, ma dovresti invertirla. Utilizzeresti un for (;;)ciclo, un costrutto in un'altra lingua o un metodo di libreria non importa.

  3. Una sequenza contiene tre valori, uno dei quali è zero.

    Actual: [5, 0, 7]. Previsto: [7, 5].

    Ora dovresti cambiare il codice per filtrare i valori. Ancora una volta, questo potrebbe essere espresso attraverso ifun'istruzione o una chiamata al tuo metodo framework preferito.

  4. A seconda dell'algoritmo (poiché si tratta di test in white box, l'implementazione è importante), potrebbe essere necessario gestire in modo specifico il [] → []caso sequenza vuota , o forse no. Oppure si può garantire che il caso bordo dove tutti i valori sono negativi [-4, 0, -5, 0] → []viene gestita correttamente, o addirittura che i valori negativi al contorno sono: [6, 4, -1] → [4, 6]; [-1, 6, 4] → [4, 6]. In molti casi, tuttavia, avrai solo i tre test sopra descritti: qualsiasi test aggiuntivo non ti farà cambiare il codice e quindi sarebbe irrilevante.

Lavora a un livello di astrazione più elevato

Tuttavia, in molti casi, è possibile evitare la maggior parte di tali errori lavorando a un livello di astrazione più elevato, utilizzando librerie / framework esistenti. Tali librerie / framework consentono di ripristinare, ordinare, dividere e unire le sequenze, inserire o rimuovere valori in array o elenchi doppiamente collegati, ecc.

Di solito, foreachpuò essere usato al posto di for, rendendo irrilevanti le condizioni al contorno: la lingua lo fa per te. Alcuni linguaggi, come Python, non hanno nemmeno il for (;;)costrutto, ma solo for ... in ....

In C #, LINQ è particolarmente conveniente quando si lavora con sequenze.

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

è molto più leggibile e meno soggetto a errori rispetto alla sua forvariante:

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

    yield return value;
}

3
Bene, dalla tua domanda originale, ho l'impressione che la scelta sia da un lato utilizzare TDD e ottenere la soluzione corretta, dall'altro saltare la parte di test e sbagliare le condizioni al contorno.
Arseni Mourzenko,

18
Grazie per essere quella di menzionare l'elefante nella stanza: non usare i loop a tutti . Il motivo per cui la gente continua a scrivere codice come il 1985 (e sono generoso) è al di là di me. BOCTAOE.
Jared Smith,

4
@JaredSmith Una volta che il computer esegue effettivamente quel codice, quanto vuoi scommettere non ci sono istruzioni di salto lì dentro? Usando LINQ stai astrarre il ciclo, ma è ancora lì. Ho spiegato questo ai colleghi che non hanno saputo del duro lavoro del pittore Shlemiel . Non riuscire a capire dove si verificano i loop, anche se sono astratti nel codice e il codice è significativamente più leggibile di conseguenza, quasi invariabilmente porta a problemi di prestazioni che si sarà in perdita per spiegare, per non parlare della correzione.
un CVn

6
@ MichaelKjörling: quando usi LINQ, il loop è lì, ma un for(;;)costrutto non sarebbe molto descrittivo di questo loop . Un aspetto importante è che LINQ (così come la comprensione dell'elenco in Python e elementi simili in altre lingue) rende le condizioni al contorno per lo più irrilevanti, almeno nell'ambito della domanda originale. Tuttavia, non posso essere più d'accordo sulla necessità di capire cosa succede sotto il cofano quando si utilizza LINQ, soprattutto quando si tratta di valutazione pigra.
Arseni Mourzenko,

4
@ MichaelKjörling Non stavo necessariamente parlando di LINQ e non riesco a capire il tuo punto. forEach, map, LazyIterator, Ecc sono forniti dall'ambiente compilatore o runtime del linguaggio e sono probabilmente meno probabilità di essere di nuovo a camminare il secchio di vernice su ogni iterazione. Ciò, la leggibilità e gli errori off-by-one sono i due motivi per cui tali funzionalità sono state aggiunte alle lingue moderne.
Jared Smith,

15

Sono d'accordo con altre persone che dicono di testare il tuo codice. Tuttavia, è anche bello farlo bene in primo luogo. Ho la tendenza a sbagliare le condizioni al contorno in molti casi, quindi ho sviluppato trucchi mentali per prevenire tali problemi.

Con un array con indice 0, le tue condizioni normali saranno:

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

o

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

Tali schemi dovrebbero diventare una seconda natura, non dovresti assolutamente pensarci.

Ma non tutto segue esattamente quel modello. Quindi se non sei sicuro di averlo scritto bene, ecco il tuo prossimo passo:

Inserisci i valori e valuta il codice nel tuo cervello. Rendi il più semplice possibile pensarci. Cosa succede se i valori rilevanti sono 0s? Cosa succede se sono 1?

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

Nel tuo esempio, non sei sicuro che dovrebbe essere [j] = valore o [j ​​+ 1] = valore. È ora di iniziare a valutarlo manualmente:

Cosa succede se la lunghezza dell'array è 0? La risposta diventa ovvia: rightIndex deve essere (lunghezza - 1) == -1, quindi j inizia da -1, quindi per inserire nell'indice 0, devi aggiungere 1.

Quindi abbiamo dimostrato la condizione finale corretta, ma non l'interno del loop.

Cosa succede se si dispone di un array con 1 elemento, 10 e si tenta di inserire un 5? Con un singolo elemento, rightIndex dovrebbe iniziare da 0. Quindi la prima volta attraverso il ciclo, j = 0, quindi "0> = 0 && 10> 5". Dato che vogliamo inserire il 5 nell'indice 0, il 10 dovrebbe essere spostato nell'indice 1, quindi array [1] = array [0]. Poiché ciò accade quando j è 0, array [j + 1] = array [j + 0].

Se provi a immaginare un array di grandi dimensioni e ciò che accade inserendo in una posizione arbitraria, il tuo cervello probabilmente sarà sopraffatto. Ma se ti attieni a semplici esempi di dimensione 0/1/2, dovrebbe essere facile fare una rapida corsa mentale e vedere dove si rompono le tue condizioni al contorno.

Immagina di non aver mai sentito parlare del problema del palo di recinzione prima e ti dico che ho 100 paletti di recinzione in linea retta, quanti segmenti tra di loro. Se provi a immaginare 100 paletti di recinzione nella tua testa, sarai solo sopraffatto. Quindi qual è il minor numero di pali per realizzare un recinto valido? Hai bisogno di 2 per creare una recinzione, quindi immagina 2 post e l'immagine mentale di un singolo segmento tra i post lo rende molto chiaro. Non devi sederti lì a contare post e segmenti perché hai trasformato il problema in qualcosa di intuitivamente ovvio per il tuo cervello.

Una volta che ritieni che sia corretto, è bene eseguirlo attraverso un test e assicurarti che il computer faccia quello che pensi che dovrebbe, ma a quel punto dovrebbe essere solo una formalità.


4
Mi piace molto for (int i = 0; i < length; i++). Una volta preso l'abitudine, ho smesso di usarlo <=quasi altrettanto spesso e ho sentito che i loop sono diventati più semplici. Ma for (int i = length - 1; i >= 0; i--)sembra eccessivamente complicato, rispetto a: for (int i=length; i--; )(che probabilmente sarebbe ancora più sensato scrivere in un whileciclo a meno che non stavo cercando di iavere un ambito / una vita più piccolo). Il risultato scorre ancora attraverso il ciclo con i == lunghezza-1 (inizialmente) fino a i == 0, con l'unica differenza funzionale che la while()versione termina con i == -1 dopo il ciclo (se rimane attivo), anziché i = = 0.
TOOGAM,

2
@TOOGAM (int i = length; i--;) funziona in C / C ++ perché 0 viene valutato come falso, ma non tutte le lingue hanno questa equivalenza. Immagino che potresti dire che ...> 0
Bryce Wagner,

Naturalmente, se si utilizza una lingua che richiede " > 0" per ottenere la funzionalità desiderata, è necessario utilizzare tali caratteri perché richiesti da quella lingua. Tuttavia, anche in quei casi, usare solo il " > 0" è più semplice che fare il processo in due parti di sottrarre prima uno e poi usare anche " >= 0". Una volta appreso che attraverso un po 'di esperienza, ho preso l'abitudine di dover usare il segno di uguaglianza (ad es. " >= 0") Nelle mie condizioni di test del ciclo molto meno frequentemente, e da allora il codice risultante è diventato più semplice.
TOOGAM,

1
@BryceWagner se avete bisogno di fare i-- > 0, perché non provare la battuta classica, i --> 0!
porglezomp,

3
@porglezomp Ah, sì, l' operatore va . La maggior parte dei linguaggi di tipo C, inclusi C, C ++, Java e C #, ha quello.
un CVn il

11

Ero troppo frustrato a causa della mancanza di un modello di calcolo corretto nella mia testa.

È un punto molto interessante a questa domanda e ha generato questo commento: -

C'è solo un modo: capire meglio il tuo problema. Ma è tanto generale quanto la tua domanda. - Thomas Junk

... e Thomas ha ragione. Non avere un chiaro intento per una funzione dovrebbe essere una bandiera rossa - una chiara indicazione che dovresti immediatamente FERMARTI, prendere carta e matita, allontanarti dall'IDE e risolvere correttamente il problema; o almeno controlla la sanità mentale di ciò che hai fatto.

Ho visto così tante funzioni e classi che sono diventate un casino completo perché gli autori hanno tentato di definire l'implementazione prima di aver definito completamente il problema. Ed è così facile da gestire.

Se non capisci completamente il problema, è improbabile che tu stia codificando la soluzione ottimale (in termini di efficienza o chiarezza), né sarai in grado di creare test di unità veramente utili in una metodologia TDD.

Prendi il tuo codice qui come esempio, contiene una serie di potenziali difetti che non hai ancora considerato ad esempio: -

  • cosa succede se rightIndex è troppo basso? (indizio: sarà comportare la perdita di dati)
  • cosa succede se rightIndex è fuori dai limiti dell'array? (otterrai un'eccezione o ti sei appena creato un buffer overflow?)

Ci sono alcuni altri problemi relativi alle prestazioni e alla progettazione del codice ...

  • questo codice dovrà essere ridimensionato? Mantenere l'array ordinato è l'opzione migliore o dovresti guardare altre opzioni (come un elenco collegato?)
  • puoi essere sicuro dei tuoi presupposti? (puoi garantire che l'array sia ordinato e cosa succede se non lo è?)
  • stai reinventando la ruota? Le matrici ordinate sono un problema ben noto, hai studiato le soluzioni esistenti? Esiste già una soluzione nella tua lingua (come SortedList<t>in C #)?
  • dovresti copiare manualmente una voce di array alla volta? o il tuo linguaggio fornisce funzioni comuni come quelle di JScript Array.Insert(...)? questo codice sarebbe più chiaro?

Esistono molti modi in cui questo codice può essere migliorato, ma fino a quando non hai definito correttamente ciò di cui hai bisogno per fare questo codice, non stai sviluppando il codice, lo stai solo hackerando nella speranza che funzioni. Investi il ​​tempo e la tua vita sarà più facile.


2
Anche se stai passando i tuoi indici a una funzione esistente (come Array.Copy), può comunque essere necessario pensare per ottenere le condizioni legate corrette. Immaginare cosa succede in situazioni di lunghezza 0 e 1 lunghezza e 2 lunghezza può essere il modo migliore per assicurarti di non copiare troppo pochi o troppi.
Bryce Wagner,

@BryceWagner - Assolutamente vero, ma senza una chiara idea di quale problema stai risolvendo, passerai molto tempo a ciondolarti nel buio in una strategia di "colpo e speranza" che è di gran lunga l'OP il problema più grande a questo punto.
James Snell,

2
@CodeYogi: hai e come sottolineato da altri hai suddiviso il problema in sotto-problemi piuttosto male, motivo per cui un numero di risposte menziona il tuo approccio alla risoluzione del problema come il modo per evitarlo. Non è qualcosa che dovresti prendere sul personale, solo l'esperienza di quelli di noi che sono stati lì.
James Snell,

2
@CodeYogi, penso che potresti aver confuso questo sito con StackTranslate.it. Questo sito equivale a una sessione di domande e risposte su una lavagna , non su un terminale di computer. "Mostrami il codice" è un'indicazione piuttosto esplicita che ti trovi sul sito sbagliato.
Carattere jolly

2
@Wildcard +1: "Mostrami il codice" è, per me, un eccellente indicatore del perché questa risposta è giusta e che forse ho bisogno di lavorare su modi per dimostrare meglio che si tratta di un problema fattore umano / design che può solo essere affrontato da cambiamenti nel processo umano - nessuna quantità di codice potrebbe insegnarlo.
James Snell,

10

Gli errori off-by-one sono uno degli errori di programmazione più comuni. Anche gli sviluppatori esperti a volte sbagliano. I linguaggi di livello superiore di solito hanno costrutti di iterazione simili foreacho mapche evitano del tutto l'indicizzazione esplicita. Ma a volte hai bisogno di indicizzazione esplicita, come nel tuo esempio.

La sfida è come pensare agli intervalli di celle array. Senza un chiaro modello mentale, diventa confuso quando includere o escludere i punti finali.

Nel descrivere gli intervalli di array, la convenzione prevede l' inclusione del limite inferiore, l'esclusione del limite superiore . Ad esempio l'intervallo 0..3 è le celle 0,1,2. Questa convention è usato in tutta lingue 0-indicizzati, ad esempio, il slice(start, end)metodo JavaScript restituisce il sottoarray partendo con indice startfino a , ma non incluso dell'indice end.

È più chiaro quando si pensa agli indici di intervallo come a descrivere i bordi tra le celle dell'array. L'illustrazione seguente è una matrice di lunghezza 9, ei numeri sotto le celle sono allineati ai bordi ed è ciò che viene utilizzato per descrivere i segmenti di matrice. Ad esempio, dall'illustrazione è chiaro che l'intervallo 2..5 è rappresentato dalle celle 2,3,4.

┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 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

Questo modello è coerente con la lunghezza della matrice come limite superiore di una matrice. Un array con lunghezza 5 ha celle 0..5, il che significa che ci sono le cinque celle 0,1,2,3,4. Ciò significa anche che la lunghezza di un segmento è il limite superiore meno il limite inferiore, ovvero il segmento 2..5 ha 5-2 = 3 celle.

Avere in mente questo modello quando si scorre verso l'alto o verso il basso rende molto più chiaro quando includere o escludere i punti finali. Quando si scorre verso l'alto, è necessario includere il punto iniziale ma escludere il punto finale. Quando si scorre verso il basso, è necessario escludere il punto iniziale (limite superiore) ma includere il punto finale (limite inferiore).

Dato che stai iterando verso il basso nel tuo codice, devi includere il limite inferiore, 0, quindi esegui iterate while j >= 0.

Detto questo, la scelta di fare in modo che l' rightIndexargomento rappresenti l'ultimo indice nel sottoarray interrompe la convenzione. Significa che devi includere entrambi gli endpoint (0 e rightIndex) nell'iterazione. Inoltre, è difficile rappresentare il segmento vuoto (necessario quando si inizia l'ordinamento). In realtà devi usare -1 come rightIndex quando inserisci il primo valore. Sembra abbastanza innaturale. Sembra più naturale aver rightIndexindicato l'indice dopo il segmento, quindi 0 rappresenta il segmento vuoto.

Naturalmente il tuo codice è molto più confuso perché espande il subarray ordinato con uno, sovrascrivendo l'elemento immediatamente dopo il subarray inizialmente ordinato. Quindi leggi dall'indice j ma scrive il valore in j + 1. Qui dovresti essere chiaro che j è la posizione nel subarray iniziale, prima dell'inserimento. Quando le operazioni sugli indici diventano troppo complicate, mi aiuta a tracciarlo su un foglio di carta griglia.


4
@CodeYogi: Disegnerei un piccolo array come una griglia su un pezzo di carta, e quindi passo attraverso un'iterazione del ciclo manualmente con una matita. Questo mi aiuta a chiarire cosa succede effettivamente, ad esempio che un intervallo di celle viene spostato a destra e dove viene inserito il nuovo valore.
Jacques B

3
"Ci sono due cose difficili nell'informatica: invalidazione della cache, denominazione delle cose ed errori off-by-one".
Trauma digitale,

1
@CodeYogi: aggiunto un piccolo diagramma per mostrare di cosa sto parlando.
Jacques B

1
Una grande intuizione specialmente degli ultimi due pars vale la pena leggere, la confusione è anche dovuta alla natura del ciclo for, anche se trovo l'indice giusto il loop decrementa una volta prima della fine e quindi mi fa un passo indietro.
CodeYogi,

1
Molto. Eccellente. Risposta. E aggiungerei che questa convenzione sull'indice inclusivo / esclusivo è anche motivata dal valore di myArray.Lengtho myList.Count- che è sempre uno in più dell'indice "più giusto" a base zero. ... La lunghezza della spiegazione smentisce l'applicazione pratica e semplice di queste euristiche di codifica esplicita. La folla di TL; DR si sta perdendo.
radarbob,

5

L'introduzione alla tua domanda mi fa pensare che non hai imparato a programmare correttamente. Chiunque stia programmando in un linguaggio imperativo per più di alcune settimane dovrebbe davvero ottenere i propri limiti di loop per la prima volta in oltre il 90% dei casi. Forse ti stai affrettando a iniziare a scrivere codice prima di aver riflettuto sufficientemente sul problema.

Ti suggerisco di correggere questa carenza apprendendo (ri) come scrivere i cicli - e consiglierei alcune ore di lavoro su una serie di cicli con carta e matita. Prenditi un pomeriggio libero per farlo. Quindi dedica circa 45 minuti al giorno a lavorare sull'argomento fino a quando non lo capisci davvero.

È tutto molto bene testato, ma dovresti testarlo nell'aspettativa che generalmente ottieni i limiti del tuo ciclo (e il resto del tuo codice) giusti.


4
Non sarei così assertivo riguardo alle capacità di OP. Fare errori di confine è facile, soprattutto in un contesto stressante come un colloquio di assunzione. Uno sviluppatore esperto potrebbe fare anche quegli errori, ma, ovviamente, uno sviluppatore esperto eviterebbe tali errori in primo luogo attraverso i test.
Arseni Mourzenko,

3
@MainMa - Penso che mentre Mark avrebbe potuto essere più sensibile, penso che abbia ragione - C'è lo stress delle interviste e c'è solo l'hacking del codice insieme senza la dovuta considerazione per definire il problema. Il modo in cui la domanda è formulata indica molto fortemente quest'ultimo e questo è qualcosa che può essere risolto nel migliore dei modi a lungo termine assicurandoti di avere solide basi, non tagliando via all'IDE
James Snell,

@JamesSnell Penso che stai diventando più sicuro di te. Guarda il codice e dimmi cosa ti fa pensare che sia sotto documentato? Se vedi chiaramente non è stato menzionato da nessuna parte che non sono riuscito a risolvere il problema? Volevo solo sapere come evitare di ripetere lo stesso errore. Penso che tutti i tuoi programmi siano corretti in una volta sola.
CodeYogi,

4
@CodeYogi Se devi fare "tentativi ed errori" e "ti senti frustrato" e "fai gli stessi errori" con la tua codifica, allora questi sono segni che non hai capito abbastanza bene il tuo problema prima di iniziare a scrivere . Nessuno sta dicendo che non l'hai capito, ma che il tuo codice avrebbe potuto essere meglio pensato e sono segni che stai lottando da cui hai la scelta di imparare e da cui imparare o no.
James Snell,

2
@CodeYogi ... e dal momento che mi chiedi, raramente mi capita di sbagliare il mio looping e il branching perché ho intenzione di avere una chiara comprensione di ciò che devo ottenere prima di scrivere il codice, non è difficile fare qualcosa di semplice come un ordinato classe di array. Come programmatore una delle cose più difficili da fare è ammettere che tu sei il problema, ma fino a quando non lo fai non inizierai a scrivere codice davvero buono.
James Snell,

3

Forse dovrei mettere un po 'di carne sul mio commento:

C'è solo un modo: capire meglio il tuo problema. Ma è tanto generale quanto la tua domanda

Il tuo punto è

Anche se ho avuto i miei presupposti corretti dopo qualche prova ed errore, ma sono stato troppo frustrato a causa della mancanza di un modello di calcolo corretto nella mia testa.

Quando leggo trial and error, le mie campane d'allarme iniziano a suonare. Naturalmente molti di noi conoscono lo stato d'animo, quando uno vuole risolvere un piccolo problema e ha avvolto la testa attorno ad altre cose e inizia a indovinare in un modo o nell'altro, per fare il codice seem, cosa dovrebbe fare. Ne derivano alcune soluzioni di hacking - e alcune sono pure geniali ; ma ad essere sinceri: molti di loro non lo sono . Me compreso, conoscendo questo stato.

Indipendentemente dal tuo problema concreto, hai posto domande su come migliorare:

1) Test

Questo è stato detto da altri e non avrei nulla di prezioso da aggiungere

2) Analisi dei problemi

È difficile dare qualche consiglio al riguardo. Ci sono solo due suggerimenti che posso darti, che probabilmente ti aiuteranno a migliorare le tue abilità su questo argomento:

  • quello ovvio e più banale è a lungo termine il più efficace: risolvere molti problemi. Mentre pratichi e ripeti, sviluppi la mentalità che ti aiuta per compiti futuri. La programmazione è come qualsiasi altra attività da migliorare con la pratica del duro lavoro

Code Katas è un modo, che potrebbe aiutare un po '.

Come puoi diventare un grande musicista? Aiuta a conoscere la teoria e a comprendere la meccanica del tuo strumento. Aiuta ad avere talento. Ma alla fine, la grandezza viene dalla pratica; applicando la teoria più e più volte, usando il feedback per migliorare ogni volta.

Codice Kata

Un sito, che mi piace molto: Code Wars

Ottieni padronanza attraverso la sfida Migliora le tue abilità allenandoti con gli altri sulle sfide del codice reale

Sono problemi relativamente piccoli, che ti aiutano ad affinare le tue capacità di programmazione. E quello che mi piace di più in Code Wars è che potresti confrontare la tua soluzione con una delle altre .

O forse, dovresti dare un'occhiata a Exercism.io dove ricevi feedback dalla community.

  • L'altro consiglio è altrettanto banale: impara a risolvere i problemi Devi allenarti, suddividendo i problemi in problemi davvero piccoli. Se dici, hai problemi nella scrittura di loop , fai l'errore, che vedi il loop come un intero costrutto e non lo decostruisci in pezzi. Se impari a smontare le cose passo dopo passo , impari a evitare tali errori.

So - come ho detto sopra a volte che ti trovi in ​​uno stato simile - che è difficile dividere le cose "semplici" in compiti più "morti semplici"; ma aiuta molto.

Ricordo, quando ho appreso per la prima volta la programmazione professionale , ho avuto enormi problemi con il debug del mio codice. Qual'era il problema? Hybris - L'errore non può essere in tale e tale regione del codice, perché so che non può essere. E di conseguenza? Ho sfogliato il codice invece di analizzarlo che dovevo imparare, anche se era noioso rompere il mio codice per istruzioni .

3) Sviluppa una cintura degli attrezzi

Oltre a conoscere la tua lingua e i tuoi strumenti - so che queste sono le cose brillanti a cui gli sviluppatori pensano prima - impara gli algoritmi (ovvero la lettura).

Ecco due libri per iniziare:

È come imparare alcune ricette per iniziare a cucinare. All'inizio non sai cosa fare, quindi devi guardare, cosa hanno cucinato per te gli chef precedenti . Lo stesso vale per gli algortihms. Gli algoritmi sono come cucinare ricette per pasti comuni (strutture dati, ordinamento, hashing ecc.) Se le conosci (almeno provaci) a memoria, hai un buon punto di partenza.

3a) Conoscere costrutti di programmazione

Questo punto è un derivato - per così dire. Conosci la tua lingua - e meglio: conosci quali sono i costrutti possibili nella tua lingua.

Un punto comune per il codice cattivo o inefficent è a volte, che il programmatore non conosce la differenza tra i diversi tipi di loop ( for-, while-e do-loops). Sono in qualche modo tutti intercambiabili utilizzabili; ma in alcune circostanze la scelta di un altro costrutto in loop porta a un codice più elegante.

E c'è il dispositivo di Duff ...

PS:

altrimenti il ​​tuo commento non è buono di Donal Trump.

Sì, dovremmo rendere di nuovo eccezionale la codifica!

Un nuovo motto per StackOverflow.


Ah, lascia che ti dica una cosa molto sul serio. Ho fatto tutto ciò che hai menzionato e posso persino darti il ​​mio link su quei siti. Ma la cosa che sta diventando frustrante per me è invece di ottenere la risposta alla mia domanda, sto ricevendo tutti i possibili consigli sulla programmazione. Solo un ragazzo ha menzionato le pre-postcondizioni fino ad ora e lo apprezzo.
CodeYogi,

Da quello che dici, è difficile immaginare dove sia il tuo problema. Forse una metafora aiuta: per me è come dire "come posso vedere" - la risposta ovvia per me è "usa i tuoi occhi" perché vedere per me è così naturale, non posso immaginare, come non si può vedere. Lo stesso vale per la tua domanda.
Thomas Junk,

Concordare completamente con le campane di allarme su "tentativi ed errori". Penso che il modo migliore per apprendere appieno la mentalità di problem solving sia eseguire algoritmi e codice con carta e matita.
Carattere jolly

Ehm ... perché hai un jab grammaticalmente povero non sequitur su un candidato politico citato senza contesto nel mezzo della tua risposta sulla programmazione?
Carattere jolly

2

Se ho capito correttamente il problema, la tua domanda è come pensare di ottenere i loop fin dal primo tentativo, non come assicurarsi che il tuo loop sia corretto (per il quale la risposta verrebbe testata come spiegato in altre risposte).

Quello che considero un buon approccio è scrivere la prima iterazione senza alcun ciclo. Dopo averlo fatto, dovresti notare cosa dovrebbe essere cambiato tra le iterazioni.

È un numero, come uno 0 o un 1? Quindi molto probabilmente hai bisogno di un for, e il bingo, hai anche il tuo io iniziale. Quindi pensa quante volte vuoi eseguire la stessa cosa e avrai anche la tua condizione finale.

Se non sai ESATTAMENTE quante volte verrà eseguito, allora non hai bisogno di un per, ma un po 'o un po'.

Tecnicamente, qualsiasi loop può essere tradotto in qualsiasi altro loop, ma il codice è più facile da leggere se si utilizza il loop giusto, quindi ecco alcuni suggerimenti:

  1. Se ti ritrovi a scrivere un if () {...; break;} all'interno di un for, hai bisogno di un po 'di tempo e hai già la condizione

  2. "While" è forse il loop più utilizzato in qualsiasi lingua, ma non dovrebbe imo. Se ti ritrovi a scrivere bool ok = True; while (controlla) {fai qualcosa e speriamo che cambi ad ok ad un certo punto}; quindi non è necessario un po 'di tempo, ma un po' di tempo, perché significa che hai tutto il necessario per eseguire la prima iterazione.

Ora un po 'di contesto ... Quando ho imparato per la prima volta a programmare (Pascal), non parlavo inglese. Per me, "per" e "while", non aveva molto senso, ma la parola chiave "repeat" (do while in C) è quasi la stessa nella mia lingua madre, quindi la userei per tutto. A mio avviso, ripeti (do while) è il ciclo più naturale, perché quasi sempre vuoi fare qualcosa e poi vuoi che sia fatto ancora e ancora, fino a quando non viene raggiunto un obiettivo. "For" è solo una scorciatoia che ti dà un iteratore e pone stranamente la condizione all'inizio del codice, anche se, quasi sempre, vuoi fare qualcosa fino a quando non accade qualcosa. Inoltre, while è solo una scorciatoia per if () {do while ()}. Le scorciatoie sono utili per dopo,


2

Ho intenzione di dare un esempio più dettagliato di come utilizzare le condizioni pre / post e gli invarianti per sviluppare un ciclo corretto. Insieme, tali affermazioni sono chiamate specifiche o contratto.

Non sto suggerendo di provare a farlo per ogni ciclo. Spero che troverai utile vedere coinvolto il processo di pensiero.

Per fare ciò, tradurrò il tuo metodo in uno strumento chiamato Microsoft Dafny , progettato per dimostrare la correttezza di tali specifiche. Controlla anche la chiusura di ciascun loop. Si noti che Dafny non ha un forciclo, quindi ho dovuto usare un whileciclo invece.

Infine, mostrerò come è possibile utilizzare tali specifiche per progettare una versione, probabilmente, leggermente più semplice del tuo loop. Questa versione più semplice del loop ha infatti le condizioni del loop j > 0e l'assegnazione array[j] = value, così come l'intuizione iniziale.

Dafny dimostrerà per noi che entrambi questi loop sono corretti e fanno la stessa cosa.

Farò quindi un reclamo generale, basato sulla mia esperienza, su come scrivere un corretto ciclo al contrario, che forse ti aiuterà se dovessi affrontare questa situazione in futuro.

Prima parte - Scrivere una specifica per il metodo

La prima sfida che dobbiamo affrontare è determinare cosa dovrebbe effettivamente fare il metodo. A tal fine ho progettato le condizioni pre e post che specificano il comportamento del metodo. Per rendere più precisa la specifica, ho migliorato il metodo per farlo restituire l'indice in cui è valuestato inserito.

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..])

Questa specifica cattura completamente il comportamento del metodo. La mia osservazione principale su questa specifica è che sarebbe semplificato se alla procedura fosse passato il valore rightIndex+1anziché rightIndex. Ma dal momento che non riesco a vedere da dove viene chiamato questo metodo, non so quale effetto avrebbe avuto quel cambiamento sul resto del programma.

Parte seconda: determinare un invariante di loop

Ora abbiamo una specifica per il comportamento del metodo, dobbiamo aggiungere una specifica del comportamento del ciclo che convincerà Dafny che l'esecuzione del ciclo terminerà e porterà allo stato finale desiderato array.

Quello che segue è il tuo loop originale, tradotto nella sintassi di Dafny con gli invarianti di loop aggiunti. L'ho anche modificato per restituire l'indice in cui è stato inserito il valore.

{
    // 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
}

Ciò si verifica in Dafny. Puoi vederlo da solo seguendo questo link . Quindi il tuo loop implementa correttamente le specifiche del metodo che ho scritto nella prima parte. Dovrai decidere se questa specifica del metodo è davvero il comportamento che volevi.

Nota che Dafny sta producendo una prova di correttezza qui. Questa è una garanzia di correttezza molto più forte di quella che si può ottenere dai test.

Terza parte: un ciclo più semplice

Ora che abbiamo una specifica del metodo che cattura il comportamento del loop. Siamo in grado di modificare in modo sicuro l'implementazione del loop pur mantenendo la sicurezza di non aver modificato il comportamento del loop.

Ho modificato il loop in modo che corrisponda alle tue intuizioni originali sulla condizione del loop e sul valore finale di j. Direi che questo loop è più semplice del loop che hai descritto nella tua domanda. È più spesso in grado di utilizzare jpiuttosto che j+1.

  1. Inizia j alle rightIndex+1

  2. Cambia la condizione del loop in j > 0 && arr[j-1] > value

  3. Cambia il compito in arr[j] := value

  4. Decrementa il contatore del loop alla fine del loop anziché all'inizio

Ecco il codice Nota che gli invarianti di loop sono anche un po 'più facili da scrivere ora:

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;
}

Quarta parte: consigli sul looping all'indietro

Dopo aver scritto e dimostrato molti loop corretti per diversi anni, ho i seguenti consigli generali sul looping all'indietro.

È quasi sempre più facile pensare e scrivere un loop all'indietro (in decremento) se il decremento viene eseguito all'inizio del loop piuttosto che alla fine.

Sfortunatamente il forcostrutto del ciclo in molte lingue lo rende difficile.

Sospetto (ma non posso provare) che questa complessità sia ciò che ha causato la differenza nella tua intuizione su ciò che dovrebbe essere il ciclo e ciò che effettivamente doveva essere. Sei abituato a pensare ai loop forward (incrementali). Quando si desidera scrivere un ciclo all'indietro (in decremento), si tenta di creare il ciclo tentando di invertire l'ordine in cui le cose accadono in un ciclo in avanti (in aumento). Ma a causa del modo in cui funziona il forcostrutto, hai trascurato di invertire l'ordine delle assegnazioni e l'aggiornamento delle variabili del ciclo, che è necessario per una vera inversione dell'ordine delle operazioni tra un ciclo indietro e un ciclo avanti.

Parte quinta: bonus

Solo per completezza, ecco il codice che ottieni se passi rightIndex+1al metodo anziché rightIndex. Questa modifica elimina tutti gli +2offset che sarebbero altrimenti necessari per pensare alla correttezza del loop.

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
Gradirei davvero un commento se
voti

2

Fare ciò in modo facile e fluido è una questione di esperienza. Anche se il linguaggio non ti consente di esprimerlo direttamente o stai utilizzando un caso più complesso di quello che la semplice cosa incorporata può gestire, quello che pensi sia di livello superiore come "visita ogni elemento una volta in ordine di riverenza" e il programmatore più esperto lo traduce immediatamente nei dettagli giusti perché lo ha fatto così tante volte.

Anche allora, nei casi più complessi è facile da sbagliare, perché la cosa si sta scrivendo è in genere non è la cosa comune in scatola. Con i linguaggi e le biblioteche più moderne, non scrivi la cosa facile perché c'è un costrutto in scatola o chiedilo. In C ++ il mantra moderno è "usa algoritmi anziché scrivere codice".

Quindi, il modo per assicurarsi che sia giusto, per questo tipo di cose in particolare, è guardare alle condizioni al contorno . Traccia il codice nella tua testa per i pochi casi ai margini di dove le cose cambiano. Se il index == array-max, cosa succede? Che dire max-1? Se il codice fa una svolta sbagliata, si troverà in uno di questi limiti. Alcuni loop devono preoccuparsi del primo o dell'ultimo elemento, nonché del costrutto di looping per ottenere i limiti giusti; ad es. se ti riferisci a[I]e a[I-1]cosa succede quando Iè il valore minimo?

Inoltre, guarda i casi in cui il numero di iterazioni (corrette) è estremo: se i limiti si incontrano e avrai 0 iterazioni, funzionerà senza un caso speciale? E che dire di solo 1 iterazione, in cui il limite più basso è anche il limite più alto contemporaneamente?

Esaminare i casi limite (entrambi i lati di ciascun bordo) è cosa dovresti fare quando scrivi il ciclo e cosa dovresti fare nelle revisioni del codice.


1

Cercherò di stare alla larga dagli argomenti citati a bizzeffe.

Quali sono gli strumenti / modelli mentali per evitare tali errori?

Utensili

Per me, il più grande strumento per scrivere meglio fore whileloop non è scrivere alcuno foro whileloop affatto.

La maggior parte delle lingue moderne cerca di affrontare questo problema in un modo o nell'altro. Ad esempio, Java, pur avendo Iteratorsin dall'inizio, che era un po 'ingombrante da usare, ha introdotto una sintassi di scelta rapida per usarli più facilmente in una versione più latente. Anche C # li ha, ecc.

Il mio linguaggio attualmente preferito, Ruby, ha assunto un approccio funzionale ( .each, .mapecc.) A tutto campo. Questo è molto potente. Ho appena fatto un rapido conteggio in alcune basi di codice di Ruby su cui sto lavorando: in circa 10.000 righe di codice, ci sono zero fore circa 5 while.

Se fossi costretto a scegliere una nuova lingua, cercare loop funzionali / basati sui dati come quello sarebbe molto in cima alla lista delle priorità.

Modelli mentali

Tieni presente che whileè il minimo indispensabile di astrazione che puoi ottenere, solo un passo sopra goto. A mio avviso, lo forrende ancora peggio invece che migliore poiché raggruppa tutte e tre le parti del circuito strettamente insieme.

Quindi, se mi trovo in un ambiente in cui forviene utilizzato, allora mi assicuro che tutte e 3 le parti siano completamente semplici e sempre uguali. Questo significa che scriverò

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

Ma niente di molto più complesso. Forse, a volte avrò un conto alla rovescia, ma farò del mio meglio per evitarlo.

Se whilesto usando , sto alla larga dagli shenannigans interni contorti che coinvolgono la condizione del loop. Il test all'interno del test while(...)sarà il più semplice possibile e lo eviterò breaknel miglior modo possibile. Inoltre il ciclo sarà breve (contando le righe di codice) e verranno prese in considerazione eventuali quantità maggiori di codice.

Soprattutto se la condizione while effettiva è complessa, userò una "variabile di condizione" che è molto facile da individuare e non inserirò la condizione nell'istruzione whilestessa:

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

(O qualcosa del genere, nella misura corretta, ovviamente.)

Questo ti dà un modello mentale molto semplice che è "questa variabile viene eseguita da 0 a 10 in modo monotono" o "questo ciclo viene eseguito fino a quando quella variabile è falsa / vera". La maggior parte dei cervelli sembra essere in grado di gestire bene questo livello di astrazione.

Spero che aiuti.


1

I loop inversi, in particolare, possono essere difficili da ragionare perché molti dei nostri linguaggi di programmazione sono orientati verso l'iterazione in avanti, sia nella sintassi for-loop comune sia mediante l'uso di intervalli semiaperti basati su zero. Non sto dicendo che è sbagliato che le lingue abbiano fatto quelle scelte; Sto solo dicendo che quelle scelte complicano il pensare ai circuiti inversi.

In generale, ricorda che un for-loop è solo zucchero sintattico costruito attorno a un ciclo while:

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

è equivalente a:

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

(possibilmente con un ulteriore livello di ambito per mantenere le variabili dichiarate nel passaggio init locali al loop).

Questo va bene per molti tipi di anelli, ma avere l'ultimo gradino è imbarazzante quando si cammina all'indietro. Quando lavoro all'indietro, trovo che sia più facile avviare l'indice del ciclo con il valore successivo a quello desiderato e spostare la stepparte all'inizio del ciclo, in questo modo:

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
}

o, come un ciclo for:

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

Questo può sembrare snervante, poiché l'espressione del passo è nel corpo piuttosto che nell'intestazione. Questo è uno sfortunato effetto collaterale della propensione intrinseca per la sintassi del loop. Per questo motivo, alcuni sosterranno che invece fai questo:

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

Ma questo è un disastro se la tua variabile indice è un tipo senza segno (come potrebbe essere in C o C ++).

Con questo in mente, scriviamo la tua funzione di inserimento.

  1. Dato che lavoreremo all'indietro, lasceremo che l'indice di loop sia la voce dopo lo slot di array "corrente". Progetterei la funzione per portare la dimensione dell'intero anziché un indice all'ultimo elemento perché gli intervalli semiaperti sono il modo naturale di rappresentare gli intervalli nella maggior parte dei linguaggi di programmazione e perché ci offrono un modo per rappresentare un array vuoto senza ricorrere ad un valore magico come -1.

    function insert(array, size, value) {
      var j = size;
    
  2. Mentre il nuovo valore è più piccolo dell'elemento precedente , continua a spostare. Naturalmente, l'elemento precedente può essere verificata solo se v'è un elemento precedente, quindi dobbiamo prima verificare che non siamo proprio all'inizio:

      while (j != 0 && value < array[j - 1]) {
        --j;  // now j become current
        array[j + 1] = array[j];
      }
    
  3. Questo lascia jdove vogliamo il nuovo valore.

      array[j] = value; 
    };
    

La programmazione di Pearls di Jon Bentley fornisce una spiegazione molto chiara del tipo di inserzione (e di altri algoritmi), che può aiutarti a costruire i tuoi modelli mentali per questo tipo di problemi.


0

Sei semplicemente confuso su ciò forche fa effettivamente un loop e sulla meccanica di come funziona?

for(initialization; condition; increment*)
{
    body
}
  1. Innanzitutto, viene eseguita l'inizializzazione
  2. Quindi viene verificata la condizione
  3. Se la condizione è vera, il corpo viene eseguito una volta. In caso contrario, vai a # 6
  4. Il codice di incremento viene eseguito
  5. Vai a # 2
  6. Fine del ciclo

Potrebbe essere utile riscrivere questo codice usando un costrutto diverso. Ecco lo stesso usando un ciclo while:

initialization
while(condition)
{
    body
    increment
}

Ecco alcuni altri suggerimenti:

  • Puoi usare un altro linguaggio costruttivo come un ciclo foreach? Questo si occupa della condizione e del passaggio incrementale per te.
  • Puoi usare una funzione Mappa o Filtro? Alcune lingue hanno funzioni con questi nomi che eseguono internamente una raccolta per te. Fornisci solo la collezione e il corpo.
  • Dovresti davvero passare più tempo a familiarizzare con i forloop. Li userai sempre. Ti suggerisco di passare attraverso un ciclo for in un debugger per vedere come viene eseguito.

* Nota che mentre uso il termine "incremento", è solo un po 'di codice che segue il corpo e prima del controllo delle condizioni. Può essere un decremento o niente del tutto.


1
perché il downvote?
user2023861,

0

Tentativo di approfondimento aggiuntivo

Per algoritmi non banali con loop, puoi provare il seguente metodo:

  1. Crea un array fisso con 4 posizioni e inserisci alcuni valori per simulare il problema;
  2. Scrivi il tuo algoritmo per risolvere il problema dato , senza alcun ciclo e con indicizzazioni codificate ;
  3. Dopodiché, sostituisci le indicizzazioni codificate nel tuo codice con alcune variabili io j, e incrementa / decrementa queste variabili come necessario (ma ancora senza alcun ciclo);
  4. Riscrivi il tuo codice e inserisci i blocchi ripetitivi all'interno di un ciclo , soddisfacendo le condizioni pre e post;
  5. [ opzionale ] riscrivi il tuo loop in modo che sia nella forma desiderata (per / while / do while);
  6. Il più importante è scrivere correttamente il tuo algoritmo; successivamente, refactoring e optmize del codice / loop se necessario (ma ciò potrebbe rendere il codice non più banale per un lettore)

Il tuo problema

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

Scrivi il corpo del ciclo manualmente più volte

Usiamo un array fisso con 4 posizioni e proviamo a scrivere l'algoritmo manualmente, senza loop:

           //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

Riscrivi, rimuovendo i valori codificati

//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

Traduci in un ciclo

Con 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;
    }
}

Rifattorizza / riscrivi / ottimizza il loop nel modo desiderato:

Con 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

con 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: il codice presuppone che l'input sia valido e che l'array non contenga ripetizioni;


-1

Questo è il motivo per cui eviterei di scrivere loop che operino su indici grezzi, a favore di operazioni più astratte, quando possibile.

In questo caso, farei una cosa del genere (in pseudo codice):

array = array[:(rightIndex - 1)] + value + array[rightIndex:]

-3

Nel tuo esempio, il corpo del loop è abbastanza ovvio. Ed è abbastanza ovvio che alcuni elementi devono essere cambiati alla fine. Quindi scrivi il codice, ma senza l'inizio del ciclo, la condizione finale e l'assegnazione finale.

Quindi ti allontani dal codice e scopri qual è la prima mossa che deve essere eseguita. Si modifica l'inizio del loop in modo che la prima mossa sia corretta. Ti allontani di nuovo dal codice e scopri qual è l'ultima mossa che deve essere eseguita. Si modifica la condizione finale in modo che l'ultima mossa sia corretta. E infine, ti allontani dal tuo codice e capisci quale deve essere l'assegnazione finale e correggi il codice di conseguenza.


1
Puoi per favore fare qualche esempio?
CodeYogi,

L'OP ha avuto un esempio.
gnasher729,

2
Cosa intendi? Sono l'OP.
CodeYogi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.