Perché i loop nidificati sono considerati cattive pratiche?


33

Il mio docente ha affermato oggi che era possibile "etichettare" i loop in Java in modo da poterli fare riferimento quando si tratta di loop nidificati. Quindi ho cercato la funzione perché non ne ero a conoscenza e in molti posti in cui questa funzione è stata spiegata è stata seguita da un avvertimento, scoraggiando i cicli annidati.

Non capisco davvero perché? È perché influenza la leggibilità del codice? O è qualcosa di più "tecnico"?


4
Se ricordo correttamente il mio corso CS3 è perché spesso porta a tempi esponenziali, il che significa che se si ottiene un set di dati di grandi dimensioni l'applicazione diventerà inutilizzabile.
Travis Pessetto,

21
Una cosa che dovresti imparare sui docenti di CS è che non tutto ciò che dicono vale al 100% nel mondo reale. Scoraggerei i loop nidificati più di alcuni in profondità, ma se devi elaborare elementi m x n per risolvere il tuo problema, farai molte iterazioni.
Blrfl,

8
@TravisPessetto In realtà è ancora complessità polinomiale - O (n ^ k), k è il numero di N annidato, non esponenziale, (k ^ n), dove k è una costante.
m3th0dman,

1
@ m3th0dman Grazie per avermi corretto. Il mio insegnante non era il massimo in materia. Trattava O (n ^ 2) e O (k ^ n) allo stesso modo.
Travis Pessetto,

2
I loop nidificati aumentano la complessità ciclomatica (vedi qui ), che diminuisce la manutenibilità di un programma, secondo alcune persone.
Marco,

Risposte:


62

I loop nidificati vanno bene purché descrivano l'algoritmo corretto.

I loop nidificati hanno considerazioni sulle prestazioni (vedere la risposta di @ Travis-Pesetto), ma a volte è esattamente l'algoritmo corretto, ad esempio quando è necessario accedere a tutti i valori in una matrice.

L'etichettatura dei loop in Java consente di uscire prematuramente da diversi loop nidificati quando altri modi per farlo sarebbero ingombranti. Ad esempio, alcuni giochi potrebbero avere un codice come questo:

Player chosen_one = null;
...
outer: // this is a label
for (Player player : party.getPlayers()) {
  for (Cell cell : player.getVisibleMapCells()) {
    for (Item artefact : cell.getItemsOnTheFloor())
      if (artefact == HOLY_GRAIL) {
        chosen_one = player;
        break outer; // everyone stop looking, we found it
      }
  }
}

Mentre il codice come nell'esempio sopra può talvolta essere il modo ottimale per esprimere un certo algoritmo, di solito è meglio suddividere questo codice in funzioni più piccole e probabilmente usare al returnposto di break. Quindi un breakcon un'etichetta è un debole odore di codice ; presta particolare attenzione quando lo vedi.


2
Proprio come la grafica di una nota a margine usa un algoritmo che deve accedere a ogni parte di una matrice. Tuttavia, la GPU è specializzata per gestirlo in modo efficiente in termini di tempo.
Travis Pessetto,

Sì, la GPU lo fa in modo enormemente parallelo; la domanda riguardava un singolo thread di esecuzione, suppongo.
9000

2
Uno dei motivi per cui le etichette tendono ad essere sospette è perché spesso esiste un'alternativa. In questo caso invece potresti tornare.
jgmjgm,

22

I loop nidificati sono spesso (ma non sempre) cattive pratiche, perché sono spesso (ma non sempre) eccessivi per quello che stai cercando di fare. In molti casi, esiste un modo molto più rapido e meno dispendioso per raggiungere l'obiettivo che stai cercando di raggiungere.

Ad esempio, se hai 100 elementi nell'elenco A e 100 elementi nell'elenco B e sai che per ogni elemento nell'elenco A c'è un elemento nell'elenco B che lo corrisponde, (con la definizione di "match" lasciato deliberatamente oscuro qui) e vuoi produrre un elenco di coppie, il modo semplice per farlo è così:

for each item X in list A:
  for each item Y in list B:
    if X matches Y then
      add (X, Y) to results
      break

Con 100 voci in ogni elenco, occorreranno in media 100 * 100/2 (5.000) matchesoperazioni. Con più articoli o se la correlazione 1: 1 non è garantita, diventa ancora più costosa.

D'altra parte, c'è un modo molto più veloce per eseguire un'operazione come questa:

sort list A
sort list B (according to the same sort order)
I = 0
J = 0
repeat
  X = A[I]
  Y = B[J]
  if X matches Y then
    add (X, Y) to results
    increment I
    increment J
  else if X < Y then
    increment I
  else increment J
until either index reaches the end of its list

Se lo fai in questo modo, invece del numero di matchesoperazioni su length(A) * length(B)cui si basa length(A) + length(B), ora è basato su , il che significa che il tuo codice verrà eseguito molto più velocemente.


10
Con l'avvertenza che la configurazione dell'ordinamento richiede una quantità non banale di tempo, O(n log n)due volte, se si utilizza Quicksort.
Robert Harvey,

@RobertHarvey: Certo. Ma è ancora molto meno che O(n^2)per i valori non minuscoli di N.
Mason Wheeler,

10
La seconda versione dell'algoritmo è generalmente errata. In primo luogo, presuppone che X e Y siano comparabili tramite <operatore, che generalmente non possono essere derivati matchesdall'operatore. In secondo luogo, anche se sia X che Y sono numerici, il 2o algoritmo potrebbe comunque produrre risultati errati, ad esempio quando lo X matches Yè X + Y == 100.
Pasha,

9
@ user958624: Ovviamente questa è una panoramica di altissimo livello di un algoritmo generale. Come l'operatore "corrispondenze", "<" deve essere definito in modo corretto nel contesto dei dati confrontati. Se questo viene fatto correttamente, i risultati saranno corretti.
Mason Wheeler,

PHP ha fatto qualcosa di simile a quello precedente con il suo array unico, credo e / o il contrario. Le persone invece userebbero solo gli array di PHP basati sull'hash e sarebbe molto più veloce.
jgmjgm,

11

Un motivo per evitare i loop di nidificazione è perché è una cattiva idea nidificare le strutture dei blocchi troppo in profondità, indipendentemente dal fatto che siano o meno loop.

Ogni funzione o metodo dovrebbe essere facile da capire, sia per lo scopo (il nome dovrebbe esprimere ciò che fa) sia per i manutentori (dovrebbe essere facile capire gli interni). Se una funzione è troppo complicata per essere facilmente comprensibile, ciò significa che alcuni interni devono essere fattorizzati in funzioni separate in modo che possano essere indicati nella funzione (ora più piccola) per nome.

I loop nidificati possono diventare difficili da capire relativamente rapidamente, anche se alcuni nidificamenti di loop vanno bene, fornendo, come altri sottolineano, ciò non significa che stai creando un problema di prestazioni utilizzando un algoritmo estremamente (e inutilmente) lento.

In realtà, non sono necessari loop nidificati per ottenere limiti di prestazioni assurdamente lenti. Si consideri, ad esempio, un singolo ciclo che in ogni iterazione prende un elemento da una coda, quindi probabilmente ne mette diversi indietro - ad esempio la prima ricerca di un labirinto. Le prestazioni non sono decise dalla profondità di annidamento del loop (che è solo 1) ma dal numero di elementi che vengono messi in quella coda prima che alla fine sia esaurito ( se mai esaurito) - quanto è grande la parte raggiungibile del il labirinto è.


1
Molto spesso puoi semplicemente appiattire un loop nidificato e richiede ancora lo stesso tempo. prendere da 0 a larghezza; da 0 a altezza; Puoi invece semplicemente inserire da 0 a larghezza per altezza.
jgmjgm,

@jgmjgm - sì, a rischio di complicare il codice all'interno del loop. L'appiattimento può semplificare anche questo a volte, ma più spesso si aggiunge almeno la complessità del recupero degli indici che si desidera effettivamente. Un trucco per questo è usare un tipo di indice che tiene conto di tutti i loop che si annidano nella logica per incrementare un indice composto speciale - non è probabile che lo facciate solo per un loop, ma forse avete più loop con strutture simili o forse puoi scrivere una versione generica più flessibile. Le spese generali per l'utilizzo di quel tipo (se presente) possono valere la pena per chiarezza.
Steve314,

Non lo sto suggerendo come una buona cosa da fare, ma quanto sia sorprendentemente semplice trasformare due loop in uno ma non avere ancora un impatto sulla complessità temporale.
jgmjgm,

7

Dato il caso di molti loop nidificati, si finisce con il tempo polinomiale. Ad esempio dato questo pseudo codice:

set i equal to 1
while i is not equal to 100
  increment i
  set j equal to 1
  while j is not equal to i
    increment j
  end
 end

Questo sarebbe considerato O (n ^ 2) tempo che sarebbe un grafico simile a: inserisci qui la descrizione dell'immagine

Dove l'asse y è la quantità di tempo impiegata per terminare il programma e l'asse x è la quantità di dati.

Se ricevi troppi dati, il tuo programma sarà così lento che nessuno lo aspetterà. e non è che circa 1.000 voci di dati credo che impiegheranno troppo tempo.


10
potresti voler riselezionare il tuo grafico: questa è una curva esponenziale non quadratica, inoltre la ricorsione non crea roba O (n log n) da O (n ^ 2)
maniaco del cricchetto

5
Per ricorsione ho due parole "stack" e "overflow"
Mateusz,

10
La tua affermazione che la ricorsione può ridurre un'operazione O (n ^ 2) a O (n log n) è in gran parte inaccurata. Lo stesso algoritmo implementato in modo ricorsivo rispetto a quello iterativo dovrebbe avere esattamente la stessa complessità temporale big-O. Inoltre, la ricorsione può spesso essere più lenta (a seconda dell'implementazione del linguaggio), poiché ogni chiamata ricorsiva richiede la creazione di un nuovo stack frame, mentre l'iterazione richiede solo una diramazione / confronto. Le chiamate di funzione sono in genere economiche, ma non sono gratuite.
Dckrooney,

2
@TravisPessetto Ho visto 3 overflow dello stack negli ultimi 6 mesi durante lo sviluppo di applicazioni C # a causa di ricorsione o riferimenti di oggetti ciclici. La cosa divertente è che si bloccherà e non sai cosa ti ha colpito. Quando vedi dei loop nidificati, sai che può accadere qualcosa di brutto e l'eccezione sull'indice errato per qualcosa è facilmente visibile.
Mateusz,

2
@Mateusz inoltre, lingue come Java consentono di rilevare un errore di overflow dello stack. Questo con una traccia dello stack dovrebbe farti vedere cosa è successo. Non ho molta esperienza, ma l'unica volta che ho visto un errore di overflow dello stack è in un PHP che ha avuto un bug che causava una ricorsione infinita e PHP ha esaurito i 512 MB di memoria che gli sono stati assegnati. La ricorsione deve avere un valore finale di chiusura, non va bene per cicli infiniti. Come ogni cosa in CS c'è un tempo e un posto per tutte le cose.
Travis Pessetto,

0

Guidare un camion da trenta tonnellate anziché un piccolo veicolo passeggeri è una cattiva pratica. Tranne quando è necessario trasportare 20 o 30 tonnellate di materiale.

Quando si utilizza un ciclo nidificato, non è una cattiva pratica. O è assolutamente stupido, oppure è esattamente ciò che è necessario. Tu decidi.

Tuttavia, qualcuno si è lamentato dei cicli di etichettatura . La risposta per questo: se devi porre la domanda, allora non usare l'etichettatura. Se sai abbastanza per decidere te stesso, allora decidi te stesso.


Se sai abbastanza per decidere te stesso, sai abbastanza per non usare anelli etichettati. :-)
user949300

No. Quando ti è stato insegnato abbastanza, ti è stato insegnato a non usare l'uso etichettato. Quando ne sai abbastanza, trascendi oltre il dogma e fai ciò che è giusto.
gnasher729,

1
Il problema con l'etichetta è che raramente è necessario e molte persone lo usano presto per aggirare gli errori di controllo del flusso. Un po 'come il modo in cui le persone mettono l'uscita dappertutto quando sbagliano il controllo del flusso. Le funzioni lo rendono inoltre ampiamente ridondante.
jgmjgm,

-1

Non c'è nulla di intrinsecamente sbagliato o necessariamente cattivo nei loop nidificati. Tuttavia hanno alcune considerazioni e insidie.

Gli articoli a cui sei stato condotto, probabilmente in nome della brevità o a causa di un processo psicologico noto come bruciato, saltando le specifiche.

Essere bruciati è quando hai un'esperienza negativa di qualcosa con l'essere implicito che poi eviti. Ad esempio, potrei tagliare le verdure con un coltello affilato e tagliare me stesso. Potrei quindi dire che i coltelli affilati sono cattivi, non usarli per tagliare le verdure per cercare di rendere impossibile che quella brutta esperienza si ripeta. Questo è ovviamente molto poco pratico. In realtà devi solo stare attento. Se stai dicendo a qualcun altro di tagliare le verdure, allora hai un senso ancora più forte di questo. Se stessi insegnando ai bambini a tagliare le verdure, mi sentirei fortemente di dire loro di non usare un coltello affilato, soprattutto se non riesco a controllarli da vicino.

Il problema è che la programmazione non è che si raggiunga la massima efficienza se si preferisce sempre prima la sicurezza. In questo caso i bambini possono tagliare solo verdure morbide. Si confrontano con qualsiasi altra cosa e faranno solo un casino con un coltello smussato. È importante imparare a usare correttamente i loop, inclusi i loop nidificati, e non puoi farlo se sono considerati cattivi e non provi mai a usarli.

Come molte risposte qui evidenziano un loop nidificato è un'indicazione delle caratteristiche prestazionali del programma che può peggiorare in modo esponenziale ad ogni annidamento. Cioè, O (n), O (n ^ 2), O (n ^ 3) e così via che comprende O (n ^ profondità) dove la profondità rappresenta il numero di anelli che hai annidato. Man mano che il tuo annidamento cresce, il tempo richiesto cresce esponenzialmente. Il problema è che non è una certezza che la complessità del tempo o dello spazio sia tale (abbastanza spesso a * b * c, ma non tutti i cicli del nido possono essere eseguiti continuamente) né è una certezza che tu avere un problema di prestazioni anche se è quello.

Per molte persone, in particolare studenti, scrittori e docenti che per essere sinceri, raramente programmare per vivere o su base giornaliera per i cicli può anche essere qualcosa a cui non sono abituati e che ha indotto un carico cognitivo eccessivo nei primi incontri. Questo è un aspetto problematico perché c'è sempre una curva di apprendimento ed evitarlo non sarà efficace nel convertire gli studenti in programmatori.

I loop nidificati possono scatenarsi, ovvero possono finire nidificati molto profondamente. Se attraverso ogni continente, quindi attraverso ogni paese, quindi attraverso ogni città, quindi attraverso ogni negozio, quindi attraverso ogni scaffale, quindi attraverso ogni prodotto se è una lattina di fagioli attraverso ogni fagiolo e misuro le sue dimensioni per ottenere la media, quindi tu posso vedere che nidificherà molto profondamente. Avrai una piramide e molto spazio sprecato lontano dal margine sinistro. Potresti anche finire per uscire dalla pagina.

Questo è un problema che sarebbe storicamente più significativo in cui gli schermi erano piccoli e di bassa risoluzione. In quei casi anche pochi livelli di nidificazione potrebbero davvero occupare molto spazio. Questa è una preoccupazione minore oggi in cui la soglia è più alta sebbene possa ancora presentare un problema se c'è abbastanza nidificazione.

Correlata è l'argomento estetico. Molte persone non trovano nidificato per i loop esteticamente gradevoli in contrasto con i layout con un allineamento più coerente questo può o meno essere collegato a ciò a cui le persone sono abituate, il tracciamento degli occhi e altre preoccupazioni. È tuttavia problematico in quanto tende ad auto-rafforzarsi e alla fine può rendere il codice più difficile da leggere poiché rompere un blocco di codice e incapsulare loop dietro astrazioni come funzioni rischia anche di interrompere la mappatura del codice nel flusso di esecuzione.

C'è una naturale tendenza a ciò a cui le persone sono abituate. Se stai programmando qualcosa nel modo più semplice, la probabilità di non avere nidificazione è massima, la probabilità di aver bisogno di un livello diminuisce di un ordine di grandezza, la probabilità di un altro livello diminuisce di nuovo. La frequenza cala e significa essenzialmente quanto più profondo è l'annidamento, tanto meno i sensi umani sono addestrati ad anticiparlo.

In relazione a ciò è che in qualsiasi costrutto complesso, che può essere considerato un loop nidificato, allora dovresti sempre chiedere che la soluzione più semplice possibile in quanto esiste il potenziale per una soluzione mancata che necessita di meno loop. L'ironia è che una soluzione nidificata è spesso il modo più semplice per produrre qualcosa che funzioni con il minimo sforzo, complessità e carico cognitivo. Spesso è naturale nidificare per i loop. Se consideri ad esempio una delle risposte sopra in cui il modo molto più veloce di un ciclo nidificato per è anche molto più complesso e consiste in molto più codice.

È necessaria una grande cura poiché è spesso possibile astrarre gli anelli o appiattirli, ma il risultato finale è in definitiva una cura peggiore della malattia, in particolare se non si riceve ad esempio un miglioramento misurabile e significativo delle prestazioni dallo sforzo.

È molto comune per le persone sperimentare frequentemente problemi di prestazioni associati a cicli che indicano al computer di ripetere un'azione molte volte e che sono intrinsecamente spesso implicati in colli di bottiglia delle prestazioni. Purtroppo le risposte a questo possono essere molto superficiali. Diventa comune per le persone vedere un loop e vedere un problema di prestazioni in cui non c'è nessuno e quindi nascondere il loop dalla vista per nessun effetto reale. Il codice "sembra" veloce ma mettilo sulla strada, digita l'accensione, pedale dell'acceleratore e dai un'occhiata al tachimetro e potresti scoprire che è ancora veloce come una vecchia signora che cammina sul telaio più luminoso.

Questo tipo di nascondiglio è simile a se hai dieci rapinatori sulla tua rotta. Se invece di avere un percorso diretto verso dove vuoi andare, lo organizzi in modo che ci sia un rapinatore dietro ogni angolo, quindi dà l'illusione mentre inizi il tuo viaggio che non ci sono rapinatori. Lontano dagli occhi, lontano dal cuore. verrai ancora rapinato dieci volte, ma ora non lo vedrai arrivare.

La risposta alla tua domanda è che sono entrambe le cose, ma nessuna delle due preoccupazioni è assoluta. Sono interamente soggettivi o solo contestualmente obiettivi. Purtroppo a volte l'opinione totalmente soggettiva o piuttosto ha la precedenza e domina.

Come regola generale, se ha bisogno di un ciclo nidificato o che sembra il prossimo passo ovvio, è meglio non deliberare e semplicemente farlo. Tuttavia, se persistono dubbi, dovrebbe essere successivamente riesaminato.

Un'altra regola empirica è che dovresti sempre controllare la cardinalità e chiederti se questo ciclo sarà un problema. Nel mio esempio precedente ho attraversato le città. Per i test potrei passare solo attraverso dieci città ma qual è il numero massimo ragionevole di città che ci si aspetta dall'uso nel mondo reale? Potrei quindi moltiplicarlo per lo stesso per i continenti. È una regola pratica da considerare sempre con i loop, in particolare che ripetono una quantità dinamica (variabile) di volte ciò che potrebbe tradursi in down line.

Indipendentemente da ciò, fai sempre ciò che funziona per primo. Il modo in cui vedi un'opportunità per l'ottimizzazione può confrontare la tua soluzione ottimizzata con il più semplice per lavorare e confermare che ha prodotto i benefici previsti. Puoi anche dedicare troppo tempo all'ottimizzazione prematura prima che le misurazioni vengano eseguite e ciò porta a YAGNI o a perdere molto tempo e scadenze non rispettate.


L'esempio di coltello affilato <-> non è eccezionale, poiché i coltelli smussati sono generalmente più pericolosi per il taglio. E O (n) -> O (n ^ 2) -> O (n ^ 3) non è esponenziale in n, è geometrico o polinomiale .
Caleth,

Che i coltelli opachi siano peggio è un po 'un mito urbano. In pratica varia e di solito è specifico per il caso in cui i coltelli opachi sono particolarmente inadatti che richiedono molta forza e comportano molto slittamento. Hai ragione però, c'è un limite nascosto ma inerente lì, puoi solo tagliare verdure morbide. Considererei l'esponenziale n ^ profondità, ma hai ragione che l'esempio da solo non lo è.
jgmjgm,

@jgmjgm: n ^ depth è polinomiale, depth ^ n è esponenziale. Non è davvero una questione di interpretazione.
Roel Schroeven,

x ^ y è diverso da y ^ x? L'errore che hai fatto è che non hai mai letto la risposta. Hai cercato esponenziale e poi hai cercato equazioni che da sole non sono esponenziali. Se leggi vedrai che ho detto che aumenta in modo esponenziale per ogni strato di annidamento e se lo metti alla prova tu stesso, il tempo per (a = 0; a <n; a ++); per (b = 0; b <n ; b ++), per (c = 0; c <n; c ++); quando aggiungi o rimuovi loop, vedrai che è davvero esponenziale. Troverai un loop su n ^ 1, due su n ^ 2 e tre su n ^ 3. Non riesci a capire nidificato: D. Sottoinsiemi gemoetrici esponenziali.
jgmjgm,

Immagino che ciò dimostri che le persone lottano davvero con costrutti nidificati.
jgmjgm,
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.