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:
... con l' next
indice ("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):
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.