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