Un albero radicato è una struttura dati composta da nodi organizzati gerarchicamente. Ha un nodo radice che non ha genitori. Ogni altro nodo dell’albero ha esattamente un genitore e può avere figli. I nodi che non hanno figli sono chiamati foglie.
Tutte queste funzioni sono realizzate con l’implementazione degli alberi con i puntatori.
Stampa
def stampa(p, livello=0):
if p:
print('| '*livello, p.key)
stampa(p.left, h+1)
stampa(p.right, h+1)
Crea un albero con nodi e un numero casuale di figli
def crea_albero(n):
if n == 0:
return None
nodo = Nodo(random.randint(0, 9))
# stabilire il numero di figli dei sottoalberi
n_left = random.randint(0, n - 1)
n_right = n - 1 - n_left
# creare i sottoalberi
nodo.left = crea_albero(n_left)
nodo.right = crea_albero(n_right)
return nodo
Visita dell’albero
La visita dell’albero è l’accesso progressivo a tutti i suoi nodi con un determinato criterio di ordine.
Visite per profondità
Per gli alberi binari esistono tre possibili visite in profondità, ovviamente dallo stesso costo computazionale:
- In preordine: il nodo è visitato prima di proseguire la visita nei suoi sottoalberi.
def preorder(nodo):
if nodo is None:
return
print(nodo.value, end=' ')
preorder(nodo.left)
preorder(nodo.right)
- In ordine: il nodo è visitato dopo la visita del sottoalbero sinistro e prima di quella del sottoalbero destro.
def inorder(nodo):
if nodo is None:
return
inorder(nodo.left)
print(nodo.value, end=' ')
inorder(nodo.right)
- In postordine: il nodo è visitato dopo entrambe le visite dei sottoalberi.
def postorder(nodo):
if nodo is
preordine(nodo.right)
print(nodo.key, end = ' ')None:
return
postorder(nodo.left)
postorder(nodo.right)
print(nodo.key, end=' ')
Visita per ampiezza/livello (BFS)
Esiste anche un altro tipo di visita, cioè la visita in ampiezza “Breadth-First Search” (per livello, da sinistra a destra).
L’idea è di sfruttare una queue (importata da collections
) per memorizzare i nodi in ogni livello e man mano estrarre i nodi visitati (da stampare o da inserire in un generatore) in .
from collections import deque
def visita_per_livello(nodo) -> List:
if nodo is not None:
coda = deque([nodo])
while coda: # aggiungiamo i figli destro e sinistro, rimuoviamo l'ultimo nodo dalla coda poi ripetiamo finché la coda si è svuotata
nodo = coda.popleft()
yield nodo.key # anche print(nodo.key, end=' ') andava bene
if nodo.left:
coda.append(nodo.left)
if nodo.right:
coda.append(nodo.right)
print(list(visita_per_livello(nodo)))
Costo computazionale
Chiamiamo il numero di nodi del sottoalbero destro, l’equazione di ricorrenza è:
Se l’albero è completo, la suddivisione delle due chiamate ricorsive è la più equa possibile:
Se l’albero è massimamente sbilanciato, quindi quando o , si ottiene
Indipendentemente dall’albero, la complessità della visita è sempre .
Funzioni utili che sfruttano la visita dell’albero
def conta_nodi_albero(nodo):
if nodo == None:
return 0
return conta_nodi_albero(nodo.left) + conta_nodi_albero(nodo.right) + 1
def cerca_nodo(nodo, query):
if nodo == None:
return False
return nodo.key==query or cerca_nodo(nodo.left, query) or cerca_nodo(nodo.right, query)
def altezza(nodo):
if nodo == None:
return -1 # albero vuoto
return max(altezza(nodo.left), altezza(nodo.right)) + 1
def conta_nodi_al_livello(nodo, livello): # O(2^livello)
if nodo == None:
return 0 # albero vuoto
if k==0: # si è raggiunto il livello
return 1
return conta_nodi_al_livello(nodo.left, k-1) + conta_nodi_al_livello(nodo.right, k-1)
def lvl_min_foglie(nodo): # O(n)
if not nodo:
return -1
if not nodo.left and not nodo.right:
return 0
elif nodo.left and nodo.right:
return min(lvl_min_foglie(nodo.left), lvl_min_foglie(nodo.right)) + 1
elif nodo.left:
return lvl_min_foglie(nodo.left) + 1
elif nodo.right:
return lvl_min_foglie(nodo.right) + 1
def helper(nodo):
if not nodo:
return 0, 0
figli_left, sb_left = helper(nodo.left)
figli_right, sb_right = helper(nodo.right)
sb = abs(figli_right-figli_left)
return figli_right + figli_left + 1, max(sb, sb_right, sb_left)
def sbilanciamento(nodo):
return helper(nodo)[1]