In quali circostanze sono utili gli elenchi collegati?


110

La maggior parte delle volte vedo persone che cercano di utilizzare elenchi collegati, mi sembra una scelta povera (o molto scarsa). Forse sarebbe utile esplorare le circostanze in cui un elenco collegato è o non è una buona scelta di struttura dati.

Idealmente, le risposte esporrebbero i criteri da utilizzare nella selezione di una struttura di dati e quali strutture di dati potrebbero funzionare meglio in circostanze specifiche.

Modifica: devo dire che sono abbastanza impressionato non solo dal numero, ma anche dalla qualità delle risposte. Posso accettarne solo uno, ma ce ne sono altri due o tre che avrei dovuto dire che varrebbe la pena accettare se non ci fosse stato qualcosa di un po 'meglio. Solo una coppia (specialmente quella che ho finito per accettare) ha indicato situazioni in cui una lista collegata forniva un vero vantaggio. Penso che Steve Jessop meriti una sorta di menzione d'onore per aver fornito non solo una, ma tre diverse risposte, che ho trovato abbastanza impressionanti. Ovviamente, anche se è stato pubblicato solo come commento, non come risposta, penso che valga la pena leggere anche il post del blog di Neil - non solo informativo, ma anche piuttosto divertente.


34
La risposta al secondo paragrafo richiede circa un semestre.
Seva Alekseyev

2
Per la mia opinione, vedere punchlet.wordpress.com/2009/12/27/letter-the-fourth . E poiché questo sembra essere un sondaggio, probabilmente dovrebbe essere CW.

1
@ Neil, gentile, anche se dubito che CS Lewis approverebbe.
Tom

@ Neil: immagino una specie di sondaggio. Per lo più è un tentativo di vedere se qualcuno può trovare una risposta che abbia una base che potrei almeno acquistare come ragionevole. @Seva: si, rileggendolo, ho reso l'ultima frase un po 'più generale di quanto intendessi originariamente.
Jerry Coffin

2
Le persone @Yar (incluso me, mi dispiace dirlo) erano solite implementare elenchi collegati senza puntatori in linguaggi come FORTRAN IV (che non avevano la nozione di puntatori), proprio come facevano gli alberi. Hai usato gli array invece della memoria "reale".

Risposte:


40

Possono essere utili per strutture dati concorrenti. (Di seguito è disponibile un esempio di utilizzo nel mondo reale non simultaneo: non sarebbe presente se @Neil non avesse menzionato FORTRAN. ;-)

Ad esempio, ConcurrentDictionary<TKey, TValue>in .NET 4.0 RC utilizzare elenchi collegati per concatenare elementi che hanno hash nello stesso bucket.

La struttura dati sottostante per ConcurrentStack<T>è anche un elenco collegato.

ConcurrentStack<T>è una delle strutture dati che servono come base per il nuovo Thread Pool , (con le "code" locali implementate come stack, essenzialmente). (L'altra struttura portante principale è ConcurrentQueue<T>.)

Il nuovo Thread Pool fornisce a sua volta la base per la pianificazione del lavoro della nuova Task Parallel Library .

Quindi possono certamente essere utili: un elenco collegato funge attualmente da una delle principali strutture di supporto di almeno una grande nuova tecnologia.

(Un elenco collegato singolarmente rende una scelta convincente senza blocchi, ma non senza attese, in questi casi, perché le operazioni principali possono essere eseguite con un singolo CAS (+ tentativi). In un ambiente GC-d moderno, come Java e .NET: il problema ABA può essere facilmente evitato. Racchiudi semplicemente gli elementi che aggiungi nei nodi appena creati e non riutilizzarli: lascia che il GC faccia il suo lavoro. La pagina sul problema ABA fornisce anche l'implementazione di un blocco stack gratuito: funziona effettivamente in .Net (e Java) con un nodo (GC-ed) che contiene gli elementi.)

Modifica : @ Neil: in realtà, ciò che hai menzionato su FORTRAN mi ha ricordato che lo stesso tipo di elenchi collegati può essere trovato probabilmente nella struttura dati più utilizzata e abusata in .NET: il semplice .NET generico Dictionary<TKey, TValue>.

Non uno, ma molti elenchi collegati vengono memorizzati in un array.

  • Evita di eseguire molte piccole (de) allocazioni su inserimenti / eliminazioni.
  • Il caricamento iniziale della tabella hash è piuttosto veloce, perché l'array viene riempito in sequenza (funziona molto bene con la cache della CPU).
  • Per non parlare del fatto che una tabella hash concatenata è costosa in termini di memoria - e questo "trucco" dimezza le "dimensioni del puntatore" su x64.

In sostanza, molti elenchi collegati sono archiviati in una matrice. (uno per ogni bucket utilizzato.) Un elenco libero di nodi riutilizzabili è "intrecciato" tra di loro (se ci sono stati eliminazioni). Un array viene allocato all'inizio / durante il rehash e i nodi delle catene vengono mantenuti in esso. C'è anche un puntatore libero - un indice nell'array - che segue le eliminazioni. ;-) Quindi, che ci crediate o no, la tecnica FORTRAN è ancora viva. (... e da nessun'altra parte, se non in una delle strutture dati .NET più comunemente usate ;-).


2
Nel caso te lo fossi perso, ecco il commento di Neil: "Le persone (incluso me, mi dispiace dirlo) erano solite implementare elenchi concatenati senza puntatori in linguaggi come FORTRAN IV (che non avevano la nozione di puntatori), proprio come facevano gli alberi Hai usato gli array invece della "vera" memoria. "
Andras Vass

Dovrei aggiungere che l'approccio "liste collegate in un array" in caso di Dictionaryrisparmi molto di più in .NET: altrimenti ogni nodo richiederebbe un oggetto separato sull'heap - e ogni oggetto allocato sull'heap avrebbe un sovraccarico. ( en.csharp-online.net/Common_Type_System%E2%80%94Object_Layout )
Andras Vass

È anche utile sapere che l'impostazione predefinita di C ++ std::listnon è sicura in un contesto multithread senza blocchi.
Mooing Duck

50

Gli elenchi collegati sono molto utili quando è necessario eseguire molti inserimenti e rimozioni, ma non troppe ricerche, su un elenco di lunghezza arbitraria (sconosciuta in fase di compilazione).

La divisione e l'unione di elenchi (collegati in modo bidirezionale) è molto efficiente.

Puoi anche combinare elenchi collegati - ad esempio, le strutture ad albero possono essere implementate come elenchi collegati "verticali" (relazioni padre / figlio) che collegano insieme elenchi collegati orizzontali (fratelli).

L'utilizzo di un elenco basato su array per questi scopi presenta gravi limitazioni:

  • L'aggiunta di un nuovo elemento significa che l'array deve essere riallocato (oppure è necessario allocare più spazio del necessario per consentire la crescita futura e ridurre il numero di riallocazioni)
  • La rimozione degli elementi lascia spazio sprecato o richiede una riallocazione
  • l'inserimento di elementi ovunque tranne che alla fine implica (possibilmente riallocare e) copiare molti dati di una posizione

5
Quindi la domanda si riduce a, quando fare quello che devi fare un sacco di inserimenti e rimozioni a metà di una sequenza, ma non molte ricerche nella lista per numero ordinale? Attraversare un elenco collegato è in genere più costoso della copia di un array, quindi tutto ciò che dici sulla rimozione e l'inserimento di elementi negli array è altrettanto negativo per l'accesso casuale negli elenchi. La cache LRU è un esempio a cui riesco a pensare, devi rimuovere molto al centro, ma non devi mai camminare nell'elenco.
Steve Jessop

2
L'aggiunta a un elenco implica l'allocazione di memoria per ogni elemento aggiunto. Ciò potrebbe comportare una chiamata di sistema che sarà molto costosa. L'aggiunta a un array richiede tale chiamata solo se l'array deve essere ingrandito. In effetti, nella maggior parte delle lingue (esattamente per questi motivi) l'array è la struttura dati preferita e gli elenchi sono usati a malapena.

1
Supponi quale? Che l'allocazione sia sorprendentemente veloce è evidente - di solito richiede l'aggiunta della dimensione dell'oggetto a un puntatore. Il sovraccarico totale per GC è basso? L'ultima volta che ho provato a misurarlo su un'app reale, il punto chiave era che Java stava facendo tutto il lavoro quando il processore era comunque inattivo, quindi naturalmente non ha influito molto sulle prestazioni visibili. In un benchmark con CPU occupata è stato facile sconvolgere Java e ottenere tempi di allocazione nel caso peggiore. Questo è accaduto molti anni fa, tuttavia, e da allora la raccolta dei rifiuti generazionali ha ridotto notevolmente il costo totale di GC.
Steve Jessop

1
@Steve: Ti sbagli sul fatto che l'allocazione sia "la stessa" tra elenchi e array. Ogni volta che è necessario allocare memoria per un elenco, allocare semplicemente un piccolo blocco - O (1). Per un array è necessario allocare un nuovo blocco abbastanza grande per l'intero elenco, quindi copiare l'intero elenco - O (n). Per inserire in una posizione nota in un elenco si aggiorna un numero fisso di puntatori - O (1), ma per inserire in un array e copiare gli elementi successivi di una posizione per fare spazio per l'inserimento - O (n). Ci sono molti casi in cui gli array sono quindi molto meno efficienti degli LL.
Jason Williams

1
@ Jerry: ho capito. Il punto è che gran parte del costo della riallocazione dell'array non è l' allocazione della memoria , è la necessità di copiare l'intero contenuto dell'array nella nuova memoria. Per inserire nell'elemento 0 di un array è necessario copiare l'intero contenuto dell'array di una posizione nella memoria. Non sto dicendo che gli array sono cattivi; solo che ci sono situazioni in cui l'accesso casuale non è necessario e dove l'inserimento / eliminazione / ricollegamento a tempo costante degli LL è preferibile.
Jason Williams

20

Gli elenchi collegati sono molto flessibili: con la modifica di un puntatore, è possibile apportare un cambiamento enorme, in cui la stessa operazione sarebbe molto inefficiente in un elenco di array.


Sarebbe possibile motivare perché usare un elenco e non un set o una mappa?
patrik

14

Gli array sono le strutture di dati con cui vengono solitamente confrontati gli elenchi collegati.

Normalmente gli elenchi concatenati sono utili quando è necessario apportare molte modifiche all'elenco stesso mentre gli array funzionano meglio degli elenchi sull'accesso diretto agli elementi.

Di seguito è riportato un elenco di operazioni che possono essere eseguite su elenchi e array, confrontato con il relativo costo dell'operazione (n = lunghezza elenco / array):

  • Aggiunta di un elemento:
    • sugli elenchi è sufficiente allocare memoria per il nuovo elemento e reindirizzare i puntatori. O (1)
    • sugli array è necessario riposizionare l'array. Sopra)
  • Rimozione di un elemento
    • sugli elenchi reindirizza solo i puntatori. O (1).
    • sugli array si spende O (n) tempo per riposizionare l'array se l'elemento da rimuovere non è il primo o l'ultimo elemento dell'array; altrimenti puoi semplicemente riposizionare il puntatore all'inizio della matrice o diminuire la lunghezza della matrice
  • Ottenere un elemento in una posizione nota:
    • sulle liste devi camminare nella lista dal primo elemento all'elemento nella posizione specifica. Caso peggiore: O (n)
    • sugli array è possibile accedere immediatamente all'elemento. O (1)

Questo è un confronto di livello molto basso di queste due strutture dati popolari e di base e puoi vedere che gli elenchi funzionano meglio in situazioni in cui devi apportare molte modifiche all'elenco stesso (rimuovendo o aggiungendo elementi). D'altra parte gli array funzionano meglio degli elenchi quando devi accedere direttamente agli elementi dell'array.

Dal punto di vista dell'allocazione della memoria, le liste sono migliori perché non c'è bisogno di avere tutti gli elementi uno accanto all'altro. D'altra parte c'è il (piccolo) sovraccarico di memorizzare i puntatori all'elemento successivo (o anche al precedente).

Conoscere queste differenze è importante per gli sviluppatori per scegliere tra elenchi e array nelle loro implementazioni.

Notare che questo è un confronto di elenchi e array. Ci sono buone soluzioni ai problemi qui riportati (es: SkipLists, Dynamic Arrays, ecc ...). In questa risposta ho preso in considerazione la struttura dei dati di base che ogni programmatore dovrebbe conoscere.


Questo è in qualche modo vero per una buona implementazione di liste e una pessima implementazione di array. La maggior parte delle implementazioni di array sono molto più sofisticate di quanto tu dia loro credito. E non credo che tu capisca quanto possa essere costosa l'allocazione dinamica della memoria.

Questa risposta non dovrebbe coprire il programma di un corso di Data Structures University. Questo è un confronto scritto tenendo conto degli elenchi e degli array collegati, che sono implementati nel modo in cui tu, io e la maggior parte delle persone sappiamo. Array che si espandono geometricamente, Skip Lists, ecc ... sono soluzioni che conosco, utilizzo e studio ma che richiederebbero una spiegazione più approfondita e che non si adatterebbero a una risposta di stackoverflow.
Andrea Zilio

1
"Dal punto di vista dell'allocazione della memoria, gli elenchi sono migliori perché non è necessario avere tutti gli elementi uno accanto all'altro". Al contrario, i contenitori contigui sono migliori perché tengono gli elementi uno accanto all'altro. Sui computer moderni, la località dei dati è il re. Tutto ciò che salta in giro nella memoria uccide le prestazioni della cache e porta a programmi che inseriscono un elemento in una posizione (effettivamente) casuale che si comporta più velocemente con un array dinamico come un C ++ std::vectorche con un elenco collegato come un C ++ std::list, semplicemente perché attraversando il l'elenco è così costoso.
David Stone

@DavidStone Forse non sono stato abbastanza chiaro, ma con quella frase mi riferivo al fatto che non è necessario disporre di spazio contiguo per memorizzare i tuoi elementi. In particolare, se vuoi memorizzare qualcosa di non troppo piccolo e hai una memoria disponibile limitata potresti non avere abbastanza spazio libero contiguo per memorizzare i tuoi dati, ma probabilmente puoi adattare i tuoi dati usando un elenco (anche se avrai il sovraccarico dei puntatori ... sia per lo spazio che occupano sia per i problemi di prestazioni che hai citato). Probabilmente dovrei aggiornare la mia risposta per renderla più chiara.
Andrea Zilio

4

L'elenco a collegamento singolo è una buona scelta per l'elenco libero in un allocatore di celle o pool di oggetti:

  1. Hai solo bisogno di uno stack, quindi è sufficiente un elenco collegato singolarmente.
  2. Tutto è già diviso in nodi. Non vi è alcun sovraccarico di allocazione per un nodo elenco intrusivo, a condizione che le celle siano abbastanza grandi da contenere un puntatore.
  3. Un vettore o una deque imporrebbe un overhead di un puntatore per blocco. Questo è significativo dato che quando crei per la prima volta l'heap, tutte le celle sono gratuite, quindi è un costo iniziale. Nel peggiore dei casi raddoppia il fabbisogno di memoria per cella.

Bene, d'accordo. Ma quanti programmatori stanno effettivamente creando cose del genere? La maggior parte sta semplicemente reimplementando ciò che std :: list ecc. Ti danno. E in realtà "invadente" normalmente ha un significato leggermente diverso da quello che gli hai dato - che ogni possibile elemento della lista contiene un puntatore separato dai dati.

1
Quanti? Più di 0, meno di un milione ;-) La domanda di Jerry era "fai un buon uso delle liste", o "dai buoni usi delle liste che ogni programmatore usa quotidianamente", o qualcosa di intermedio? Non conosco nessun altro nome oltre a "intrusivo" per un nodo della lista che è contenuto all'interno dell'oggetto che è un elemento della lista - sia come parte di un'unione (in termini C) o meno. Il punto 3 si applica solo ai linguaggi che ti consentono di farlo: C, C ++, assemblatore buono. Java cattivo.
Steve Jessop

4

L'elenco a doppio collegamento è una buona scelta per definire l'ordine di una hashmap che definisce anche un ordine sugli elementi (LinkedHashMap in Java), specialmente se ordinato dall'ultimo accesso:

  1. Più overhead di memoria rispetto a un vettore o deque associato (2 puntatori invece di 1), ma migliori prestazioni di inserimento / rimozione.
  2. Nessun overhead di allocazione, poiché è comunque necessario un nodo per una voce hash.
  3. La località di riferimento non è un problema aggiuntivo rispetto a un vettore o una deque di puntatori, poiché dovresti estrarre ogni oggetto in memoria in entrambi i casi.

Certo, puoi discutere se una cache LRU sia una buona idea in primo luogo, rispetto a qualcosa di più sofisticato e sintonizzabile, ma se ne avrai uno, questa è un'implementazione abbastanza decente. Non si desidera eseguire un'eliminazione dal centro e l'aggiunta alla fine su un vettore o una rimozione dell'accumulo a ogni accesso in lettura, ma in genere lo spostamento di un nodo in coda va bene.


4

Gli elenchi collegati sono una delle scelte naturali quando non è possibile controllare la posizione di archiviazione dei dati, ma è comunque necessario passare in qualche modo da un oggetto all'altro.

Ad esempio, quando si implementa il rilevamento della memoria in C ++ (sostituzione nuova / eliminazione) è necessaria una struttura di dati di controllo che tenga traccia di quali puntatori sono stati liberati, che è necessario implementare completamente. L'alternativa è sovrallocazione e aggiunta di un elenco collegato all'inizio di ogni blocco di dati.

Poiché sai sempre immediatamente dove ti trovi nella lista quando viene chiamata la cancellazione, puoi facilmente rinunciare alla memoria in O (1). Anche l'aggiunta di un nuovo blocco che è stato appena mallocato è in O (1). In questo caso, camminare nell'elenco è molto raramente necessario, quindi il costo O (n) non è un problema qui (camminare su una struttura è comunque O (n)).


3

Sono utili quando hai bisogno di spingere, far scoppiare e ruotare ad alta velocità e non ti preoccupare dell'indicizzazione O (n).


Ti sei mai preoccupato di cronometrare gli elenchi collegati C ++ rispetto a (diciamo) un deque?

@ Neil: non posso dire di averlo fatto.
Ignacio Vazquez-Abrams

@ Neil: se C ++ ha deliberatamente sabotato la sua classe di elenco collegato per renderlo più lento di qualsiasi altro contenitore (il che non è lontano dalla verità), cosa c'entra questo con una domanda indipendente dal linguaggio? Un elenco collegato intrusivo è ancora un elenco collegato.
Steve Jessop

@Steve C ++ è un linguaggio. Non riesco a vedere come possa avere volontà. Se stai suggerendo che i membri del Comitato C ++ abbiano in qualche modo sabotato le liste concatenate (che devono essere logicamente lente per molte operazioni), allora dai un nome ai colpevoli!

3
Non è un vero sabotaggio: i nodi di elenchi esterni hanno i loro vantaggi, ma le prestazioni non sono uno di questi. Tuttavia, sicuramente tutti erano consapevoli quando hanno fatto il compromesso della stessa cosa di cui sei consapevole, che è piuttosto difficile trovare un buon uso per std::list. Un elenco intrusivo semplicemente non si adatta alla filosofia C ++ dei requisiti minimi sugli elementi contenitore.
Steve Jessop

3

Gli elenchi collegati singolarmente sono l'ovvia implementazione del tipo di dati "elenco" comune nei linguaggi di programmazione funzionale:

  1. L'aggiunta alla testa è veloce (append (list x) (L))e (append (list y) (L))può condividere quasi tutti i dati. Non è necessario il copy-on-write in una lingua senza scritture. I programmatori funzionali sanno come trarne vantaggio.
  2. L'aggiunta alla coda è purtroppo lenta, ma lo sarebbe anche qualsiasi altra implementazione.

In confronto, un vettore o una deque sarebbero tipicamente lenti da aggiungere a entrambe le estremità, richiedendo (almeno nel mio esempio di due distinte aggiunte) che venga presa una copia dell'intero elenco (vettore), o del blocco indice e del blocco dati essere aggiunto a (deque). In realtà, potrebbe esserci qualcosa da dire lì per deque su elenchi di grandi dimensioni che devono essere aggiunti alla coda per qualche motivo, non sono sufficientemente informato sulla programmazione funzionale per giudicare.


3

Un esempio di buon utilizzo di un elenco collegato è dove gli elementi dell'elenco sono molto grandi, ad es. abbastanza grande che solo uno o due possono entrare nella cache della CPU allo stesso tempo. A questo punto il vantaggio che hanno i contenitori di blocchi contigui come vettori o matrici per l'iterazione è più o meno annullato, e un vantaggio in termini di prestazioni può essere possibile se molti inserimenti e rimozioni si verificano in tempo reale.


2

Dalla mia esperienza, implementando matrici sparse e cumuli di Fibonacci. Gli elenchi collegati offrono un maggiore controllo sulla struttura complessiva di tali strutture di dati. Anche se non sono sicuro che le matrici sparse siano implementate al meglio utilizzando elenchi collegati, probabilmente esiste un modo migliore, ma ha davvero aiutato ad apprendere i dettagli delle matrici sparse utilizzando elenchi collegati in CS undergrad :)


1

Ci sono due operazioni complementari che sono banalmente O (1) sugli elenchi e molto difficili da implementare in O (1) in altre strutture di dati: rimuovere e inserire un elemento da una posizione arbitraria, assumendo che sia necessario mantenere l'ordine degli elementi.

Le mappe hash possono ovviamente eseguire l'inserimento e l'eliminazione in O (1) ma non è possibile iterare sugli elementi in ordine.

Dato il fatto di cui sopra, la mappa hash può essere combinata con un elenco collegato per creare un'elegante cache LRU: una mappa che memorizza un numero fisso di coppie chiave-valore e rilascia la chiave a cui si è effettuato meno di recente per fare spazio a nuove.

Le voci nella mappa hash devono avere puntatori ai nodi dell'elenco collegato. Quando si accede alla mappa hash, il nodo dell'elenco collegato viene scollegato dalla sua posizione corrente e spostato all'inizio dell'elenco (O (1), yay per gli elenchi collegati!). Quando è necessario rimuovere l'elemento utilizzato meno di recente, quello dalla coda dell'elenco deve essere rilasciato (di nuovo O (1) assumendo che si mantenga il puntatore al nodo della coda) insieme alla voce della mappa hash associata (quindi i backlink da l'elenco per la mappa hash sono necessari.)


1

Considera che un elenco collegato potrebbe essere molto utile in un'implementazione in stile Domain Driven Design di un sistema che include parti che si sincronizzano con la ripetizione.

Un esempio che viene in mente potrebbe essere se dovessi modellare una catena sospesa. Se volessi sapere quale fosse la tensione su un particolare collegamento, la tua interfaccia potrebbe includere un getter per il peso "apparente". L'implementazione del quale includerebbe un collegamento che chiede al collegamento successivo il suo peso apparente, aggiungendo quindi il proprio peso al risultato. In questo modo, l'intera lunghezza fino al fondo verrebbe valutata con una singola chiamata dal client della catena.

Essendo un sostenitore del codice che si legge come il linguaggio naturale, mi piace come questo permetterebbe al programmatore di chiedere a un anello di catena quanto peso sta portando. Mantiene anche la preoccupazione di calcolare questi figli di proprietà entro i confini dell'implementazione del collegamento, eliminando la necessità di un servizio di calcolo del peso della catena ".


1

Uno dei casi più utili che trovo per gli elenchi collegati che lavorano in campi critici per le prestazioni come l'elaborazione di mesh e immagini, motori fisici e raytracing è quando l'utilizzo di elenchi collegati migliora effettivamente la località di riferimento e riduce le allocazioni di heap e talvolta riduce persino l'uso della memoria rispetto a le alternative semplici.

Ora può sembrare un ossimoro completo che le liste collegate potrebbero fare tutto ciò poiché sono famose per fare spesso il contrario, ma hanno una proprietà unica in quanto ogni nodo della lista ha una dimensione fissa e requisiti di allineamento che possiamo sfruttare per consentire per essere immagazzinati in modo contiguo e rimossi in tempo costante in modi che le cose di dimensioni variabili non possono.

Di conseguenza, prendiamo un caso in cui vogliamo fare l'equivalente analogico di memorizzare una sequenza di lunghezza variabile che contiene un milione di sotto-sequenze di lunghezza variabile annidate. Un esempio concreto è una mesh indicizzata che memorizza un milione di poligoni (alcuni triangoli, alcuni quad, alcuni pentagoni, alcuni esagoni, ecc.) Ea volte i poligoni vengono rimossi da qualsiasi punto della mesh e talvolta i poligoni vengono ricostruiti per inserire un vertice in un poligono esistente o rimuoverne uno. In tal caso, se memorizziamo un milione di minuscoli std::vectors, finiamo per affrontare un'allocazione di heap per ogni singolo vettore e un utilizzo della memoria potenzialmente esplosivo. Un milione di minuscoli SmallVectorspotrebbe non soffrire di questo problema tanto nei casi comuni, ma il loro buffer preallocato che non è allocato separatamente nell'heap potrebbe comunque causare un uso esplosivo della memoria.

Il problema qui è che un milione di std::vectoristanze tenterebbe di memorizzare un milione di cose di lunghezza variabile. Le cose a lunghezza variabile tendono a volere un'allocazione di heap poiché non possono essere archiviate in modo molto efficace in modo contiguo e rimosse a tempo costante (almeno in modo diretto senza un allocatore molto complesso) se non memorizzano il loro contenuto altrove nell'heap.

Se invece facciamo questo:

struct FaceVertex
{
    // Points to next vertex in polygon or -1
    // if we're at the end of the polygon.
    int next;
    ...
};

struct Polygon
{
     // Points to first vertex in polygon.
    int first_vertex;
    ...
};

struct Mesh
{
    // Stores all the face vertices for all polygons.
    std::vector<FaceVertex> fvs;

    // Stores all the polygons.
    std::vector<Polygon> polys;
};

... poi abbiamo ridotto drasticamente il numero di allocazioni di heap e di cache miss. Invece di richiedere un'allocazione di heap e potenziali errori di cache obbligatori per ogni singolo poligono a cui accediamo, ora richiediamo l'allocazione di heap solo quando uno dei due vettori memorizzati nell'intera mesh supera la loro capacità (un costo ammortizzato). E mentre il passo per passare da un vertice al successivo potrebbe ancora causare la sua quota di cache mancate, è ancora spesso minore che se ogni singolo poligono memorizzasse un array dinamico separato poiché i nodi sono memorizzati in modo contiguo e c'è una probabilità che un vertice vicino potrebbe essere accessibile prima dello sfratto (soprattutto considerando che molti poligoni aggiungeranno i loro vertici tutti in una volta, il che rende la maggior parte dei vertici del poligono perfettamente contigui).

Ecco un altro esempio:

inserisci qui la descrizione dell'immagine

... dove le celle della griglia vengono utilizzate per accelerare la collisione particella-particella per, diciamo, 16 milioni di particelle che si muovono ogni singolo fotogramma. In quell'esempio di griglia di particelle, utilizzando elenchi collegati possiamo spostare una particella da una cella della griglia a un'altra semplicemente cambiando 3 indici. La cancellazione da un vettore e il rinvio a un altro può essere notevolmente più costosa e introdurre più allocazioni di heap. Le liste collegate riducono anche la memoria di una cella fino a 32 bit. Un vettore, a seconda dell'implementazione, può preallocare il proprio array dinamico al punto in cui può richiedere 32 byte per un vettore vuoto. Se abbiamo circa un milione di celle della griglia, è una bella differenza.

... ed è qui che trovo gli elenchi collegati più utili in questi giorni, e in particolare trovo utile la varietà "elenco collegato indicizzato" poiché gli indici a 32 bit dimezzano i requisiti di memoria dei collegamenti su macchine a 64 bit e implicano che il i nodi vengono memorizzati in modo contiguo in un array.

Spesso li combino anche con elenchi gratuiti indicizzati per consentire rimozioni e inserimenti a tempo costante ovunque:

inserisci qui la descrizione dell'immagine

In tal caso, l' nextindice punta al successivo indice libero se il nodo è stato rimosso o al successivo indice utilizzato se il nodo non è stato rimosso.

E questo è il caso d'uso numero uno che trovo per gli elenchi collegati in questi giorni. Quando vogliamo memorizzare, diciamo, un milione di sotto-sequenze di lunghezza variabile che media, diciamo, 4 elementi ciascuna (ma a volte con elementi rimossi e aggiunti a una di queste sotto-sequenze), l'elenco collegato ci consente di memorizzare 4 milioni nodi della lista concatenata contigui invece di 1 milione di contenitori che sono ciascuno individualmente allocato nell'heap: un vettore gigante, cioè non un milione di piccoli.


0

Ho utilizzato elenchi collegati (anche elenchi doppiamente collegati) in passato in un'applicazione C / C ++. Questo era prima di .NET e persino di stl.

Probabilmente non utilizzerei un elenco collegato ora in un linguaggio .NET perché tutto il codice di attraversamento necessario viene fornito tramite i metodi di estensione Linq.

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.