Le liste collegate devono sempre avere un puntatore di coda?


11

La mia comprensione...

vantaggi:

  • L'inserimento alla fine è O (1) invece di O (N).
  • Se l'elenco è un elenco doppiamente collegato, la rimozione dalla fine è anche O (1) anziché O (N).

Svantaggio:

  • Occupa una quantità banale di memoria aggiuntiva: 4-8 byte .
  • L'implementatore deve tenere traccia della coda.

Guardando questi vantaggi e svantaggi, non riesco a capire perché un elenco collegato eviterebbe mai di usare un puntatore di coda. C'è qualcosa che mi manca?


1
un puntatore di coda è di 4-8 byte (a seconda del sistema a 32 o 64 bit)
maniaco del cricchetto

1
Sembra che tu l'abbia già riassunto.
Robert Harvey,

@RobertHarvey Sto studiando le strutture di dati in questo momento e non sono consapevole di quali siano le migliori pratiche. Quindi quello che ho scritto sono le mie impressioni, ma quello che sto chiedendo è se sono corrette. Ma grazie per il chiarimento!
Adam Zerner,

7
Le "migliori pratiche" sono gli oppiacei delle masse . Festeggia il fatto che hai ancora la capacità di pensare da solo.
Robert Harvey,

Grazie per il link @RobertHarvey - Adoro questo punto! Ho sicuramente adottato un approccio costi-benefici che esamina i dettagli della situazione.
Adam Zerner,

Risposte:


7

Hai ragione, un puntatore di coda non fa mai male e può solo aiutare. Tuttavia, c'è una situazione in cui non è necessario un puntatore di coda.

Se si sta usando un elenco collegato per implementare uno stack, non è necessario un puntatore di coda perché si può garantire che tutti gli accessi, inserimenti e rimozioni avvengano in testa. Detto questo, si potrebbe usare comunque un elenco doppiamente collegato con un puntatore di coda perché questa è l'implementazione standard in una libreria o piattaforma e la memoria è economica, ma non ne è necessaria .


9

Le liste collegate sono molto persistenti e immutabili. In effetti, nei linguaggi di programmazione funzionale, questo utilizzo è onnipresente. I puntatori di coda infrangono entrambe queste proprietà. Tuttavia, se non ti interessa l'immutabilità o la persistenza, ci sono pochi svantaggi nell'includere un puntatore di coda.


3
Ti dispiacerebbe spiegare perché rompono la persistenza e l'immutabilità?
Adam Zerner,

Si prega di aggiungere preoccupazione per la cache
Basilevs

Guarda il mio esempio da questa domanda . Se lavori solo dalla testa dell'elenco ed è immutabile, puoi condividere la coda. Se si utilizza un puntatore di coda, non è possibile utilizzare questa tecnica per condividere e mantenere l'immutabilità.
Karl Bielefeldt,

In realtà con l'immutabilità un puntatore di coda è quasi inutile perché l'unica cosa che puoi fare è vedere quale sia l'ultimo elemento. Tutto il resto deve funzionare dalla testa.
maniaco del cricchetto,

0

Raramente uso un puntatore di coda per gli elenchi collegati e tendo ad usare gli elenchi collegati singolarmente più spesso laddove è sufficiente un modello push / pop di inserimento e rimozione (o solo rimozione lineare al centro) simile a uno stack. È perché nei miei casi d'uso comuni, il puntatore di coda è in realtà costoso, così come rendere costoso l'elenco a link singolo in un elenco a doppio link è costoso.

Spesso l'utilizzo del mio caso comune per un elenco collegato singolarmente potrebbe memorizzare centinaia di migliaia di elenchi collegati che contengono solo pochi nodi elenco ciascuno. Inoltre, generalmente non utilizzo i puntatori per gli elenchi collegati. Uso invece gli indici in un array poiché gli indici possono essere a 32 bit, ad esempio occupando metà dello spazio di un puntatore a 64 bit. Inoltre, in genere non allocare i nodi di elenco uno alla volta e invece, ancora una volta, utilizzo semplicemente un array grande per archiviare tutti i nodi e quindi utilizzare gli indici a 32 bit per collegare i nodi insieme.

Ad esempio, immagina un videogioco che utilizza una griglia 400x400 per partizionare un milione di particelle che si muovono e rimbalzano l'una dall'altra per accelerare il rilevamento delle collisioni. In tal caso, un modo piuttosto efficiente per memorizzare è quello di memorizzare 160.000 elenchi collegati singolarmente, che nel mio caso si traducono in 160.000 numeri interi a 32 bit (~ 640 kilobyte) e un overhead intero a 32 bit per particella. Ora che le particelle si muovono sullo schermo, tutto ciò che dobbiamo fare è aggiornare alcuni numeri interi a 32 bit per spostare una particella da una cella all'altra, in questo modo:

inserisci qui la descrizione dell'immagine

... con l' nextindice ("puntatore") di un nodo particellare che funge da indice per la particella successiva nella cella o per la successiva particella libera da recuperare se la particella è morta (sostanzialmente un'implementazione dell'allocatore di elenchi liberi che utilizza indici):

inserisci qui la descrizione dell'immagine

La rimozione del tempo lineare da una cella non è in realtà un sovraccarico poiché stiamo elaborando la logica delle particelle iterando attraverso le particelle in una cella, quindi un elenco doppiamente collegato aggiungerebbe semplicemente un sovraccarico di un tipo che non è vantaggioso in tutto nel mio caso, proprio come una coda non mi gioverebbe affatto.

Un puntatore di coda raddoppierebbe l'utilizzo della memoria della griglia e aumenterebbe il numero di errori nella cache. Richiede inoltre l'inserimento per richiedere un ramo per verificare se l'elenco è vuoto invece di essere senza ramo. Rendendolo un elenco doppiamente collegato raddoppierebbe il sovraccarico dell'elenco di ogni particella. Il 90% delle volte che uso elenchi collegati, è per casi come questi, e quindi un puntatore di coda sarebbe in realtà relativamente costoso da memorizzare.

Quindi 4-8 byte in realtà non sono banali nella maggior parte dei contesti in cui uso gli elenchi collegati in primo luogo. Volevo solo fare un chip lì perché se si utilizza una struttura di dati per archiviare un carico di elementi, allora 4-8 byte potrebbero non essere sempre così trascurabili. In realtà utilizzo elenchi collegati per ridurre il numero di allocazioni di memoria e la quantità di memoria richiesta rispetto, ad esempio, alla memorizzazione di 160.000 array dinamici che crescono per la griglia che avrebbe un uso di memoria esplosiva (in genere un puntatore più due interi almeno per cella della griglia insieme alle allocazioni di heap per cella della griglia invece di un solo numero intero e zero allocazioni di heap per cella).

Trovo spesso molte persone che cercano elenchi collegati per la loro complessità a tempo costante associata alla rimozione frontale / centrale e all'inserimento frontale / centrale quando le LL sono spesso una cattiva scelta in quei casi a causa della loro generale mancanza di contiguità. Dove le LL sono belle per me dal punto di vista delle prestazioni è la possibilità di spostare un elemento da un elenco all'altro semplicemente manipolando alcuni puntatori e riuscendo a ottenere una struttura di dati di dimensioni variabili senza un allocatore di memoria di dimensioni variabili (poiché ogni nodo ha una dimensione uniforme, possiamo usare liste libere, ad es.). Se ogni nodo dell'elenco viene allocato individualmente rispetto a un allocatore generico, di solito è quando gli elenchi collegati sono molto peggio rispetto alle alternative, ed è "

Suggerirei invece che per la maggior parte dei casi in cui gli elenchi collegati fungono da ottimizzazione molto efficace rispetto alle alternative semplici, i moduli più utili sono generalmente collegati singolarmente, richiedono solo un puntatore principale e non richiedono un'allocazione di memoria per scopi generici per nodo e può invece spesso solo mettere in comune la memoria già allocata per nodo (da un grande array già allocato in anticipo, ad es.). Inoltre, ogni SLL generalmente memorizzava un numero molto piccolo di elementi in quei casi, come bordi collegati a un nodo grafico (molti piccoli elenchi collegati invece di un enorme elenco collegato).

Vale anche la pena ricordare che al giorno d'oggi abbiamo un carico di DRAM, ma questo è il secondo tipo di memoria più lento disponibile. Siamo ancora a qualcosa come 64 KB per core quando si tratta della cache L1 con linee di cache a 64 byte. Di conseguenza, quei piccoli risparmi di byte possono davvero importare in un'area critica per le prestazioni come la sim di particelle sopra se moltiplicata per milioni di volte se ciò significa la differenza tra l'archiviazione del doppio di nodi in una linea di cache o meno, ad es.

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.