920 likes | 1.15k Views
Lezione 12. B-Alberi Algoritmi su grafi. Sommario. B-Alberi definizione ricerca inserimento Rappresentazione dei grafi Visita in ampiezza Visita in profondità Ordinamento topologico. B-Alberi. I B-Alberi sono una generalizzazione degli alberi binari di ricerca
E N D
Lezione 12 B-Alberi Algoritmi su grafi
Sommario • B-Alberi • definizione • ricerca • inserimento • Rappresentazione dei grafi • Visita in ampiezza • Visita in profondità • Ordinamento topologico
B-Alberi • I B-Alberi sono una generalizzazione degli alberi binari di ricerca • la principale differenza è che i B-Alberi • ogni nodo dell’albero può contenere n>2 chiavi • il grado di un nodo è alto (50-2000) • i B-Alberi sono utilizzati per garantire l’efficienza delle operazioni su insiemi dinamici (ricerca, inserzione e cancellazione) di dati memorizzati su supporti secondari (dischi)
Visualizzazione M D H Q T X B C F G J K L N P Y Z R S V W
Memorie Secondarie • La memoria primaria (RAM) si basa su una tecnologia costosa ma che permette di eseguire le operazioni di scrittura e lettura in modo veloce • la memoria secondaria (dischi) è più lenta (vi sono componenti meccaniche da muovere), ma più economica • questo permette di rendere disponibile una quantità di memoria secondaria di uno o due ordini di grandezza maggiore della memoria primaria
Memorie Secondarie • Le informazioni in un disco sono organizzate in blocchi • il blocco minimo accessibile in lettura e scrittura è detto pagina • una pagina corrisponde a circa 2 MB
Accesso alla memoria secondaria • Per trattare quantità estremamente grandi di dati si devono pertanto sviluppare algoritmi che lavorino con dati memorizzati in memoria secondaria • si devono pertanto minimizzare gli accessi alla memoria oltre che garantire efficienza computazionale di CPU • Le operazioni di accesso ai dati negli algoritmi vengono modificate in: x=puntatore a un dato Disk-Read(x) …operazioni di elaborazione di x Disk-Write(x) …operazioni che accedono a x in sola lettura
Accesso alla memoria secondaria • Le operazioni di lettura su disco si intendono fatte nel caso in cui il dato puntato da x non sia già disponibile nella memoria primaria • le operazioni di scrittura vengono invece eseguite solo se il dato puntato da x è stato in qualche modo modificato
B-Alberi • In ogni istante è possibile mantenere in memoria primaria solo un numero limitato di pagine • le operazioni eseguite su i B-Alberi garantiscono di poter essere eseguite conservando solo un numero costante di pagine in memoria principale (tante più pagine tanto più efficienti saranno le varie operazioni) • in genere un nodo di un B-Albero e tanto grande quanto una pagina di memoria secondaria • Nota: nel presentare gli algoritmi si trascurerà la gestione di basso livello della memoria
B-Alberi • Per semplicità si suppone di memorizzare in un nodo solo la chiave dei dati • un eventuale puntatore associato alla chiave servirà per indirizzare la pagina del disco su cui trovare i dati satellite
Definizione dei B-Alberi • un B-Albero è un albero radicato T che soddisfa le seguenti proprietà: • ogni nodo x è caratterizzato dai seguenti attributi: • n[x] numero delle chiavi memorizzate in x • le n[x] chiavi sono memorizzate in ordine decrescente • leaf[x] è true se il nodo è una foglia, false altrimenti • un nodo interno x contiene n[x]+1 puntatori c1[x], c2[x],…, cn[x]+1[x] ai suoi figli (o NIL se x è una foglia) • i campi keyi[x] definiscono gli intervalli delle chiavi memorizzate in ciascun sottoalbero: se ki è una qualunque chiave memorizzata nel sottoalbero di radice ci[x] allora k1 key1[x] k2 key2[x] … keyn[x][x] kn[x]+1 • tutte le foglie sono alla stessa profondità, che coincide con l’altezza dell’albero
Definizione dei B-Alberi • il numero delle chiavi per ogni nodo è limitato sia inferiormente che superiormente in funzione di un intero t chiamato grado minimo del B-Albero • t 2 • ogni nodo (eccetto la radice) contiene almeno t-1 chiavi • ogni nodo interno (eccetto la radice) ha almeno t figli • ogni nodo può contenere al massimo 2t-1 chiavi • ogni nodo interno può avere al massimo 2t figli • un nodo è detto pieno se contiene esattamente 2t-1 chiavi
Altezza di un B-Albero • Un B-Albero con n chiavi e grado minimo t ha una altezza h logt (n+1)/2 • Infatti: il caso peggiore è che un B-Albero abbia una radice con un’unica chiave e che tutti i nodi contengano il numero minimo di chiavi, cioè t-1 • a profondità 1 ci saranno pertanto 2 nodi, a profondità 2, 2t nodi, a profondità 3, 2t2 nodi. • Ogni nodo contiene t-1 chiavi • pertanto il numero totale di chiavi n deve essere: • n 1 + (t-1)i=1..h 2ti-1= 1+2(t-1)(th-1)/(t-1)=2th-1 • ovvero h logt (n+1)/2
Operazioni sui B-Alberi • La radice del B-Albero è sempre in memoria principale • non devono pertanto essere effettuate operazioni di Disk-Read per leggere la radice • tuttavia se si modifica la radice deve essere eseguita una operazione di Disk-Write • si suppone che per tutti i nodi passati come parametro alle varie procedure si sia correttamente compiuta l’operazione di Disk-Read • tutte le procedure che vedremo sono a “singola passata” cioè algoritmi che visitano l’albero a partire dalla radice e non risalgono mai indietro
Ricerca • E’ un operazione simile alla ricerca sugli alberi binari di ricerca • la differenza è che non ci sono solo due vie possibili ad ogni nodo, ma n[x]+1 • la procedura B-Tree-Search • prende in ingresso il puntatore alla radice dell’albero e la chiave da cercare • restituisce la coppia ordinata (y,i) che consiste di un puntatore a nodo y e un indice i tale che keyi[y]=k
Pseudocodice per la Ricerca B-Tree-Search(x,k) 1 i 1 2 while i n[x] e k > keyi[x] 3 do i i+1 4 if i n[x] e k = keyi[x] 5 then return (x,i) 6 if leaf[x] 7 then return NIL 8 else DISK-READ(ci[x]) 9 return B-Tree-Search(ci[x],k)
Spiegazione pseudocodice • Nelle linee 1-3 si esegue una ricerca lineare per trovare il più piccolo indice i tale che k keyi[x] • in 4-5 si controlla se la chiave è stata trovata • altrimenti 6-9 o siamo in una foglia e la ricerca termina senza successo • o procediamo ricorsivamente su un opportuno sottoalbero del nodo in esame che contiene chiavi comprese fra un valore sicuramente più piccolo di k e uno più grande
Visualizzazione Ricerca della chiave R M D H Q T X B C F G J K L N P Y Z R S V W
Analisi • La ricerca procede dalla radice lungo un cammino verso una foglia • il numero di accessi è pertanto O(h)=O(logtn) • poiché il numero di chiavi in un nodo è n[x]<2t la ricerca lineare 2-3 impiega per esaminare un qualsiasi nodo un tempo O(t) • il tempo complessivo sarà pertanto O(t logtn)
Costruzione di un B-Albero • Per costruire un B-Albero si utilizza una procedura B-Tree-Create per creare un nodo radice vuoto • poi si utilizza la procedura B-Tree-Insert per inserire ogni nodo • entrambe queste procedure fanno uso di una procedura ausiliaria Allocate-Node() che ha il compito di creare un nuovo nodo e di assegnargli una opportuna pagina del disco in tempo O(1)
Pseudocodice per la costruzione della radice di un B-Albero B-Tree-Create 1 x Allocate-Node() 2 leaf[x] true 3 n[x] 0 4 Disk-Write(x) 5 root[T] x
Divisione di un nodo in un B-Albero • L’operazione di inserzione di un nodo è complicata dal fatto che se la nuova chiave deve essere memorizzata in un nodo pieno allora bisogna procedere a dividere questo nodo in due • un nodo pieno y con 2t-1 chiavi viene diviso in due nodi di t-1 chiavi all’altezza della chiave mediana keyt[y] • la chiave mediana viene spostata nel nodo padre • se y è la radice si aumenta l’altezza dell’albero: è infatti questo il meccanismo di crescita dei B-Alberi
Visualizzazione keyi-1[x] keyi[x] keyi[x] keyi+1[x] x … N W …. … N S W …. y=ci[x] P Q R S T U V P Q R T U V y=ci[x] z=ci+1[x]
Idea intuitiva • La procedura ha come parametri un nodo interno x non pieno, un indice i e un nodo y pieno. • y è il figlio i-esimo di x. • In origine y ha 2t-1 chiavi, dopo la divisione rimane con i t-1 chiavi minori • un nuovo nodo z acquisisce i t-1 chiavi maggiori e diventa un figlio di x dopo y • la chiave mediana di y viene rimossa da y e posta in x e diventa la chiave che separa y da z
Divisione di un nodo B-Tree-Split-Child(x,i,y) 1 z Allocate-Node() 2 leaf[z] leaf[y] 3 n[z] t-1 4 for j 1 to t-1 5 do keyj[z] keyj+t[y] 6 if not leaf[y] 7 then for j 1 to t 8 do cj[z] cj+t[y] 9 n[y] t-1 10 for j n[x]+1 downto i+1 11 do cj+1[x] cj[x] 12 cj+1[x] z 13 for j n[x] downto i 14 do keyj+1[x] keyj[x] 15 keyi[x] keyt[y] 16 n[x] n[x]+1 17 Disk-Write(y); Disk-Write(z); Disk-Write(x)
Spiegazione dello pseudocodice • Le linee 1-8 creano un nuovo nodo z e gli assegnano le t-1 chiavi più grandi di y, assieme ai figli corrispondenti • in 10-14 si inserisce z come nuovo figlio di x • in 15 si inserisce la chiave mediana di y come separatore • in 16 si modifica il contatore delle chiavi n[x] • in 17 si riporta su disco le modifiche effettuate
Analisi • Il tempo di esecuzione è dominato dai cicli alle linee 4 o 7 o 10 o 13 che impiegano tutti un tempo limitato superiormente da O(t)
Inserimento di una nuova chiave • L’inserimento di una nuova chiave può avvenire in due casi: • quando il nodo radice è pieno • quando il nodo radice non è pieno • La procedura B-Tree-Insert inserisce una nuova chiave k in un B-Albero e gestisce il caso in cui si debba inserire la chiave in una radice piena • in questo caso si aumenta di 1 l’altezza dell’albero inserendo una nuova radice • ci si riporta così al caso di inserimento in un albero con radice non piena che viene trattato dalla procedura B-Tree-Insert-Nonfull
Visualizzazione root[T] H s root[T] A D F H L N P A D F L N P r r
Pseudocodice per l’inserimento di una nuova chiave B-Tree-Insert(T,k) 1 r root[T] 2 if n[r] = 2t-1 3 then s Allocate-Node() 4 root[T] s 5 leaf[s] false 6 n[s] 0 7 c1[s] 0 8 B-Tree-Split-Child(s,1,r) 9 B-Tree-Insert-NonFull(s,k) 10 else B-Tree-Insert-NonFull(r,k)
Inserimento in nodo non pieno • La procedura è organizzata in modo tale da essere chiamata sempre solo su nodi non pieni • la procedura distingue il caso in cui si debba inserire la nuova chiave in un nodo foglia o si debba scendere ricorsivamente in un nodo interno • per un nodo foglia si deve gestire la collocazione della chiave nella giusta posizione e aggiornare il numero di chiavi • per un nodo interno si deve verificare che questo non sia pieno per poter applicare ricorsivamente la B-Tree-Insert-Nonfull • nel caso in cui sia un nodo pieno si richiama la procedura B-Tree-Split-Child
Pseudocodice per l’inserimento B-Tree-Insert-Nonfull(x,k) 1 i n[x] 2 if leaf[x] 3 then while i1 e k<keyi[x] 4 do keyi+1[x] keyi[x] 5 i i-1 6 keyi+1[x] k 7 n[x] n[x]+1 8 Disk-Write(x) 9 else while i1 e k<keyi[x] 10 do i i -1 11 i i+1 12 Disk-Read(ci[x]) 13 if n[ci[x]]=2t-1 14 then B-Tree-Split-Child(x,i,ci[x]) 15 if k > keyi[x] 16 then i i+1 17 B-Tree-Insert-Nonfull(ci[x],k)
Spiegazione pseudocodice • In 3-8 ci si occupa del caso di inserimento della chiave nel nodo foglia: si determina la posizione della chiave facendole contemporaneamente posto • in 9-17 si considera il caso in cui si debba scendere ricorsivamente attraverso nodi interni • in 9-11 si determina quale figlio esaminare • in 13 se il figlio è pieno si divide e in 15-16 si determina per quale dei due nuovi sotto figli si debba proseguire • in 17 si procede ricorsivamente su un nodo figlio sicuramente non pieno fino a raggiungere una foglia
Visualizzazione inserzione della chiave B in B-Albero con t=3 G M P X A C D E J K N O R S T U V Y Z G M P X A B C D E J K N O R S T U V Y Z
Visualizzazioneinserzione della chiave Q G M P X A B C D E J K N O R S T U V Y Z G M P T X A B C D E J K N O Q R S Y Z U V
Visualizzazioneinserzione della chiave L G M P T X A B C D E J K N O Q R S Y Z U V P G M T X A B C D E L J K N O Q R S Y Z U V
Visualizzazioneinserzione della chiave F P G M T X A B C D E L J K N O Q R S Y Z U V P C G M T X A B L J K N O Q R S Y Z D E F U V
Analisi • Per un B-Albero di altezza h la procedura B-Tree-Insert effettua O(h) accessi al disco • infatti: • questa richiama la procedura B-Tree-Insert-Nonfull ricorsivamente su un numero di nodi al più numeroso come il massimo cammino fino ad una foglia (h) • inoltre la procedura B-Tree-Insert-Nonfull esegue un numero O(1) di operazioni di lettura-scrittura
Analisi • per il tempo di computazione di CPU si ha che B-Tree-Insert-Nonfull ha un ciclo O(t) (linea 3 o 9) • inoltre richiama una volta la procedura B-Tree-Split-Child che costa O(t) • dato che B-Tree-Insert-Nonfull viene chiamata ricorsivamente al più O(h) volte si ha complessivamente un costo O(th)=O(t logtn)
Grafi • I grafi sono strutture dati molto diffuse in informatica • Vengono utilizzati per rappresentare reti e organizzazioni dati complesse e articolate • Per elaborare i grafi in genere è necessario visitarne in modo ordinato i vertici • Vedremo a questo proposito due modi fondamentali di visita: per ampiezza e per profondità
Nota sulla notazione asintotica • Il tempo di esecuzione di un algoritmo su un grafo G=(V,E) viene dato in funzione del numero di vertici |V| e del numero di archi |E| • Utilizzando la notazione asintotica adotteremo la convenzione di rappresentare |V| con il simbolo V e |E| con E: quando diremo che il tempo di calcolo è O(E+V) vorremo significare O(|E|+|V|)
Rappresentazione di un grafo • Vi sono due modi per rappresentare un grafo: • collezione di liste di adiacenza • matrice di adiacenza • si preferisce la rappresentazione tramite liste di adiacenza quando il grafo è sparso, cioè con |E| molto minore di |V|2 • si preferisce la rappresentazione tramite matrice di adiacenza quando, al contrario, il grafo è denso o quando occorre alta efficienza nel rilevare se vi è un arco fra due vertici dati
Liste di adiacenza • Si rappresenta un grafo G=(V,E) con un vettore Adj di liste, una lista per ogni vertice del grafo • per ogni vertice u, Adj[u] contiene tutti i vertici v adiacenti a u, ovvero quei vertici v tali per cui esiste un arco (u,v)E • in particolare questo insieme di vertici è memorizzato come una lista • l’ordine dei vertici nella lista è arbitrario
Visualizzazione:grafo non orientatocon liste di adiacenza 1 2 2 1 2 3 4 5 5 1 3 4 5 2 4 3 2 5 3 4 1 2 5 4
Visualizzazione:grafo orientato con liste di adiacenza 1 2 1 2 3 4 5 6 2 4 5 6 5 3 2 4 4 5 6 6
Proprietà della rappresentazione con liste di adiacenza • Se un grafo è orientato allora la somma delle lunghezze di tutte le liste di adiacenza è |E| • infatti per ogni arco (u,v) c’è un vertice v nella lista di posizione u • Se un grafo non è orientato allora la somma delle lunghezze di tutte le liste di adiacenza è 2|E| • infatti per ogni arco (u,v) c’è un vertice v nella lista di posizione u e un vertice u nella lista di posizione v • La quantità di memoria necessaria per memorizzare un grafo (orientato o non) è O(max(V,E)) = O(V+E)
Grafi pesati • In alcuni problemi si vuole poter associare una informazione (chiamata peso) ad ogni arco • un grafo con archi con peso si dice grafo pesato • si dice che esiste una funzione peso che associa ad un arco un valore w : E R • ovvero un arco (u,v) ha peso w(u,v)
Grafi pesati con liste di adiacenza • Si memorizza il peso w(u,v) insieme al vertice v nella lista per il vertice u
Visualizzazione:grafo orientato pesato con liste di adiacenza 0.2 1 2 1 2 3 4 5 6 2 0.2 4 0.3 5 0.4 0.1 6 0.2 5 0.6 0.4 3 0.3 2 0.1 0.6 0.2 4 0.5 4 5 0.8 6 0.5 6 0.8
Svantaggi della rappresentazione con liste di adiacenza • Per sapere se un arco (u,v) è presente nel grafo si deve scandire la lista degli archi di u