È ora di tornare indietro nel tempo per una lezione. Anche se oggi non pensiamo molto a queste cose nelle nostre fantasiose lingue gestite, sono costruite sulla stessa base, quindi vediamo come viene gestita la memoria in C.
Prima di immergermi, una rapida spiegazione del significato del termine " puntatore ". Un puntatore è semplicemente una variabile che "punta" verso una posizione nella memoria. Non contiene il valore effettivo in questa area di memoria, contiene l'indirizzo di memoria ad esso. Pensa a un blocco di memoria come a una cassetta postale. Il puntatore sarebbe l'indirizzo di quella casella di posta.
In C, un array è semplicemente un puntatore con un offset, l'offset specifica quanto lontano nella memoria guardare. Ciò fornisce il tempo di accesso O (1) .
MyArray [5]
^ ^
Pointer Offset
Tutte le altre strutture di dati si basano su questo o non usano la memoria adiacente per l'archiviazione, con conseguente scarso tempo di ricerca dell'accesso casuale (sebbene ci siano altri vantaggi nel non usare la memoria sequenziale).
Ad esempio, supponiamo di avere un array con 6 numeri (6,4,2,3,1,5), in memoria sarebbe simile a questo:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
In un array, sappiamo che ogni elemento è uno accanto all'altro in memoria. L'array AC (chiamato MyArrayqui) è semplicemente un puntatore al primo elemento:
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray
Se volessimo cercare MyArray[4], internamente si accederà in questo modo:
0 1 2 3 4
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^
MyArray + 4 ---------------/
(Pointer + Offset)
Poiché possiamo accedere direttamente a qualsiasi elemento dell'array aggiungendo l'offset al puntatore, possiamo cercare qualsiasi elemento nello stesso lasso di tempo, indipendentemente dalle dimensioni dell'array. Ciò significa che ottenere MyArray[1000]richiederebbe lo stesso tempo di ottenere MyArray[5].
Una struttura di dati alternativa è un elenco collegato. Questo è un elenco lineare di puntatori, ognuno dei quali punta al nodo successivo
======== ======== ======== ======== ========
| Data | | Data | | Data | | Data | | Data |
| | -> | | -> | | -> | | -> | |
| P1 | | P2 | | P3 | | P4 | | P5 |
======== ======== ======== ======== ========
P(X) stands for Pointer to next node.
Nota che ho trasformato ogni "nodo" nel suo blocco. Questo perché non è garantito che siano (e molto probabilmente non lo saranno) adiacenti in memoria.
Se voglio accedere a P3, non posso accedervi direttamente, perché non so dove sia in memoria. Tutto quello che so è dove si trova la radice (P1), quindi devo iniziare da P1 e seguire ogni puntatore al nodo desiderato.
Questo è un tempo di ricerca O (N) (il costo di ricerca aumenta man mano che viene aggiunto ogni elemento). È molto più costoso arrivare a P1000 rispetto a P4.
Strutture di dati di livello superiore, come hashtable, stack e code, possono utilizzare internamente un array (o più array), mentre gli elenchi collegati e gli alberi binari di solito usano nodi e puntatori.
Potresti chiederti perché qualcuno dovrebbe usare una struttura di dati che richiede l'attraversamento lineare per cercare un valore invece di usare solo un array, ma hanno i loro usi.
Prendi di nuovo il nostro array. Questa volta, voglio trovare l'elemento dell'array che contiene il valore "5".
=====================================
| 6 | 4 | 2 | 3 | 1 | 5 |
=====================================
^ ^ ^ ^ ^ FOUND!
In questa situazione, non so quale offset aggiungere al puntatore per trovarlo, quindi devo iniziare da 0 e risalire fino a quando non lo trovo. Ciò significa che devo eseguire 6 controlli.
Per questo motivo, la ricerca di un valore in un array è considerata O (N). Il costo della ricerca aumenta all'aumentare dell'array.
Ricordi sopra dove ho detto che a volte l'utilizzo di una struttura dati non sequenziale può avere vantaggi? La ricerca di dati è uno di questi vantaggi e uno dei migliori esempi è l'albero binario.
Un albero binario è una struttura di dati simile a un elenco collegato, tuttavia invece di collegarsi a un singolo nodo, ogni nodo può collegarsi a due nodi figlio.
==========
| Root |
==========
/ \
========= =========
| Child | | Child |
========= =========
/ \
========= =========
| Child | | Child |
========= =========
Assume that each connector is really a Pointer
Quando i dati vengono inseriti in un albero binario, usano diverse regole per decidere dove posizionare il nuovo nodo. Il concetto di base è che se il nuovo valore è maggiore dei genitori, lo inserisce a sinistra, se è inferiore, lo inserisce a destra.
Ciò significa che i valori in un albero binario potrebbero apparire così:
==========
| 100 |
==========
/ \
========= =========
| 200 | | 50 |
========= =========
/ \
========= =========
| 75 | | 25 |
========= =========
Quando si cerca un albero binario per il valore di 75, è necessario visitare solo 3 nodi (O (log N)) a causa di questa struttura:
- 75 è inferiore a 100? Guarda il nodo destro
- 75 è maggiore di 50? Guarda il nodo sinistro
- C'è il 75!
Anche se nel nostro albero ci sono 5 nodi, non abbiamo avuto bisogno di guardare i due rimanenti, perché sapevamo che loro (e i loro figli) non potevano contenere il valore che stavamo cercando. Questo ci dà un tempo di ricerca che nel peggiore dei casi significa che dobbiamo visitare tutti i nodi, ma nel migliore dei casi dobbiamo visitare solo una piccola parte dei nodi.
È qui che le matrici vengono battute, forniscono un tempo di ricerca O (N) lineare, nonostante il tempo di accesso O (1).
Questa è una panoramica di livello incredibilmente alto sulle strutture di dati in memoria, saltando molti dettagli, ma si spera che illustri la forza e la debolezza di un array rispetto ad altre strutture di dati.