Gli alberi sono organizzati da una struttura di "primogenito, prossimo"? In caso contrario, perché no?


12

Di solito, le strutture di dati ad albero sono organizzate in modo tale che ciascun nodo contenga puntatori a tutti i suoi figli.

       +-----------------------------------------+
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------------+    +---------------+    +---------------+
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Sembra naturale, ma presenta alcuni problemi. Ad esempio, quando il numero di nodi figlio varia, è necessario qualcosa come una matrice o un elenco per gestire i figli.

Usando invece solo i puntatori (primo) figlio e (successivo) fratello, otteniamo qualcosa del genere:

       +-------------------+
       |        root       |
       | child    sibling  +--->NULL
       +--+----------------+
          |             
+----------------+    +----------------+    +----------------+
|    node1       |    |     node2      |    |     node3      |
| child  sibling +--->| child  sibling +--->| child  sibling +--->NULL
+--+-------------+    +--+-------------+    +--+-------------+
   |                     |                     |

Ovviamente, questo tipo di struttura può rappresentare anche alberi, ma offre anche alcuni vantaggi. La cosa più importante è che non dobbiamo più preoccuparci del numero di nodi figlio. Se utilizzato per un albero di analisi, offre una rappresentazione naturale per un termine come "a + b + c + d + e" senza diventare un albero profondo.

Le librerie di raccolte offrono strutture ad albero del genere? I parser usano una struttura del genere? In caso contrario, quali sono i motivi?


2
Bene, questa struttura ha ovviamente un costo di maggiore complessità. Ne vale la pena solo se in realtà hai bisogno di un numero variabile di bambini. Molti alberi hanno un numero fisso di bambini (o almeno un massimo fisso) insito nella loro progettazione. In questi casi le ulteriori indicazioni indirette non aggiungono alcun valore.
Joachim Sauer

4
Inserire elementi in un elenco collegato introduce un O(n)fattore nell'algoritmo.

E per arrivare a node3 da root avresti bisogno di prendere il cddar di root ...
Tacroy,

Tacroy: Corretto, tornare alla radice non è esattamente facile, ma se ne avessi davvero bisogno, un puntatore posteriore sarebbe approriato (anche se rovinerebbe il diagramma ;-)
user281377

Risposte:


7

Gli alberi, come gli elenchi, sono "tipi di dati astratti" che possono essere implementati in diversi modi. Ogni modo ha i suoi vantaggi e svantaggi.

Nel primo esempio, il vantaggio principale di questa struttura è che puoi accedere a qualsiasi bambino in O (1). Lo svantaggio è che aggiungere un bambino a volte potrebbe essere un po 'più costoso quando l'array deve essere espanso. Questo costo è relativamente piccolo però. È anche una delle implementazioni più semplici.

Nel secondo esempio, il vantaggio principale è che aggiungi sempre un figlio in O (1). Lo svantaggio principale è che l'accesso casuale a un bambino costa O (n). Inoltre, potrebbe essere meno interessante per alberi enormi per due motivi: ha un sovraccarico di memoria di un'intestazione di oggetto e due puntatori per nodo, e i nodi sono sparsi casualmente sulla memoria, il che può causare molti scambi tra la cache della CPU e il memoria quando l'albero viene attraversato, rendendo questa implementazione meno attraente per loro. Tuttavia, questo non è un problema per alberi e applicazioni normali.

Un'ultima possibilità interessante che non è stata menzionata è quella di memorizzare l'intero albero in un singolo array. Ciò porta a un codice più complesso, ma a volte rappresenta un'implementazione molto vantaggiosa in casi specifici, specialmente per enormi alberi fissi, poiché è possibile risparmiare il costo dell'intestazione dell'oggetto e allocare memoria contigua.


1
Ad esempio: un albero B + non userebbe mai questa struttura di "primogenito, di prossima generazione". Sarebbe inefficiente al punto di assurdità per un albero basato su disco, e ancora molto inefficiente per un albero basato su memoria. Un R-tree in memoria potrebbe tollerare questa struttura, ma implicherebbe comunque molti più cache-miss. Ho difficoltà a pensare a una situazione in cui "primogenito, prossimo" sarebbe superiore. Bene, sì, potrebbe funzionare per un albero di sintassi come menzionato in munizioni. Qualunque altra cosa?
Qwertie

3
"aggiungi sempre un bambino in O (1)" - Penso che puoi sempre inserire un bambino all'indice 0 in O (1), ma aggiungere un bambino sembra essere chiaramente O (n).
Scott Whitlock,

La memorizzazione dell'intero albero in un singolo array è comune per gli heap.
Brian,

1
@Scott: beh, ho ipotizzato che anche l'elenco collegato contenesse un puntatore / riferimento all'ultimo elemento, che lo renderebbe O (1) sia per la prima che per l'ultima posizione ... sebbene manchi nell'esempio dei PO
dagnelies

Scommetto che (tranne forse in casi estremamente degeneri) l'implementazione "primogenita, di prossima generazione" non è mai più efficiente delle implementazioni di tabelle figlio basate su array. La località cache vince, alla grande. Gli alberi B hanno dimostrato di essere le implementazioni più efficienti di gran lunga sulle architetture moderne, vincendo contro gli alberi rosso-neri usati tradizionalmente proprio grazie alla migliore localizzazione della cache.
Konrad Rudolph,

2

Quasi ogni progetto che ha un modello o documento modificabile avrà una struttura gerarchica. Può essere utile implementare il "nodo gerarchico" come classe base per entità diverse. Spesso l'elenco collegato (fratello minore, 2 ° modello) è il modo naturale in cui crescono molte librerie di classi, tuttavia i bambini possono essere di diversi tipi e probabilmente un " modello a oggetti " non è ciò che consideriamo quando parliamo di alberi in generale.

La mia implementazione preferita di un albero (nodo) del tuo primo modello è un one-liner (in C #):

public class node : List<node> { /* props go here */ }

Eredita da un elenco generico del tuo tipo (o eredita da qualsiasi altra raccolta generica del tuo tipo). Camminare è possibile in una direzione: forma la radice verso il basso (gli oggetti non conoscono i loro genitori).

Albero solo genitore

Un altro modello che non hai menzionato è quello in cui ogni bambino ha un riferimento al suo genitore:

               null
                 |
       +---------+---------------------------------+
       |       parent                              |
       | root                                      |
       +-------------------------------------------+
          |                   |                |
+---------+------+    +-------+--------+    +--+-------------+
|     parent     |    |     parent     |    |     parent     |
|     node 1     |    |     node 2     |    |     node 3     |
+----------------+    +----------------+    +----------------+

Camminare su questo albero è possibile solo viceversa, normalmente tutti questi nodi saranno archiviati in una raccolta (array, tabella hash, dizionario ecc.) E un nodo sarà localizzato cercando la raccolta su criteri diversi dalla posizione gerarchica nella albero che in genere non sarebbe di primaria importanza.

Questi alberi solo per i genitori sono di solito visualizzati nelle applicazioni di database. È abbastanza facile trovare i figli di un nodo con le istruzioni "SELECT * WHERE ParentId = x". Tuttavia raramente li troviamo trasformati in oggetti di classe ad albero-nodo in quanto tali. Nelle applicazioni statefull (desktop) possono essere racchiuse in controlli di nodi dell'albero esistenti. Nelle applicazioni senza stato (web) anche questo può essere improbabile. Ho visto gli strumenti del generatore di classi di mappatura ORM generare errori di overflow dello stack durante la generazione di classi per le tabelle che hanno una relazione con se stessi (ridacchiare), quindi forse questi alberi non sono poi così comuni.

alberi navigabili bidirezionali

Nella maggior parte dei casi pratici, tuttavia, è conveniente avere il meglio di entrambi i mondi. Nodi che hanno un elenco di bambini e inoltre conoscono il loro genitore: alberi navigabili bidirezionali.

                          null
                            |
       +--------------------+--------------------+
       |                  parent                 |
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------+-----+    +-------+-------+    +---+-----------+
|      parent   |    |     parent    |    |  parent       |
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Ciò comporta molti altri aspetti da considerare:

  • Dove implementare il collegamento e lo scollegamento dei genitori?
    • lascia che la logica di affari si prenda cura di te e lascia l'aspetto fuori dal nodo (dimenticheranno!)
    • i nodi hanno metodi per creare figli (non consente il riordino) (scelta di Microsofts nella loro implementazione DOM System.Xml.XmlDocument, che mi ha fatto quasi impazzire quando l'ho incontrato per la prima volta)
    • I nodi accettano un genitore nel loro costruttore (non consente il riordino)
    • in tutti i metodi add (), insert () e remove () e i loro sovraccarichi dei nodi (di solito la mia scelta)
  • persistenza
    • Come camminare sull'albero durante la persistenza (ad esempio tralasciare i collegamenti principali)
    • Come ricostruire il collegamento bidirezionale dopo la deserializzazione (impostando nuovamente tutti i genitori come azione post-deserializzazione)
  • notifiche
    • Meccanismi statici (flag IsDirty), gestire ricorsivamente nelle proprietà?
    • Eventi, passa in rassegna attraverso i genitori, in giù attraverso i bambini o in entrambi i modi (ad esempio considera il pump dei messaggi di Windows).

Ora, per rispondere alla domanda , gli alberi navigabili bidirezionali tendono ad essere (nella mia carriera e campo finora) i più utilizzati. Esempi sono l'implementazione di Microsofts di System.Windows.Forms.Control o System.Web.UI.Control nel framework .Net, ma anche ogni implementazione DOM (Document Object Model) avrà nodi che conoscono il loro genitore e un elenco dei loro figli. Il motivo: facilità d'uso e facilità di implementazione. Inoltre, si tratta generalmente di classi di base per classi più specifiche (XmlNode può essere la base delle classi Tag, Attribute e Text) e queste classi di base sono luoghi naturali in cui inserire architetture generiche di serializzazione e gestione degli eventi.

L'albero è al centro di molte architetture, ed essere in grado di navigare liberamente significa essere in grado di implementare soluzioni più velocemente.


1

Non conosco alcuna libreria di container che supporti direttamente il tuo secondo caso, ma la maggior parte delle librerie di container può facilmente supportare tale scenario. Ad esempio, in C ++ potresti avere:

class Node;  // forward reference to satisfy the compiler
typedef std::list<Node*> NodeList;
class Node : public NodeList { /* . . . */ };  // a node is also a list

Node* n = new Node;
n->push_back(new Node);
Node* tree = new Node;
tree->push_back(new Node);
tree->push_back(n);

I parser probabilmente usano una struttura simile a questa, perché supporta in modo efficiente nodi con un numero variabile di elementi e elementi secondari. Non lo so per certo perché di solito non leggo il loro codice sorgente.


1

Uno dei casi in cui è preferibile avere una schiera di figli è quando è necessario un accesso casuale ai figli. E questo di solito è quando i bambini sono ordinati. Ad esempio, l'albero della gerarchia simile a un file può utilizzarlo per una ricerca più rapida del percorso. O albero dei tag DOM quando l'accesso all'indice è molto naturale

Un altro esempio è quando avere i "puntatori" su tutti i bambini consente un uso più conveniente. Ad esempio, entrambi i tipi descritti possono essere utilizzati durante l'implementazione delle relazioni ad albero con il database relazionale. Ma il primo (dettaglio principale dal genitore ai figli in questo caso) consentirà di eseguire query con SQL generale per dati utili, mentre il secondo ti limiterà in modo significativo.

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.