12b - Unife

annuncio pubblicitario
ADT ALBERO BINARIO (tree)!
n 
Rappresentazione collegata (puntatori a strutture)
1
ADT ALBERO BINARIO
OPERAZIONI PRIMITIVE DA REALIZZARE
Operazione!
Descrizione!
cons_tree: D x tree x tree Costruisce un nuovo albero, che ha
-> tree!
l’elemento fornito come radice con figli i due
sottoalberi dati!
root: tree -> D!
Restituisce l’elemento radice dell’albero!
left: tree -> tree!
Restituisce il sottoalbero sinistro !
right: tree -> tree!
Restituisce il sottoalbero destro!
emptytree: -> tree!
Restituisce (costruisce) l’albero vuoto!
empty: tree-> boolean!
!
Restituisce vero se l’albero dato è vuoto,
falso altrimenti!
2
ADT ALBERO BINARIO
OPERAZIONI DERIVATE (etc etc)
Operazione!
Descrizione!
preorder: tree!
Visita in preordine (ordine anticipato)!
inorder: tree!
Visita in ordine (“” simmetrico)!
postorder: tree!
Visita in postordine (“” ritardato)!
!
member: D x tree ->!
boolean !
Ricerca di un elemento nell’albero!
height: tree -> int!
Calcola l’altezza di un albero!
nodi: tree -> int !
Conta il numero dei nodi!
3
Esercizio 10.3.0!
n 
Un file binario ad accesso diretto (PERSONE.DAT) contiene un elenco di
informazioni su persone (cognome, nome, data di nascita, città di
residenza). Il cognome di ciascuna persona costituisce la chiave unica
di tale elenco. Per effettuare ricerche su questo elenco, si costruisca un
albero binario di ricerca mantenendo, per ciascun cognome il
numero di record corrispondente. Si definisca un programma C che:
a) costruisca la struttura dati in memoria principale a partire dal file
PERSONE.DAT e stampi l’elenco ordinato dei cognomi;
b) calcoli l'altezza dell’albero così ottenuto e il numero dei suoi nodi;
c) letto un cognome a terminale, verifichi se esiste un elemento con
quella chiave nell’albero e, trovatolo, acceda al file e legga il record
corrispondente, visualizzando i campi nome, datan e città;
d) letto un cognome a terminale, stampi ordinatamente a video i
cognomi minori o uguali di quello letto.
4
$ Esercizio 10.3.0!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Il main da realizzare deve:
a) leggere il contenuto del file ricordando il numero del
blocco letto e inserendo chiave, posizione come elemento in
un albero binario di ricerca, e stampare l’elenco ordinato dei
cognomi;
b) calcolarne altezza e numero di nodi;
c) letto un cognome e accedendo all’albero recuperare le
informazioni complete di quel cognome dal file;
d) stampare i cognomi minori di quello letto.
COMPONENTI!
el.h
tree.h
tree.c
main.c
el.c
ADT ELEMENT: el.h!
/* ELEMENT TYPE - file el.h*/
typedef struct
{
char nome[15];
char cognome[15];
char datan[7];
char citta[15]; } record_type;
typedef struct {
char cognome[15];
int pos;} el_type;
typedef enum {false,true} boolean;
/* operatori esportabili */
boolean isequal(el_type, el_type);
boolean isless(el_type, el_type);
void showel (el_type);
ADT ELEMENT: el.c!
/* ELEMENT TYPE - file el.c*/
#include <stdio.h>
#include <string.h>
#include "el.h"
boolean isequal(el_type e1, el_type e2)
/* due elementi uguali se stesso cognome */
{ if (strcmp(e1.cognome,e2.cognome)==0)
return true;
else return false; }
boolean isless(el_type e1, el_type e2)
{
if (strcmp(e1.cognome,e2.cognome) <0)
return true;
else return false; }
void showel (el_type e)
{
printf("%s\t%d\n",e.cognome,e.pos); }
ADT TREE: tree.h!
/* TREE INTERFACE - file trees.h*/
#include "el.h“
typedef struct nodo { el_type value;
struct nodo *left, *right; } NODO;
typedef NODO * tree;
/* operatori esportabili */
boolean empty(tree t);
tree emptytree(void);
el_type root(tree t);
tree left(tree t);
tree right(tree t);
tree cons_tree(el_type e, tree l, tree r);
tree ord_ins(el_type e, tree t);
void inorder(tree t);
int height (tree t);
int nodi (tree t);
int member_ord(el_type e, tree t); /*NOTA TIPO RESTITUITO
*/
void stampa_minori(tree t, el_type e);
int max_int(int, int);
$ TO DO!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Mumble
mumble …
Il main da realizzare deve: a) leggere il contenuto del file
ricordando il numero del blocco letto e inserendo chiave,
posizione come elemento in un albero binario di ricerca, e
stampare l’elenco ordinato dei cognomi; b) calcolarne
numero di nodi e altezza; c) letto un cognome e accedendo
all’albero recuperare le informazioni complete di quel
cognome dal file; d) stampare i cognomi minori di quello
letto.
main.c (1 - stub)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "tree.h"
main() {
tree indice=emptytree();
int pos;
FILE *f;
record_type REC;
el_type EL;
char COGNOME[15];
f = fopen("PERSONE.DAT", "rb");
if(f==NULL){ printf("errore apertura file\n");
exit(-1);
}
pos=0; /* CICLO DI SCANSIONE DEL FILE */
while(fread(&REC,sizeof(record_type),1,f)>0){
// “definizione” campi dell’elemento EL
// inserimento EL nell’albero indice
// da implementare
}
main.c (2 - stub)
fclose(f);
printf("Chiusura del file\n");
printf("Stampa dell’albero\n");
inorder(indice);
/* CALCOLO ALTEZZA E NUM.NODI DELL’ALBERO */
printf("Altezza dell'albero: %d\n", height(indice));
printf("Num. Nodi dell'albero: %d\n", nodi(indice));
$ TO DO – passo 1!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Mumble
mumble …
Il main da realizzare deve: a) leggere il contenuto del file
ricordando il numero del blocco letto e inserendo chiave,
posizione come elemento in un albero binario di ricerca, e
stampare l’elenco ordinato dei cognomi; b) calcolarne
numero di nodi e altezza; c) letto un cognome e accedendo
all’albero recuperare le informazioni complete di quel
cognome dal file; d) stampare i cognomi minori di quello
letto.
tree.c (1)
#include <stdlib.h>
#include "tree.h“
boolean empty(tree t)
/* test di albero vuoto */
{ return (t==NULL); }
tree emptytree(void)
/* inizializza un albero vuoto */
{ return NULL; }
14
tree.c (2)
el_type root (tree t)
/* restituisce la radice dell'albero t */
{ if (empty(t)) abort();
else return(t->value); }
tree left (tree t)
/* restituisce il sottoalbero sinistro */
{ if (empty(t)) return(NULL);
else return(t->left); }
tree right (tree t)
/* restituisce il sottoalbero destro */
{ if (empty(t)) return(NULL);
else return(t->right); }
15
tree.c (3)
tree cons_tree(el_type e, tree l, tree r)
/* costruisce un albero che ha nella
radice e; per sottoalberi sinistro e
destro l ed r rispettivamente
*/
{ tree t;
t = (NODO *) malloc(sizeof(NODO));
t-> value = e;
t-> left = l;
t-> right = r;
return (t); }
16
tree.c (4)
void inorder(tree t)
{ if (! empty(t))
{ inorder(left(t));
showel( root(t) );
inorder(right(t));
}
}
17
tree.c (5 – ord_ins)
tree ord_ins(el_type e, tree t)
/* albero binario di ricerca senza duplicazioni */
{ if (empty(t))
/* inserimento */
return cons_tree(e,emptytree(),emptytree());
else
{ if (isless(e,root(t)))
t->left = ord_ins(e,left(t));
else t->right = ord_ins(e,right(t));
return t;
}
}
18
BST – Inserimento iterativo!
n 
L’algoritmo di inserimento è simile a quello di ricerca
•  Cerca, nell’albero, la posizione corretta di inserimento, ovvero il
nodo che diventerà il padre di quello da inserire
•  Una volta trovato il nodo, appende il nuovo nodo come figlio
sinistro/destro in modo da mantenere la proprietà di ordinamento
dell’albero
Inserimento
di 16
8
3
2
12
5
15
6
13
18
16
19
$ Inserimento iterativo
tree ord_ins_it(el_type e,tree root)
{tree p, t=root;
if (root==NULL) return cons_tree(e,NULL,NULL);
else
{ while (t!=NULL)
if (isLess(e,t->value))
{p=t; t=t->left;}
else
{p=t; t=t->right;}
}
//p punta al padre
if (isLess(e,p->value))
p->left = cons_tree(e,NULL,NULL);
else
p->right = cons_tree(e,NULL,NULL);
return root; }
20
main.c (soluzione)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "trees.h"
main() {
tree indice=emptytree();
int pos;
FILE *f;
record_type REC;
el_type EL;
char COGNOME[15];
f = fopen("PERSONE.DAT", "rb");
if(f==NULL){ printf("errore apertura file\n");
exit(-1);
}
pos=0; /* CICLO DI SCANSIONE DEL FILE */
while(fread(&REC,sizeof(record_type),1,f)>0)
{ strcpy(EL.cognome,REC.cognome);
EL.pos=pos;
/* INSERIMENTO NELL'ALBERO */
printf("Inserimento nell’albero di %s
con posizione %d\n", EL.cognome, pos);
indice = ord_ins(EL,indice);
pos++;
}
main.c (2 - soluzione)
fclose(f);
printf("Chiusura del file\n");
printf("Stampa dell’albero\n");
inorder(indice);
/* CALCOLO ALTEZZA E NUM.NODI DELL’ALBERO */
printf("Altezza dell'albero: %d\n", height(indice));
printf("Num. Nodi dell'albero: %d\n", nodi(indice));
$ TO DO – passo 2!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Il main da realizzare deve:
a) leggere il contenuto del file ricordando il numero del
blocco letto e inserendo chiave, posizione come elemento in
un albero binario di ricerca, e stampare l’elenco ordinato dei
cognomi;
b) calcolarne numero di nodi e altezza;
c) letto un cognome e accedendo all’albero recuperare le
informazioni complete di quel cognome dal file;
d) stampare i cognomi minori di quello letto.
CONTARE I NODI (Albero binario)
Algoritmo (per un albero binario)
n  se l'albero è vuoto, i nodi sono 0
n  altrimenti, i nodi sono 1 (la radice) +
quelli del figlio sinistro + quelli del figlio destro
1 nodo (radice)
Numero di
nodi: nL
Numero di
nodi: nR
sottoalbero
sinistro
Totale nodi: 1 + nL + nR
sottoalbero
destro
tree.c (7)
Int nodi(tree t)
{ if (empty(t)) return 0;
else
return (1+nodi(left(t))+nodi(right(t)));
}
n 
E’ tail ricorsiva?
25
ALTEZZA DI UN ALBERO
ALTEZZA DI UN ALBERO (Albero binario)
Algoritmo (per un albero binario)
n  se l'albero è vuoto, l’altezza è nulla (0)
n  altrimenti, è il massimo tra l’altezza_aux del figlio
sinistro e l’altezza_aux del figlio destro
1 arco (lo conto in
altezza_aux, solo se
figlio non vuoto)
Altezza_aux L
Altezza_aux R
sottoalbero
sinistro
sottoalbero
destro
Altezza: 1+ max(height_aux(L),height_aux(R))
$ To Do: altezza di un albero
Altezza di un albero, lunghezza del cammino più lungo
dalla radice a una delle foglie
Algoritmo:
n  se l'albero è vuoto, o ha solo un nodo (radice) la sua
altezza è 0
n  altrimenti, è 1 +
max( altezza del figlio sinistro , altezza del figlio destro )
n 
3
8
11
2
tree.c (8 - height)
int height (tree t)
{ if (empty(t)) return 0;
else return max_int( height_aux(left(t)),
height_aux(right(t))) ;
}
int height_aux(tree t)
{ if (empty(t)) return 0;
else return (1 + max_int( height_aux(left(t)),
height_aux(right(t)) ));
}
n 
E’ tail ricorsiva?
29
$ To Do: altezza di un albero
9
8
14
7
6
5
3
4
13
11
$ TO DO – passo 3!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Il main da realizzare deve:
a) leggere il contenuto del file ricordando il numero del
blocco letto e inserendo chiave, posizione come elemento in
un albero binario di ricerca, e stampare l’elenco ordinato dei
cognomi;
b) calcolarne numero di nodi e altezza;
c) letto un cognome e accedendo all’albero recuperare le
informazioni complete di quel cognome dal file;
d) stampare i cognomi minori di quello letto.
main.c (3 - stub)
/* VISUALIZZAZIONE RECORD */
printf("Inserisci un cognome: ");
scanf("%s", COGNOME);
f = fopen("PERSONE.DAT", "rb");
if(f==NULL){
printf("errore apertura file\n");
exit(-1);
}
// invocare opportunamente member_ord,
// trovare il record sul file con fseek
// e stampare risultato
// invocare opportunamente stampa_minori
}
/* VISUALIZZAZIONE RECORD */
printf("Inserisci un cognome: ");
scanf("%s", COGNOME);
f = fopen("PERSONE.DAT", "rb");
if(f==NULL){
printf("errore apertura file\n");
exit(-1);
}
strcpy(EL.cognome, COGNOME);
pos = member_ord(EL, indice);
if (pos>=0){
fseek(f, pos*sizeof(record_type), SEEK_SET);
if(fread(&REC,sizeof(record_type),1,f)>0){
printf("Posizione %d:
Cognome:%s\tNome:%s\tDatan:
%s\tCitta:%s\n",
pos,REC.cognome, REC.nome, REC.datan, REC.citta);
}
stampa_minori(indice, EL);
}
}
Ricerca binaria - iterativa!
1. 
2. 
Sia t un puntatore ad albero binario (tree) e gli
sia inizialmente assegnata la radice dell’albero
Finché t non è nullo e la sua radice non contiene
il valore cercato, confrontare il valore contenuto
nella radice di t con il valore cercato
a.  Se è uguale, restituire il valore trovato
b.  Se è minore, assegnare a t il figlio destro e
procedere con 2
c.  Se è maggiore, assegnare a t il figlio sinistro
e procedere con 2
34
Ricerca (iterativa) in BST!
n 
E’possibile decidere, per ogni nodo, se proseguire la
ricerca a sinistra o a destra (ricerca binaria), aggiornando
il puntatore t
Ricerca di
13
8
3
2
12
5
15
6
13
18
35
$ TO DO – Passo 3!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Il main da realizzare deve:
a) leggere il contenuto del file ricordando il numero del
blocco letto e inserendo chiave, posizione come elemento in
un albero binario di ricerca, e stampare l’elenco ordinato dei
cognomi;
b) calcolarne numero di nodi e altezza;
c) letto un cognome e accedendo all’albero recuperare le
informazioni complete di quel cognome dal file;
d) stampare i cognomi minori di quello letto.
/* VISUALIZZAZIONE RECORD */
printf("Inserisci un cognome: ");
scanf("%s", COGNOME);
f = fopen("PERSONE.DAT", "rb");
if(f==NULL){
printf("errore apertura file\n");
exit(-1);
}
strcpy(EL.cognome, COGNOME);
pos = member_ord(EL, indice);
if (pos>=0){
fseek(f, pos*sizeof(record_type), SEEK_SET);
if(fread(&REC,sizeof(record_type),1,f)>0){
printf("Posizione %d:
Cognome:%s\tNome:%s\tDatan:
%s\tCitta:%s\n",
pos,REC.cognome, REC.nome, REC.datan, REC.citta);
}
// stampa_minori(indice, EL);
}
}
tree.c (7)
int member_ord(el_type e,tree t)
{ if (empty(t)) return -1;
else
if (isequal(e,root(t))
return (t->value).pos;
else
if (isless(e,root(t))
return member_ord(e,left(t));
else return member_ord(e,right(t)) ;
}
n 
E’ tail ricorsiva?
39
tree.c (6 – member_ord)
int member_ord(el_type e, tree t)
{ while (t!=NULL){
if (isless(e,root(t)))
t=t->left;
else
if (isequal(e,root(t)))
return (t->value).pos;
else
t=t->right;
}
return -1;
}
40
$ Esercizio 10.3.0!
Si modifichino i file main.c e tree.c già disponibili,
implementando le operazioni richieste in funzione di quelle
esportate dall’ADT degli elementi
Il main da realizzare deve:
a) leggere il contenuto del file ricordando il numero del
blocco letto e inserendo chiave, posizione come elemento in
un albero binario di ricerca, e stampare l’elenco ordinato dei
cognomi;
b) calcolarne numero di nodi e altezza;
c) letto un cognome e accedendo all’albero recuperare le
informazioni complete di quel cognome dal file;
d) stampare i cognomi minori di quello letto.
Stampa_minori
Montale
STAMPA MINORI
Montale
Hesse
Alberoni
Pennac
Marquez
sottoalbero
sinistro
Ziliotto
sottoalbero
destro
Stampa_minori
Montale
STAMPA MINORI
Montale
Hesse
Alberoni
Marquez
sottoalbero
sinistro
n 
n 
Pennac
Ziliotto
sottoalbero
destro
Modifica della visita inorder; stampo il contenuto del nodo
solo se il cognome precede quello dato
Non efficientissima (visito tutti i nodi così, anche quelli con
cognome sicuramente maggiore)
tree.c
void stampa_minori(tree t, el_type e)
/* ricorsiva – visita in ordine simmetrico */
{
if (!empty(t))
{ stampa_minori(left(t),e);
if (isless(root(t),e)) showel(root(t));
stampa_minori(right(t),e);
}
}
UN PO’ DI CONSIDERAZIONI …!
n  Quanto
è bilanciato l’albero binario di ricerca
ottenuto?
n  Quanti
nodi? à 10
Quale altezza? à 6
n  Ci
sono molti algoritmi per bilanciare alberi non
bilanciati
non bilanciato
bilanciato
Alberi bilanciati!
n  Il
problema di BST è che normalmente NON
sono bilanciati:
•  Inserimenti e cancellazioni sbilanciano l’albero
•  Se l’albero non è correttamente bilanciato le
operazioni (tutte) costano “parecchio”
n  Soluzione:
alberi che si autobilanciano
•  AVL (Adel'son-Vel'skii-Landis)
•  Red-Black
•  …
52
Alberi AVL!
n 
n 
n 
n 
Un albero AVL è un Albero Binario di Ricerca
bilanciato
Un nodo si dice bilanciato quando l’altezza del
sotto-albero sinistro differisce dall’altezza del
sotto-albero destro di al più una unità
Un albero si dice bilanciato quando tutti i nodi
sono bilanciati
Le operazioni sono le stesse che si possono
eseguire su un albero binario di ricerca
53
Rotazioni!
n  Si
supponga di disporre di un albero
sbilanciato – è possibile bilanciarlo tramite
opportune rotazioni Ruotare il nodo 12 verso sinistra in
8
modo che diventi la radice
dell’albero à il padre di 12 (8)
diventa il suo sotto-albero
sinistro
12
15
13
18
16
20
54
Rotazioni!
n  Rotazione
1: il nodo 12 diventa la radice
Ruotare il nodo 15 verso sinistra
in modo che diventi la radice
dell’albero à il padre di 15 (12)
diventa il suo sotto-albero
sinistro, il sotto-albero sinistro
di 15 (13) diventa il sotto-albero
destro di 12
12
8
15
13
18
16
20
55
Rotazioni!
n  Rotazione
2: il nodo 15 diventa la radice e
l’albero risulta bilanciato
15
12
8
18
13
16
20
56
Alberi AVL!
n 
n 
n 
n 
Si supponga di partire con un albero bilanciato,
secondo la definizione data in precedenza
Una serie di inserimenti/cancellazioni può
sbilanciare l’albero
Opportune rotazioni sono in grado di ribilanciare
l’albero
Naturalmente i costi di inserimento/cancellazione
crescono di molto ma la ricerca rimane sempre
molto efficiente! (O(log2 N) se N nodi)
57
Per giocare un po’…!
http://www.qmatica.com/DataStructures/Trees/AVL/AVLTree.html
n 
Applet java per giocare con alberi di ricerca di vario tipo,
con possibilità di inserire, cancellare, ruotare e vedere
come si comportano i vari tipi di alberi supportati
http://cprogramminglanguage.net/avl-tree.aspx
n  Realizzazione modulare in C delle operazioni su AVL tree
(insert, delete da fare)
n  … ma anche RedBlack tree
58
59
Esercizi molto consigliati…!
•  Scrivere la ricerca in albero binario di
ricerca in modo iterativo
•  Calcolare l’altezza di un albero
•  Calcolare il bilanciamento di un nodo
(differenza fra le altezze dei sotto-alberi
sinistro e destro)
60
Scarica