14-02-08 Esercizio: scrivere un programma che calcola l`altezza di

14-02-08
Esercizio: scrivere un programma che calcola
l'altezza di
un albero non vuoto.
Altezza = massimo fra le profondita' di tutti i
nodi, lunghezza
del cammino piu' lungo dalla radice a una
foglia.
Alberi che hanno un solo nodo hanno altezza
0.
*
/\
*
*
/
*
Possiamo usare un algoritmo che assegna
altezza -1 all'albero vuoto (vedi il programma
height).
Oppure ragionare con questo algoritmo:
- se un albero e' costituito da un solo
elemento *, allora
la sua altezza e' 0;
- altrimenti, l'altezza sara' 1 + il massimo fra
l'altezza
del sottoalbero sinistro e l'altezza del
sottoalbero destro.
Su questo secondo algoritmo e' basato il
programma
height2 (che ha un codice leggermente piu'
complicato).
[un albero definito per fare qualche esempio
con
l'applicazione dr. scheme]
(define t1
(grow_tree 2 (grow_tree 3 () ())
(grow_tree 4 (grow_tree 5 () ())
())))
2
/
\
3
4
/
5
Foglia = albero di un elemento, quindi ha
questa forma
*
/\
() ()
Un test che stabilisca se un albero e' una
foglia
sara' utile per il programma height2, dove il
caso
base e' per l'appunto costituito dalle foglie.
(define height
(lambda (t)
(if (empty_tree? t)
-1
(+ 1 (max (height (lft t)) (height (rgt
t)))))))
(define height2
(lambda (t)
(cond ((leaf? t) 0)
((empty_tree? (lft t)) (+ 1 (height2
(rgt t))))
((empty_tree? (rgt t)) (+ 1 (height2
(lft t))))
(else (+ 1 (max (height2 (lft t))
(height2 (rgt t))))))))
===============
ESERCIZIO: Scrivere un programma tree?
che stabilisce
se un dato argomento e' un albero binario
(generico).
Per essere albero binario un argomento t,
puo' essere:
- o la lista vuota;
- oppure una tripla, rappresentata come
coppia di
elemento + coppia di altre due componenti,
dove:
= il primo elemento e' una qualunque
espressione;
= la seconda e la terza componente (cadr
t) e (cddr t)
devono a loro volta essere alberi binari
(define tree?
(lambda (t)
(if (null? t)
#t
(and (pair? t)
(pair? (cdr t))
(tree? (lft t))
(tree? (rgt t))))))
===================================
=============
Binary search trees (BST)
Contesto: parliamo di alberi binari di interi.
Definizione di BST: un albero binario t e' un
BST se:
- e' l'albero vuoto oppure:
- e' un albero binario tale che la sua radice e'
maggiore o uguale
a tutti i nodi che stanno nel sottoalbero
sinistro, ed e'
minore o uguale a tutti i nodi che stanno nel
sottoalbero destro, e
inoltre, sono BST anche (lft t) e (rgt t).
10
/
\
5
\
7
20
/ \
15 22
(define t2
(grow_tree 10 (grow_tree 5 () (grow_tree 7
() ()))
(grow_tre 20 (grow_tree
15 () ())
(grow_tree 22 () ()) ) ) )
nell'albero sopra la proprieta' BST e' distrutta
dal sottalbero sinistro, dove la radice 5 e' piu'
grande
di un elemento che sta nel suo sottoalbero
destro
(il nodo che contiene il numero 7).
Caratteristica dei BST: molti algoritmi hanno
un costo
proporzionale all'altezza dell'albero.
Vantaggio: se il bst e' bilanciato, l'altezza
dell'albero e' logaritmica rispetto al numero
degli elementi
presenti nell'albero e quindi quegli algoritm
che hanno un costo proporzionale a (height t)
sono molto efficienti.
*
/
\
*
*
<====
non e' bilanciato
/
*
/
*
\
*
*
/\
*
/\ /
*
<======
bilanciato
* **
Confronto fra ricerca di un elemento in un
albero
generico ed in un bst.
ESERCIZIO: dato un albero binario t e un
elemento
n, stabilire se n compare in qualche nodi di t.
(define belongs?
(lambda (t n)
(cond ((empty_tree? t) #f)
((= n (root t)) #t)
(else (or (belongs? (lft t) n)
(belongs? (rgt
t) n))))))
Se l'albero ha n nodi, occorre nel caso piu'
sfortunato
visitarli tutti, quindi il caso peggiore ha costo
n.
In un bst, invece, a ogni passo, facendo un
confronto
con le radici dei sottoalberi esplorate
ricorsivamente,
sappiamo dove scendere, se su (lft t) oppure
(rgt t).
Questo significa, a patto che l'albero sia
bilanciato,
che la ricerca dell'elemento nel caso peggiora
costa
proporzionalmente a log(n).
(define bst_belongs?
(lambda (bst n)
(cond ((empty_tree? bst) #f)
((= n (root bst)) #t)
((< n (root bst)) (bst_belongs? (lft
bst) n))
(else (bst_belongs? (rgt bst) n)))))
================
OSS: organizzare dei dati numerici in un bst
BILANCIATO non e' cosi' semplice.
Supponiamo che ci vengano dati, uno ad uno,
gli elementi
di una lista, e noi creiamo dinamicamente un
BST via
via inserendo nuovi elementi nell'albero.
Supponiamo che ci venga data (attenzione:
fornendoci un elemento per volta, ossia: non
possiamo sfruttare il fatto che la lista
e' ordinata: questa e' una informazione che
avremo solo a posteriori), una lista di questo
tipo:
(1 2 3 4 5 6 7 8 9.....)
vogliamo trasformarla in un bst: come detto,
a noi viene dato di volta in volta il car della
lista.
Siccome non conosciamo i futuri elementi
della lista,
possiamo senz'altro via via inserire i nuovi
elementi
rispettando la proprieta' dei bst, come segue:
1
\
2
\
3
\
4......
....ma in questo modo creiamo un albero non
bilanciato
(in sostanza, l'albero cosi' creato e' solo un
modo
piu' complicato per rappresentare la lista di
partenza)
PB: come creare BST bilanciati nel problema
precedente? Non facile, non viene fatto in
questo corso
(il lettore interessato veda i cosiddetti RED
BLACK TREES).
=======
I BST consentono l'ordinamento degli
elementi in essi
presenti in tempo lineare: in altre parole, un
BST contiene
(almeno) tanta informazione quanto una lista
ordinata.
La creazione, con un costo proporzionale al
numero n
dei nodi, della lista ordinata (in senso
crescente) degli
elementi di un bst, avviene visitando l'albero
con la
cosiddetta visita "in-order".
Visita in-order di un albero
(in = radice visitata fra visita di (lft t) e visita di
(rgt t))
[ Esistono le visite pre-order e post-order:
pre = radice visitata per prima e poi (lft t) e
(rgt t)
post = radice visitata dopo la visita di (lft t) e
(rgt t)]
Visita in-order avviene in questo modo:
- se l'albero e' vuoto non c'e' nulla da visitare
(visita = esplorazione degli elementi)
- altrimenti visitiamo prima, ricorsivamente, (lft
t),
poi la radice, ed infine (rgt t) (anche su (rgt t)
applichiamo
ricorsivamente l'algoritmo in-order)
10
/
5
\
7
\
20
/ \
15 22
Scriviamo gli elementi nell'ordine in cui
vengono visitati.
5 7 10 15 20 22
Viene prodotta la lista ordinata dei numeri.
Verifichiamo scrivendo esplicitamente il
codice della
visita in-order di un albero (il risultato e' una
lista).
(define in-order
(lambda (t)
(if (empty_tree? t)
()
(append (in-order (lft t))
(list (root t))
(in-order (rgt t))))))
Provare in modo rigoroso (per induzione
sull'altezza dell'albero) che in-order produce,
dato un bst, la lista
dei suoi elementi ordinata in ordine crescente.
Per completezza ecco il codice della visita
pre-order
(sara' in relazione con i programmi che
passano
dagli alberi di parsing delle espressioni
aritmetiche
alle espressioni Scheme).
(define pre-order
(lambda (t)
(if (empty_tree? t)
()
(append (list (root t))
(pre-order (lft t))
(pre-order (rgt t))))))
la preorder visit non da' risultato
particolarmente
significativo sui bst di interi.
Esercizio: scrivere il codice della visita
post-order.
==================
Esercizio: scrivere un programma che
stabilisce
se un dato albero e' un bst.
Una possibilita' e' questa: utilizzare
due programmi che calcolano il massimo
ed il minimo di alberi binari non vuoti:
tree_max e tree_min
(define bst?
(lambda (t)
(cond ((empty_tree? t) #t)
(else (and (>= (root t)
(max_tree (lft t)))
(<= (root t)
(min_tree (rgt t)))
(bst? (lft t))
(bst? (rgt t)) )
) )))
(continua)
+
/ \
+
/
*
/\
3 4
10
\
*
/\
5 +
/ \
7
8
albero di parsing dell'espressione:
((3 * 4) + (5 * (7 + 8)) ) + 10