Esiste un modo pratico per rendere immutabile una struttura di nodi collegati?


26

Ho deciso di scrivere un elenco collegato singolarmente e ho avuto il piano per rendere immutabile la struttura del nodo collegato interno.

Mi sono imbattuto in un intoppo però. Supponiamo di avere i seguenti nodi collegati (dalle addoperazioni precedenti ):

1 -> 2 -> 3 -> 4

e dire che voglio aggiungere a 5 .

Per fare ciò, poiché il nodo 4è immutabile, devo creare una nuova copia di 4, ma sostituire il suo nextcampo con un nuovo nodo contenente un 5. Il problema è ora, 3si riferisce al vecchio 4; quello senza l'aggiunta 5. Ora devo copiare 3e sostituire il suo nextcampo per fare riferimento alla 4copia, ma ora 2fa riferimento al vecchio3 ...

O in altre parole, per fare un'appendice, sembra che l'intero elenco debba essere copiato.

Le mie domande:

  • Il mio pensiero è corretto? C'è un modo per fare un'appendice senza copiare l'intera struttura?

  • Apparentemente "Java efficace" contiene la raccomandazione:

    Le lezioni dovrebbero essere immutabili a meno che non ci siano ottime ragioni per renderle mutabili ...

    È un buon caso di mutabilità?

Non penso che questo sia un duplicato della risposta suggerita poiché non sto parlando dell'elenco stesso; che ovviamente deve essere mutabile per conformarsi all'interfaccia (senza fare qualcosa come mantenere la nuova lista internamente e recuperarla tramite un getter. A pensarci bene, anche se ciò richiederebbe qualche mutazione; sarebbe solo ridotto al minimo). Sto parlando se gli interni dell'elenco devono essere immutabili.


Direi di sì: le rare collezioni immutabili in Java sono o con metodi mutanti scavalcati da lanciare, o le CopyOnWritexxxclassi incredibilmente costose utilizzate per il multi-threading. Nessuno si aspetta che le collezioni siano davvero immutabili (anche se crea alcune stranezze)
Ordous,

9
(Commenta la citazione.) Quella, una grande citazione, è spesso molto fraintesa. L'immutabilità dovrebbe essere applicata a ValueObject a causa dei suoi vantaggi , ma se si sta sviluppando in Java, non è pratico utilizzare l'immutabilità in luoghi in cui è prevista la mutabilità. Il più delle volte, una classe ha alcuni aspetti che dovrebbero essere immutabili e alcuni aspetti che devono essere mutabili in base ai requisiti.
rwong,

2
correlati (forse un duplicato): è meglio che l'oggetto di raccolta sia immutabile? "Senza spese generali per le prestazioni, questa raccolta (classe LinkedList) può essere introdotta come raccolta immutabile?"
moscerino del

Perché dovresti creare un elenco immutabile di link? Il più grande vantaggio di un elenco collegato è la mutabilità, non staresti meglio con un array?
Pieter B,

3
Potresti aiutarmi per favore a capire le tue esigenze? Vuoi avere un elenco immutabile, che vuoi mutare? Non è una contraddizione? Cosa intendi con "gli interni" della lista? Vuoi dire che i nodi aggiunti in precedenza non dovrebbero essere modificabili in seguito, ma consentire solo l'aggiunta all'ultimo nodo nell'elenco?
null,

Risposte:


46

Con gli elenchi in linguaggi funzionali, lavori quasi sempre con una testa e una coda, il primo elemento e il resto dell'elenco. La preparazione è molto più comune perché, come hai ipotizzato, l'aggiunta richiede di copiare l'intero elenco (o altre strutture di dati pigri che non assomigliano precisamente a un elenco collegato).

Nei linguaggi imperativi, l'aggiunta è molto più comune, perché tende a sembrare semanticamente più naturale e non ti importa di invalidare i riferimenti alle versioni precedenti dell'elenco.

Come esempio del perché anteporre non richiede la copia dell'intero elenco, considera di avere:

2 -> 3 -> 4

Preparare un 1ti dà:

1 -> 2 -> 3 -> 4

Ma nota che non importa se qualcun altro ha ancora un riferimento 2come capo della loro lista, perché la lista è immutabile e i collegamenti vanno solo in una direzione. Non c'è modo di dire che 1c'è anche se hai solo un riferimento a 2. Ora, se hai aggiunto un 5a uno dei due elenchi, dovresti fare una copia dell'intero elenco, perché altrimenti apparirebbe anche sull'altro elenco.


Ah, buon punto sul perché anteporre non richiede una copia; Non ci ho pensato.
Carcigenicato,

17

Hai ragione, l'aggiunta richiede di copiare l'intero elenco se non sei disposto a mutare qualsiasi nodo sul posto. Perché dobbiamo impostare il nextpuntatore del (ora) penultimo nodo, che in un'impostazione immutabile crea un nuovo nodo, e quindi dobbiamo impostare il nextpuntatore del terzo penultimo nodo e così via.

Penso che il problema principale qui non sia l'immutabilità, né l' appendoperazione è sconsiderata. Entrambi stanno perfettamente bene nei rispettivi domini. Mischiarli è male: l'interfaccia naturale (efficiente) per un elenco immutabile ha enfatizzato la manipolazione nella parte anteriore dell'elenco, ma per gli elenchi mutabili è spesso più naturale costruire l'elenco aggiungendo successivamente gli elementi dal primo all'ultimo.

Pertanto ti consiglio di prendere una decisione: vuoi un'interfaccia effimera o persistente? La maggior parte delle operazioni produce un nuovo elenco e lascia una versione non modificata accessibile (persistente) o scrivi codice come questo (effimero):

list.append(x);
list.prepend(y);

Entrambe le scelte vanno bene, ma l'implementazione dovrebbe rispecchiare l'interfaccia: una struttura di dati persistente beneficia di nodi immutabili, mentre una effimera necessita di mutabilità interna per soddisfare effettivamente le promesse che fa implicitamente. java.util.Liste altre interfacce sono effimere: implementarle in un elenco immutabile è inappropriato e di fatto un rischio per le prestazioni. Buoni algoritmi su strutture di dati mutabili sono spesso abbastanza diversi da buoni algoritmi su strutture di dati immutabili, quindi vestire una struttura di dati immutabili come mutabile (o viceversa) invita algoritmi cattivi.

Mentre un elenco persistente presenta alcuni svantaggi (nessun accodamento efficiente), questo non deve essere un problema serio durante la programmazione funzionale: molti algoritmi possono essere formulati in modo efficiente spostando la mentalità e utilizzando funzioni di ordine superiore come mapo fold(per nominarne due relativamente primitive ) o anteponendo ripetutamente. Inoltre, nessuno ti sta obbligando a utilizzare solo questa struttura di dati: quando altri (effimeri o persistenti ma più sofisticati) sono più appropriati, usali. Devo anche notare che gli elenchi persistenti presentano alcuni vantaggi per altri carichi di lavoro: condividono le loro code, che possono conservare la memoria.


4

Se hai un elenco collegato singolarmente, lavorerai con il fronte se più di quanto faresti con il retro.

Linguaggi funzionali come prolog e haskel forniscono semplici modi per ottenere l'elemento frontale e il resto dell'array. L'aggiunta alla parte posteriore è un'operazione O (n) con copie di ciascun nodo.


1
Ho usato Haskell. Dopo tutto, elude anche parte del problema usando la pigrizia. Sto facendo degli allegati perché pensavo fosse quello che ci si aspettava Listdall'interfaccia (potrei sbagliarmi lì). Tuttavia, non penso che quel po 'risponda davvero alla domanda. L'intero elenco dovrebbe comunque essere copiato; renderebbe più rapido l'accesso all'ultimo elemento aggiunto poiché non sarebbe richiesto un attraversamento completo.
Carcigenicato,

La creazione di una classe in base a un'interfaccia che non corrisponde alla struttura / algoritmo dei dati sottostanti è un invito al dolore e alle inefficienze.
maniaco del cricchetto,

JavaDocs usa esplicitamente la parola "append". Stai dicendo che è meglio ignorarlo per motivi di implementazione?
Carcigenicato,

2
@Carcigenicato no Sto dicendo che è un errore cercare di inserire un elenco singolarmente collegato con nodi immutabili nello stampo dijava.util.List
maniaco del cricchetto

1
Con la pigrizia non ci sarebbe più un elenco collegato; non esiste un elenco, quindi non esiste una mutazione (append). Invece c'è del codice che genera l'elemento successivo su richiesta. Ma non può essere utilizzato ovunque in cui viene utilizzato un elenco. Vale a dire, se si scrive un metodo in Java che prevede di mutare un elenco che viene passato come argomento, tale elenco deve essere mutabile in primo luogo. L'approccio del generatore richiede una completa ristrutturazione (riorganizzazione) del codice per farlo funzionare e per consentire di eliminare fisicamente l'elenco.
rwong,

4

Come altri hanno sottolineato, hai ragione nel dire che un elenco immutabile con collegamento singolo richiede la copia dell'intero elenco quando si eseguono operazioni di accodamento.

Spesso è possibile utilizzare la soluzione alternativa per implementare l'algoritmo in termini di cons(anteporre) operazioni e quindi invertire l'elenco finale una volta. Questo deve ancora copiare l'elenco una volta, ma l'overhead di complessità è lineare nella lunghezza dell'elenco, mentre usando ripetutamente append è possibile ottenere facilmente una complessità quadratica.

Le liste delle differenze (vedi ad esempio qui ) sono un'alternativa interessante. Un elenco di differenze avvolge un elenco e fornisce un'operazione di aggiunta in tempo costante. Fondamentalmente lavori con il wrapper fintanto che devi aggiungere e poi riconvertito in un elenco quando hai finito. Questo è in qualche modo simile a quello che fai quando usi a StringBuilderper costruire una stringa, e alla fine ottieni il risultato come String(immutabile!) Chiamando toString. Una differenza è che a StringBuilderè mutabile, ma un elenco di differenze è immutabile. Inoltre, quando si converte di nuovo un elenco di differenze in un elenco, è comunque necessario costruire l'intero nuovo elenco ma, ancora una volta, è necessario farlo una sola volta.

Dovrebbe essere abbastanza facile implementare una DListclasse immutabile che fornisce un'interfaccia simile a quella di Haskell Data.DList.


4

Devi guardare questo fantastico video del 2015 React conf del creatore di Immutable.js Lee Byron. Ti fornirà puntatori e strutture per capire come implementare un elenco immutabile efficiente che non duplica il contenuto. Le idee di base sono: - fintanto che due elenchi utilizzano nodi identici (stesso valore, stesso nodo successivo), viene utilizzato lo stesso nodo - quando gli elenchi iniziano a differire, viene creata una struttura nel nodo della divergenza, che contiene puntatori al prossimo nodo particolare di ciascun elenco

L'immagine di un tutorial di reazione potrebbe essere più chiara del mio inglese non funzionante:inserisci qui la descrizione dell'immagine


2

Non è strettamente Java, ma ti suggerisco di leggere questo articolo su una struttura di dati persistente indicizzata immutabile, ma performante scritta in Scala:

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

Poiché è una struttura di dati Scala, può essere utilizzata anche da Java (con un po 'più di verbosità). È basato su una struttura di dati disponibile in Clojure e sono sicuro che ci sia anche una libreria Java "nativa" che offre.

Inoltre, una nota sulla costruzione di strutture di dati immutabili: ciò che di solito si desidera è una sorta di "Builder", che consente di "mutare" la struttura di dati "in costruzione" aggiungendo elementi ad essa (all'interno di un singolo thread); dopo aver completato l'aggiunta, si chiama un metodo sull'oggetto "in costruzione" come .build()o .result(), che "costruisce" l'oggetto, offrendo una struttura di dati immutabile che è possibile condividere in modo sicuro.


1
C'è una domanda sulle porte Java di PersistentVector a stackoverflow.com/questions/15997996/...
James_pic

1

Un approccio che a volte può essere utile sarebbe quello di avere due classi di oggetti che mantengono la lista: un oggetto della lista diretta con un finalriferimento al primo nodo in una lista collegata in avanti e un non finalriferimento inizialmente nullo che (quando non -null) identifica un oggetto elenco inverso che contiene gli stessi elementi nell'ordine opposto e un oggetto elenco inverso con un finalriferimento all'ultimo elemento di un elenco con collegamento inverso e un riferimento non finale inizialmente nullo che (quando non -null) identifica un oggetto dell'elenco forward che contiene gli stessi elementi in ordine opposto.

Preparare un elemento a una lista in avanti o aggiungere un elemento a una lista inversa richiederebbe semplicemente la creazione di un nuovo nodo che si collega al nodo identificato dal finalriferimento e la creazione di un nuovo oggetto elenco dello stesso tipo dell'originale, con un finalriferimento a quel nuovo nodo.

L'aggiunta di un elemento a una lista in avanti o di una lista inversa richiederebbe un elenco di tipo opposto; la prima volta che si fa con un particolare elenco, si dovrebbe creare un nuovo oggetto di tipo opposto e memorizzare un riferimento; la ripetizione dell'azione dovrebbe riutilizzare l'elenco.

Si noti che lo stato esterno di un oggetto elenco verrà considerato uguale se il riferimento a un elenco di tipo opposto è nullo o identifica un elenco di ordine opposto. Non sarebbe necessario creare alcuna variabile final, anche quando si utilizza il codice multi-thread, poiché ogni oggetto elenco avrebbe un finalriferimento a una copia completa del suo contenuto.

Se il codice su un thread crea e memorizza nella cache una copia invertita di un elenco e il campo in cui tale copia viene memorizzata nella cache non è volatile, è possibile che il codice su un altro thread non visualizzi l'elenco memorizzato nella cache, ma l'unico effetto negativo da ciò sarebbe che l'altro thread avrebbe bisogno di fare un lavoro extra costruendo un'altra copia rovesciata dell'elenco. Poiché un'azione di questo tipo peggiorerebbe l'efficienza, ma non influirebbe sulla correttezza e poiché le volatilevariabili creano di per sé una riduzione dell'efficienza, spesso sarà meglio avere la variabile non volatile e accettare la possibilità di operazioni ridondanti occasionali.

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.