Range query su B-tree F. d’Amore 4 luglio 2002 Problema Dato un B-tree di ordine m, le cui chiavi sono numeri reali, e dato un intervallo [α, β] dell’asse reale, progettare ed analizzare un algoritmo per l’individuazione di tutte le chiavi ξ ∈ [α, β] presenti in B. Soluzione Algoritmo Nella descrizione della soluzione di farà riferimento alla classe BTreeNode per la rappresentazione Java dei nodi di un B-tree. class BTreeNode { int m; boolean leaf; int keyTally; // n.ro chiavi int keys = new int[m-1]; BTreeNode references[] = new BTreeNode[m]; // metodi non riportati } L’algoritmo che verrà usato, BTreeRangeSearch, è una variante della visita DFS di un albero m-ario, il cui schema generale di lavoro è il seguente: 1. visita (ricorsivamente) primo sottoalbero; 2. visita nodo; 3. visita (ricorsivamente) ordinatamente tutti i sottoalberi successivi al primo. Nel caso specifico del B-tree di ordine m la visita DFS può essere cosı̀ descritta. Algorithm BTreeDFS(BTreeNode v) { if(v == null) return; for(i = 0; i < v.keyTally; i++) { BTreeDFS(v.references[i]); <visita v.keys[i]> } BTreeDFS(v.references[v.keyTally]); } Si tratta, come è evidente, di una generalizzazione della visita DFS in ordine simmetrico. L’algoritmo che usiamo per risolvere la range query è una semplice variante di BTreeDFS, in cui la visita viene opportunamente “sfrondata” poiché non siamo interessati alle chiavi esterne 1 all’intervallo assegnato. In particolare, osserviamo che ad ogni sottoalbero Ts (sottoalbero avente radice nel nodo s) è possibile associare un intervallo Is = [min(Ts ), max(Ts )], dove min(Ts ) e max(Ts ) sono, rispettivamente la minima e la massima chiave presente nel sottoalbero Ts . È chiaro che converrà visitare un sottoalbero Ts se e solo se Is e [α, β] non hanno intersezione vuota (se hanno intersezione vuota sicuramente Ts non può contenere chiavi interne all’intervallo [α, β]). Dunque, è opportuno effettuare tale controllo prima di iniziare la visita di un qualunque sottoalbero. Il problema pratico che nasce è ora: dato un nodo s, come posso ottenere Is ? In effetti, mentre il calcolo esatto di Is può essere computazionalmente rilevante, è invece facile l’individuazione di un intervallo (aperto) Is0 ⊃ Is , per tutti i nodi che abbiano un genitore: è sufficiente tener conto della chiave o delle chiavi che nel genitore delimitano lo spazio attribuito al sottoalbero. Più precisamente, a causa della proprietà che rende “di ricerca” un B-tree, e dunque per costruzione, dato un nodo v, l’intervallo I 0 associato al sottoalbero v.references[i], per 0 < i < v.keyTally, è (v.keys[i-1],v.keys[i]); se i == 0 l’intervallo è (−∞,v.keys[0]); se i == v.keyTally l’intervallo è (v.keys[v.keyTally-1],+∞). Chiaramente, ogni nodo s tale che Is0 ∩ [α, β] = ∅ è radice di un sottoalbero che non contiene chiavi interessanti. Algorithm BTreeRangeSearch(BTreeNode v, double alpha, double beta) { if(v == null) return; for(i = 0; i < v.keyTally; i++) { if(v.keys[i] >= beta) { BTreeRangeSearch(v.references[i], alpha, beta); if(v.keys[i] == beta) <visita v.keys[i]> halt; // termina l’elaborazione } else if(v.keys[i] >= alpha) { if(v.keys[i] > alpha) BTreeRangeSearch(v.references[i], alpha, beta); <visita v.keys[i]> } } /* se l’algoritmo non si e’ arrestato deve essere evidentemente v.keys[i] > beta */ BTreeRangeSearch(v.references[v.keyTally]); } Analisi Una prima analisi, ovvia ma corretta, ci consente di valutare il costo temporale dell’algoritmo in O(2n/m) accessi al disco, in cui n è il numero delle chiavi ed m è l’arità dell’albero. Infatti, vengono al più visitati tutti i nodi e il numero di nodi appartiene approssimativamente1 all’intervallo [n/m, 2n/m]. Una valutazione più raffinata separa le componenti di costo, tenendo conto della modalità di lavoro dell’algoritmo: dapprima la visita scende nell’albero, in pratica cercando la chiave alpha; successivamente, appena trovata alpha o la prima chiave superiore ad alpha (ma non superiore a beta), l’algoritmo comincia a riportare le chiavi nell’intervallo [alpha, beta], terminando appena si incontra o si supera la chiave beta. Nella prima parte l’algoritmo spenderà al più un numero di accessi al disco pari all’altezza dell’albero; nella seconda, se k chiavi rispondono alla query, saranno spesi al più 2k/m (approssimativamente il numero max di nodi che contengono k chiavi consecutive) accessi al disco. In definitiva, O(logm/2 n) accessi nella prima parte e O(2k/m) accessi nella seconda. In totale, O(logm/2 n + 2k/m) accessi al disco. 1 Si lascia al lettore l’esercizio di valutare con precisione la quantità massima e la quantità minima di nodi in un B-tree di ordine m con n chiavi. 2 Spunti per esercizi 1. Valutare il costo dell’algoritmo BTreeRangeSearch in funzione di n (numero di chiavi) e di B (dimensione del blocco), e non in funzione di m (arità). 2. Scrivere una variante dell’algoritmo BTreeRangeSearch in cui le chiavi richieste vengono riportate in ordine inverso (dalla più grande alla più piccola). 3. Ipotizzando che le chiavi siano memorizzate all’interno dei nodi in array ordinati, modificare l’algoritmo BTreeRangeSearch in maniera da sfruttare tale ordinamento, effettuando ricerche binarie all’interno dei nodi. Come si modifica la complessità asintotica dell’algoritmo? 4. Modificare l’implementazione del B-tree prevedendo la memorizzazione esplicita, all’interno di ciascun nodo, della massima e della minima chiave presenti nel sottoalbero avente tale nodo come radice. Modificare opportunamente gli algoritmi di aggiornamento (inserimento ed eliminazione) del B-tree. In presenza di tale implementazione di un B-tree come è possibile semplificare l’algoritmo BTreeRangeSearch? 5. Risolvere il problema della range query su un BST. 3