Quando si impara a codificare per la prima volta, è comune imparare gli array come “struttura dati principale”
Eventualmente, si impara anche hash tables. Se state perseguendo una laurea in informatica, dovete seguire un corso sulla struttura dei dati. Imparerete anche linked listsqueues, e stacks. Queste strutture di dati sono chiamate strutture di dati “lineari” perché hanno tutte un inizio logico e una fine logica.
Quando iniziamo ad imparare su trees e graphs, può diventare molto confuso. Non memorizziamo i dati in modo lineare. Entrambe le strutture dati memorizzano i dati in un modo specifico.
Questo post è per aiutarvi a capire meglio la struttura di dati ad albero e per chiarire qualsiasi confusione possiate avere su di essa.
In questo articolo, impareremo:
- Cos’è un albero
- Esempi di alberi
- La sua terminologia e come funziona
- Come implementare strutture ad albero nel codice.
Iniziamo questo viaggio di apprendimento. 🙂
Definizione
Quando si inizia a programmare, è comune capire meglio le strutture dati lineari rispetto a strutture dati come alberi e grafi.
Gli alberi sono ben noti come una struttura dati non lineare. Non memorizzano i dati in modo lineare. Organizzano i dati in modo gerarchico.
Tuffiamoci in esempi di vita reale!
Cosa intendo quando dico in modo gerarchico?
Immaginate un albero genealogico con relazioni di tutte le generazioni: nonni, genitori, figli, fratelli, ecc. Organizziamo comunemente gli alberi genealogici in modo gerarchico.
Il disegno sopra è il mio albero genealogico. Tossico, Akikazu, Hitomi, e Takemi sono i miei nonni.
Toshiaki e Juliana sono i miei genitori.
TK, Yuji, Bruno, e Kaio sono i figli dei miei genitori (io e i miei fratelli).
In HTML, il Document Object Model (DOM) funziona come un albero.
Il tag HTML contiene altri tag. Abbiamo un tag head e un tag body. Questi tag contengono elementi specifici. Il tag head ha i tag meta e title. Il tag body ha elementi che si mostrano nell’interfaccia utente, per esempio, h1ali, ecc.
Una definizione tecnica
Un tree è un insieme di entità chiamate nodes. I nodi sono collegati da edges. Ogni node contiene un value o data, e può avere o meno un child node .
Il first node del tree è chiamato root. Se questo root node è collegato da un altro node, il root è allora un parent node e il node collegato è un child.
Tutti i Tree nodes sono collegati da link chiamati edges. E’ una parte importante di trees, perché gestisce la relazione tra nodes.
Leaves sono gli ultimi nodes su un tree. Sono nodi senza figli. Come i veri alberi, abbiamo il rootbranches, e infine il leaves.
Altri concetti importanti da capire sono height e depth.
Il height di un tree è la lunghezza del percorso più lungo di un leaf.
Il depth di un node è la lunghezza del percorso verso il suo root.
Riassunto terminologico
- Radice è il
nodepiù alto deltree - Bordo è il collegamento tra due
nodes - Bambino è un
nodeche ha unparent node - Genitore è un
nodeche ha unedgea unchild node - Foglia è un
nodeche non ha unchild nodeneltree - Altezza è la lunghezza del percorso più lungo verso un
leaf - La profondità è la lunghezza del percorso verso il suo
root
Alberi binari
Ora discuteremo un tipo specifico di tree. Lo chiamiamo ilbinary tree.
“In informatica, un albero binario è una struttura dati ad albero in cui ogni nodo ha al massimo due figli, che sono indicati come il figlio sinistro e il figlio destro.” – Wikipedia
Guardiamo quindi un esempio di un binary tree.
Codifichiamo un albero binario
La prima cosa che dobbiamo tenere a mente quando implementiamo un binary tree è che è un insieme di nodes. Ogni node ha tre attributi: valueleft_child, e right_child.
Come implementiamo un semplice binary tree che inizializza con queste tre proprietà?
Diamo un’occhiata.
class BinaryTree: def __init__(self, value): self.value = value self.left_child = None self.right_child = None
Ecco qui. Il nostro binary tree classe.
Quando istanziamo un oggetto, passiamo il value (i dati del nodo) come parametro. Guardate il left_child e il right_child. Entrambi sono impostati su None.
Perché?
Perché quando creiamo il nostro node, non ha nessun figlio. Abbiamo solo il node data.
Testiamolo:
tree = BinaryTree('a')print(tree.value) # aprint(tree.left_child) # Noneprint(tree.right_child) # None
Ecco.
Possiamo passare il stringa‘ come value al nostro Binary Tree node. Se stampiamo il valueleft_child, e right_child, possiamo vedere i valori. Cosa dobbiamo fare qui?
Implementeremo un metodo per inserire un nuovo node al right e al left.
Ecco le regole:
- Se l’attuale
nodenon ha unleft child, creiamo semplicemente un nuovonodee lo impostiamo sulleft_childdel nodo attuale. - Se ha il
left child, creiamo un nuovo nodo e lo mettiamo al posto dell’attualeleft child. Allocate questoleft child nodealleft childdel nuovo nodo. 🙂
Ecco il codice:
Ancora una volta, se il nodo corrente non ha un
left child, creiamo semplicemente un nuovo nodo e lo impostiamo sulleft_childdel nodo corrente. Oppure creiamo un nuovo nodo e lo mettiamo al posto dell’attualeleft child. Assegniamo questoleft child nodealleft childdel nuovo nodo.E facciamo la stessa cosa per inserire un
right child node.Fatto. 🙂
Ma non del tutto. Dobbiamo ancora testarlo.
Costruiamo il seguente
tree:Per riassumere l’illustrazione di questo albero:
-
anodesarà ilrootdel nostrobinary Tree -
aleft childbnode -
aright childcnode -
bright childdnodebnodenon ha unleft child) -
cleft childenode -
cright childfnode - sia
echefnodesnon hanno figli
Quindi ecco il codice per il
tree:L’inserimento è fatto.
Ora dobbiamo pensare al
treetraversal.Abbiamo due opzioni: Depth-First Search (DFS) e Breadth-First Search (BFS).
- DFS “è un algoritmo per attraversare o cercare una struttura dati ad albero. Si parte dalla radice e si esplora il più possibile lungo ogni ramo prima di fare il backtracking”. – Wikipedia
- BFS “è un algoritmo per attraversare o cercare la struttura ad albero dei dati. Inizia alla radice dell’albero ed esplora prima i nodi vicini, prima di passare ai vicini di livello successivo”. – Wikipedia
Perciò immergiamoci in ogni tipo di attraversamento dell’albero.
Ricerca in profondità (DFS)
DFS esplora un percorso fino ad una foglia prima di tornare indietro ed esplorare un altro percorso. Vediamo un esempio con questo tipo di attraversamento.
Il risultato per questo algoritmo sarà 1-2-3-4-5-6-7.
Perché?
Ripartiamo.
- Partiamo dal
root(1). Stampalo.
2. Vai al
left child(2). Stampalo.3. Poi vai al
left child(3). Stampatelo. (Questonodenon ha figli)4. Tornate indietro e andate al
right child(4). Stampatelo. (Questonodenon ha figli)5. Ritornate al
rootnodee andate alright child(5). Stampalo.6. Vai al
left child(6). Stampatelo. (Questonodenon ha figli)7. Tornate indietro e andate al
right child(7). Stampatelo. (Questonodenon ha figli)8. Fatto.
Quando andiamo in profondità alla foglia e facciamo backtrack, questo si chiama algoritmo DFS.
Ora che abbiamo familiarità con questo algoritmo di attraversamento, discuteremo i tipi di DFS:
pre-orderin-order, epost-order.Pre-ordine
Questo è esattamente quello che abbiamo fatto nell’esempio precedente.
- Stampa il valore del
node. - Vai al
left childe stampalo. Questo se, e solo se, ha unleft child. - Vai al
right childe stampalo. Questo se, e solo se, ha unright child.
In-order
Il risultato dell’algoritmo in-order per questo
treeesempio è 3-2-4-1-6-5-7.La sinistra per prima, la metà per seconda e la destra per ultima.
Ora codifichiamo.
def in_order(self): if self.left_child: self.left_child.in_order() print(self.value) if self.right_child: self.right_child.in_order()- Vai al
left childe stampalo. Questo se, e solo se, ha unleft child. - Stampa il valore del
node - Vai al
right childe stampalo. Questo se, e solo se, ha unright child.
Post-order
Il risultato dell’
post orderalgoritmo per questotreeesempio è 3-4-2-6-7-5-1.La sinistra per prima, la destra per seconda, e la metà per ultima.
Codifichiamo questo.
- Vai al
left childe stampalo. Questo se, e solo se, ha unleft child. - Vai al
right childe stampalo. Questo se, e solo se, ha unright child. - Stampa il valore del
node
Ricerca Breadth-First (BFS)
L’algoritmo BFS attraversa il
treelivello per livello e profondità per profondità.Ecco un esempio che aiuta a spiegare meglio questo algoritmo:
Si attraversa livello per livello. In questo esempio, il risultato è 1-2-5-3-4-6-7.
- Livello/Profondità 0: solo
nodecon valore 1 - Livello/Profondità 1:
nodescon valori 2 e 5 - Livello/Profondità 2:
nodescon valori 3, 4, 6 e 7
Ora codifichiamo.
Per implementare un algoritmo BFS, usiamo la struttura dati
queuecome aiuto.Come funziona?
Ecco la spiegazione.
- Prima aggiungi il
rootnodenelqueuecon il metodoput. - Iterate mentre il
queuenon è vuoto. - Prendi il primo
nodenelqueue, e poi stampa il suo valore. - Aggiungere sia
leftcherightchildrenin ilqueue(se ilnodecorrente hachildren). - Fatto. Stamperemo il valore di ogni
node,livello per livello, con il nostroqueuehelper.
Albero di ricerca binaria
“Un albero di ricerca binaria è talvolta chiamato albero binario ordinato o ordinato, e mantiene i suoi valori in ordine, in modo che la ricerca e altre operazioni possano utilizzare il principio della ricerca binaria” – Wikipedia
Un’importante proprietà di un
Binary Search Treeè che il valore di unBinary Search Treenodeè maggiore del valore della prole del suoleft child, ma più piccolo del valore della prole del suoright child.“Ecco una ripartizione dell’illustrazione precedente:
- A è invertito. Il
subtree7-5-8-6 deve essere a destra, e ilsubtree2-1-3 deve essere a sinistra. - B è l’unica opzione corretta. Soddisfa la proprietà
Binary Search Tree. - C ha un problema: il
nodecon il valore 4. Deve stare a sinistra delrootperché è più piccolo di 5.
Codifichiamo un albero di ricerca binario!
Ora è il momento di codificare!
Cosa vedremo qui? Inseriremo nuovi nodi, cercheremo un valore, cancelleremo nodi, e il saldo del
tree.Iniziamo.
Inserimento: aggiungere nuovi nodi al nostro albero
Immaginiamo di avere un
treevuoto e vogliamo aggiungere nuovinodescon i seguenti valori in questo ordine: 50, 76, 21, 4, 32, 100, 64, 52.La prima cosa che dobbiamo sapere è se 50 è il
rootdel nostro albero.Possiamo ora iniziare a inserire
nodedanode.- 76 è maggiore di 50, quindi inserire 76 sul lato destro.
- 21 è minore di 50, quindi inserire 21 sul lato sinistro.
- 4 è minore di 50.
Nodecon valore 50 ha unleft child21. Dato che 4 è più piccolo di 21, inseriscilo a sinistra di questonode. - 32 è più piccolo di 50.
Nodecon valore 50 ha unleft child21. Poiché 32 è maggiore di 21, inseriamo 32 sul lato destro di questonode. - 100 è maggiore di 50.
Nodecon valore 50 ha unright child76. Poiché 100 è più grande di 76, inseriamo 100 sul lato destro di questonode. - 64 è più grande di 50.
Nodecon valore 50 ha unright child76. Poiché 64 è più piccolo di 76, inseriamo 64 sul lato sinistro di questonode. - 52 è maggiore di 50.
Nodecon valore 50 ha unright child76. Poiché 52 è più piccolo di 76,nodecon valore 76 ha unleft child64. 52 è più piccolo di 64, quindi inserisci 54 sul lato sinistro di questonode.
Nota uno schema qui?
- Il nuovo
nodevalore è maggiore o minore dell’attualenode? - Se il valore del nuovo
nodeè maggiore dell’attualenode,vai a destrasubtree. Se l’attualenodenon ha unright child, inseritelo lì, altrimenti tornate al passo #1. - Se il valore del nuovo
nodeè più piccolo dell’attualenode, andate a sinistrasubtree. Se l’attualenodenon ha unleft child, inseriscilo lì, altrimenti torna al passo #1. - Non abbiamo gestito casi speciali qui. Quando il valore di un nuovo
nodeè uguale al valore corrente delnode,usa la regola numero 3. Considera di inserire valori uguali al lato sinistro delsubtree.
Ora codifichiamo.
Sembra molto semplice.
La parte potente di questo algoritmo è la parte di ricorsione, che è sulla linea 9 e sulla linea 13. Entrambe le linee di codice chiamano il metodo
insert_node, e lo usano rispettivamente per i suoilefterightchildren. Le linee11e15sono quelle che fanno l’inserimento per ognichild.Cerchiamo il valore del nodo… Oppure no…
L’algoritmo che costruiremo ora riguarda il fare ricerche. Per un dato valore (numero intero), diremo se il nostro
Binary Search Treeha o non ha quel valore.Un elemento importante da notare è come abbiamo definito l’algoritmo di inserimento dell’albero. Prima abbiamo il nostro
rootnode. Tutti isubtreedi sinistranodesavranno valori più piccoli delrootnode. E tutti isubtreenodesdi destra avranno valori maggiori delrootnode.Guardiamo un esempio.
Immaginiamo di avere questo
tree.Ora vogliamo sapere se abbiamo un nodo basato sul valore 52.
Facciamo un esempio.
- Iniziamo con il
rootnodecome nostronodeattuale. Il valore dato è più piccolo del valore attualenode? Se sì, allora lo cercheremo sulla sinistrasubtree. - Il valore dato è maggiore del valore attuale
node? Se sì, allora lo cercheremo sulla destrasubtree. - Se le regole #1 e #2 sono entrambe false, possiamo confrontare il valore corrente
nodee il valore dato se sono uguali. Se il confronto restituiscetrue, allora possiamo dire: “Sì! Il nostrotreeha il valore dato,” altrimenti, diciamo, “Nooo, non l’ha fatto.”
Ora codifichiamo.
Facciamo il codice:
- Le linee 8 e 9 rientrano nella regola #1.
- Le linee 10 e 11 rientrano nella regola #2.
- La linea 13 rientra nella regola #3.
Come lo testiamo?
Creiamo il nostro
Binary Search Treeinizializzando ilrootnodecon il valore 15.bst = BinarySearchTree(15)E ora inseriremo molti nuovi
nodes.Per ogni
nodeinserito, testeremo se il nostro metodofind_nodefunziona davvero.Sì, funziona per questi valori dati! Testiamo un valore che non esiste nel nostro
Binary Search Tree.print(bst.find_node(0)) # FalseOh sì.
La nostra ricerca è fatta.
Cancellazione: rimuovere e organizzare
La cancellazione è un algoritmo più complesso perché dobbiamo gestire diversi casi. Per un dato valore, dobbiamo rimuovere il
nodecon questo valore. Immaginate i seguenti scenari per questonode: non ha nessunchildren, ha un solochild, o ha duechildren.- Scenario #1: Un
nodesenzachildrenleafnode).
# |50| |50|# / \ / \# |30| |70| (DELETE 20) ---> |30| |70|# / \ \# |20| |40| |40|Se il
nodeche vogliamo cancellare non ha figli, lo cancelliamo semplicemente. L’algoritmo non ha bisogno di riorganizzare iltree.- Scenario #2: Un
nodecon un solo figlio (leftorightfiglio).
# |50| |50|# / \ / \# |30| |70| (DELETE 30) ---> |20| |70|# / # |20|In questo caso, il nostro algoritmo deve far sì che il genitore del
nodepunti al nodochild. Se ilnodeè illeft child, facciamo puntare il genitore delleft childalchild. Se ilnodeè ilright childdel suo genitore, facciamo puntare il genitore delright childalchild.- Scenario #3: Un
nodecon due bambini.
# |50| |50|# / \ / \# |30| |70| (DELETE 30) ---> |40| |70|# / \ /# |20| |40| |20|Quando il
nodeha 2 figli, dobbiamo trovare ilnodecon il valore minimo, partendo dalnode‘sright child. Metteremo questonodecon valore minimo al posto delnodeche vogliamo rimuovere.E’ il momento di codificare.
- Prima: Notate i parametri
valueeparent. Vogliamo trovare ilnodeche ha questovalue, e il genitore delnodeè importante per la rimozione delnode. Il nostro algoritmo restituirà un valore booleano. RestituisceTruese trova ilnodee lo rimuove. Altrimenti restituisceFalse. - Dalla linea 2 alla linea 9: Iniziamo a cercare il
nodeche ha ilvalueche stiamo cercando. Se ilvalueè più piccolo delcurrent nodevalue, andiamo alleft subtree, ricorsivamente (se, e solo se, ilcurrent nodeha unleft child). Se ilvalueè maggiore, vai alright subtree, ricorsivamente. - Linea 10: Cominciamo a pensare all’algoritmo
remove. - Dalla linea 11 alla linea 13: Copriamo il
nodesenzachildren, ed è illeft childdal suoparent. Rimuoviamo ilnodeimpostando ilparentdelleft childaNone. - Linee 14 e 15: Copriamo il
nodesenzachildren, ed è ilright childdal suoparent. Rimuoviamo ilnodeimpostando ilparentdelright childaNone. - Metodo Clear node: Mostrerò il codice
clear_nodequi sotto. Imposta i nodileft child , right child, e il suovalueaNone. - Dalla linea 16 alla linea 18: Copriamo il
nodecon un solochildleft child), ed è illeft childdal suoparent. Impostiamo ilparentdelleft childalnodedelleft child(l’unico figlio che ha). - Dalla linea 19 alla linea 21: Copriamo il
nodecon un solochildleft child), ed è ilright childdel suoparent. Impostiamo ilparentdelright childalnodedelleft child(l’unico figlio che ha). - Dalla linea 22 alla linea 24: Copriamo il
nodecon un solochildright child), ed è illeft childdal suoparent. Impostiamo ilparentdelleft childalnodedelright child(l’unico figlio che ha). - Dalla linea 25 alla linea 27: Copriamo il
nodecon un solochildright child) , ed è ilright childdel suoparent. Impostiamo ilparentdelright childalnodedelright child(l’unico figlio che ha). - Dalla linea 28 alla linea 30: Copriamo il
nodecon entrambi ilefterightfigli. Prendiamo ilnodecon il più piccolovalue(il codice è mostrato sotto) e lo impostiamo sulvaluedelcurrent node. Terminate rimuovendo il più piccolonode. - Linea 32: Se troviamo il
nodeche stiamo cercando, deve restituireTrue. Dalla linea 11 alla linea 31, gestiamo questo caso. Quindi basta restituireTruee questo è tutto.
- Per usare il metodo
clear_node: impostare il valoreNonesu tutti e tre gli attributi – (valueleft_child, eright_child)
def clear_node(self): self.value = None self.left_child = None self.right_child = None- Per usare il metodo
find_minimum_value: andare molto in basso a sinistra. Se non troviamo più nodi, abbiamo trovato il più piccolo.
def find_minimum_value(self): if self.left_child: return self.left_child.find_minimum_value() else: return self.valueOra testiamolo.
Useremo questo
treeper testare il nostroremove_nodealgoritmo.# |15|# / \# |10| |20|# / \ / \# |8| |12| |17| |25|# \# |19|Togliamo il
nodecon ilvalue8. E’ unnodesenza figli.print(bst.remove_node(8, None)) # Truebst.pre_order_traversal()# |15|# / \# |10| |20|# \ / \# |12| |17| |25|# \# |19|Ora rimuoviamo il
nodecon ilvalue17. E’ unnodecon un solo figlio.print(bst.remove_node(17, None)) # Truebst.pre_order_traversal()# |15|# / \# |10| |20|# \ / \# |12| |19| |25|Infine, rimuoveremo un
nodecon due figli. Questo è ilrootdel nostrotree.print(bst.remove_node(15, None)) # Truebst.pre_order_traversal()# |19|# / \# |10| |20|# \ \# |12| |25|I test sono ora fatti. 🙂
E’ tutto per ora!
Abbiamo imparato molto qui.
Congratulazioni per aver finito questo denso contenuto. È davvero difficile capire un concetto che non conosciamo. Ma tu ce l’hai fatta 🙂
Questo è un ulteriore passo avanti nel mio viaggio verso l’apprendimento e la padronanza di algoritmi e strutture dati. Puoi vedere la documentazione del mio viaggio completo qui sulla mia pubblicazione Renaissance Developer.
Divertiti, continua a imparare e a codificare.
Il mio Twitter & Github. ☺
Risorse aggiuntive
- Introduzione alla struttura di dati ad albero da mycodeschool
- Albero da Wikipedia
- Come non rimanere bloccati dagli alberi dal talentuoso Vaidehi Joshi
- Introduzione agli alberi, lezione del Professor Jonathan Cohen
- Introduzione agli alberi, Lecture by Professor David Schmidt
- Intro to Trees, Lecture by Professor Victor Adamchik
- Trees with Gayle Laakmann McDowell
- Binary Tree Implementation and Tests by TK
- Coursera Course: Data Structures dell’Università della California, San Diego
- Corso Coursera: Data Structures and Performance by University of California, San Diego
- Concetti e implementazione dell’albero di ricerca binaria da Paul Programming
- Implementazione dell’albero di ricerca binaria e test da TK
- Tree Traversal da Wikipedia
- Albero di Ricerca Binario Rimuovi Nodo Algoritmo da GeeksforGeeks
- Albero di Ricerca Binario Rimuovi Nodo Algoritmo da Algolist
- Imparare Python Da Zero a Eroe
-