Heap Proprietà di un Heap binario: • • • • Ogni nodo figlio è più piccolo del padre (max heap) { key[ x] ≤ key[ P[ x]] } Lʹultimo livello deve esser riempito partendo da sinistra verso destra Altezza è uguale ad O(lgn) Può essere rappresentato da una stringa PARENT[i] Return ⎣i / 2⎦ LEFT(i) Return 2i RIGHT(i) Return 2i + 1 Dove con i indichiamo la posizione del nodo. Heapify: Durante scambi, eliminazioni o inserimenti di nodi, il nostro heap viene modificato, e può accadere che non vengano più rispettate le proprietà iniziali. Per rimediare a questo viene richiamata la procedura Heapify sul nodo su cui abbiamo il dubbio di aver violato le proprietà. MAX‐HEAPIFY (A, i) l ← left (i ) r ← right (i ) IF l ≤ heapsize(A) AND A[l ] > A[i ] THEN MAX ← l ELSE MAX ← i IF r ≤ heapsize( A) AND A[r ] > A[ MAX ] THEN MAX ← r IF MAX ≠ i THEN SCAMBIA A[i ] ↔ A[ MAX ] MAX‐HEAPIFY (A, MAX) Dove: A è l’array, i è il nodo da esaminare, e heapsize è la grandezza dell’albero. Dato un nodo i, se è più piccolo del suo figlio sinistro, lo mette in una variabile MAX, altrimenti, nella variabile MAX metterà lʹindice del suo figlio sinistro (LEFT (i)). Successivamente confronta la variabile MAX con il figlio destro. Se MAX è minore di RIGHT (x) il nuovo MAX sarà il figlio destro. Infine se il massimo è diverso dal nodo scelto, allora scambiamo le chiavi di A[i] e A[MAX] e richiamiamo MAX‐HEAPIFY sul nuovo nodo. Tempo di esecuzione: T(n) = O(lgn) Build‐Heap: Questo algoritmo ci permette di creare un heap partendo da un array non ordinato. BUILD‐MAX‐HEAP (A) FOR i ← ⎣heapsize[A] / 2⎦ DOWNTO 1 DO MAX‐HEAPIFY (A, i) Vi sono n chiamate di Heapify, dunque il tempo di esecuzione è: T(n) = O(nlgn) Heapsort: Questo algoritmo invece partendo da un array iniziale, costruisce un heap ed inoltre da in output un array ordinato. HEAP‐SORT (A) BUILD‐MAX‐HEAP (A) FOR i ← lenght[A] DOWNTO 2 DO SCAMBIA A[1] ↔ A[i] heapsize( A) ← heapsize( A) − 1 MAX‐HEAPIFY (A, 1) Ad ogni passo prende la radice dell’heap e la scambia con l’ultimo elemento. Quindi viene estratto il massimo (min), ridimensionata la dimensione di heapsize e chiamata ricorsivamente Heapify sull’elemento di testa. Dopo n‐1 passi avremo in uscita un array ordinato. Tempo di esecuzione: T(n) = O (nlgn) Code di priorità Serie di operazioni con estrazioni o inserimenti nell’heap. INSERT (A, x) Inserisce un elemento MAXIMUM (A) Ritorna il massimo EXTRACT‐MAX (A) Estrae il massimo INCREASE‐KEY (A, i, k) Incrementa la chiave Maximum: HEAP‐MAXIMUM (A) RETURN A[1] Infatti il Massimo è uguale alla radice dell’heap. Tempo di esecuzione: T(n) = O(1) Estract: HEAP‐ESTRACT‐MAX (A) IF heapsize[ A] ≥ 1 ← A[1] A[1] ← A[heapsize[ A]] heapsize( A) ← heapsize( A) − 1 THEN MAX MAX‐HEAPIFY (A, 1) RETURN MAX ELSE RETURN NIL Mettiamo in una variabile MAX il valore della radice e continuiamo scambiando il primo elemento con l’ultimo e richiamando la procedura MAX‐HEAPIFY sul nodo di testa. Alla fine decrementiamo heapsize e restituiamo MAX. Tempo di esecuzione: T(n) = O(lgn) Increase: HEAP‐INCREASE‐KEY (A, i, k) IF k < A[i ] THEN RETURN error A[i ] ← k WHILE ( i > 1 AND A[ PARENT (i )] < A[i] ) DO SCAMBIA A[ PARENT (i )] ↔ A[i ] i ← PARENT (i ) Scegliamo l’elemento A[i] e ne cambiamo la chiave all’interno. Se il nodo i che scegliamo è la radice, la procedura termina, altrimenti se la nuova chiave è maggiore (minore) di quelle del padre, scambiamo i 2 valori. La procedura termina quando arriviamo al nodo radice. Tempo di esecuzione: T(n) = O(lgn) Insert: MAX‐HEAP‐INSERT (A, x) heapsize( A) ← heapsize( A) + 1 A[heapsize[ A]] ← x i ← heapsize[ A] WHILE ( PARENT (i ) ≥ 1 AND A[ PARENT (i )] < A[i ] ) DO SCAMBIA A[ PARENT (i )] ↔ A[i ] i ← PARENT (i ) Incrementiamo la dimensione di heapsize e inseriamo la nuova chiave nel nodo vuoto. Successivamente se la chiave è maggiore (minore) della chiave del padre, scambia i valori, altrimenti la procedura termina. Usciremo dal ciclo while anche quando arriveremo alla radice. Tempo di esecuzione: T(n) = O(lg n) Quicksort Questo è un algoritmo di ordinamento per confronto che divide l’array in input A[p…r] in due sotto array A[p…q] e A[q+1…r] e li ordina separatamente in maniera ricorsiva. Prima di comprenderlo, analizziamo PARTITION. Tale funzione prende un elemento chiamato pivot e fa si che tutti gli elementi minori stiano alla sua sinistra, mentre quelli maggiori stiano alla sua destra. Partition: PARTITION (A, p, r) x ← A[ p] i ← p −1 j ← r +1 WHILE TRUE DO REPEAT j ← j − 1 UNTIL A[ j ] ≤ x REPEAT i ← i + 1 UNTIL A[i ] ≥ x IF i < j THEN SCAMBIA A[i ] ↔ A[ j ] ELSE RETURN j L’elemento A[p] rappresenta il pivot. Prendiamo due indici i e j. L’indice i scorre l’array da sinistra verso destra fin quando non trova un numero maggiore o uguale al pivot. L’indice j scorre l’array da destra verso sinistra fin quando non trova in numero minore o uguale al pivot. A questo punto se l’indice i si trova ancora a sinistra di j allora scambia A[i] con A[j], altrimenti restituisce j. Quicksort: QUICK‐SORT (A, p, r) IF p < r THEN q ← PARTITION( A, p, r ) QUICKSORT (A, p, q) QUICKSORT (A, q+1, r) T(n) = Θ (n2) Tempo di esecuzione: T(n) = Θ (nlgn) Ordinamento in tempo lineare Algoritmi che risolvono i problemi nel tempo massimo di O(n). Questi algoritmi non si basano sui confronti (essi hanno un limite inferiore di Ω (nlgn)) Counting sort: Questo algoritmo serve ad ordinare una lista di numeri interi di cui si conosce il valore massimo. Sia A il nostro array in input e k il valore massimo contenuto in esso: • A occuperà spazio pari a lenght [A] • k invece occuperà dello spazio pari a tutti i numeri interi fino da 0 a k COUNTING‐SORT (A, B, K) n ← lenght[ A] FOR i ← 0 TO k DO C[i] ← 0 FOR i ← 0 TO n‐1 DO C[ A[i]] ← C[ A[i]] + 1 ← 1 TO k DO C[i] ← C[i] + C[i − 1] FOR i ← n − 1 DOWNTO 0 DO B[C[ A[i ]] − 1] ← A[i] C[ A[i]] ← C[ A[i ]] − 1 Il primo ciclo for inizializza a 0 tutti i numeri compresi tra 0 e k. Nel secondo ciclo for, analizza tutti i numeri contenuti in A e incrementa i corrispettivi indici in C. Il terzo ciclo, somma i valori dell’ array C in modo da sapere in quale posizione il numero deve essere scritto nell’array di output. Infine il quarto ciclo, scandisce l’array A da destra verso sinistra, in modo da mantenere la stabilità. Per ogni elemento di A, inserisce in B il valore contenuto nella cella di indice A[i]. Alla fine decrementiamo ogni indice di i inserito in B. Il tempo di esecuzione è O(k+n) che nel caso in cui k=n diviene O(n). FOR i Radix sort: Il Radix‐sort era un algoritmo implementato su grandi calcolatori che serviva per ordinare delle schede perforate. Funzionava nel seguente modo: prendeva una serie di numeri in ingresso, tutti di uguale dimensione, cioè con lo stesso numero di cifre. Una volta messi uno sopra l’atro li possiamo ordinare in base a una sua cifra. Il ragionamento umano farebbe si che gli input verrebbero ordinati partendo dalla cifra più significativa, ma questo si rileva di una complessità molto dispendiosa. Quindi ragionando in maniera contro‐intuitiva ordiniamo partendo dalla cifra meno significativa e andando avanti fino alla più significativa. RADIX‐SORT (A, d) FOR i ← d DOWNTO 1 DO Usa un algoritmo stabile per ordinare l’array A sulla cifra i Radix sort utilizzando un algoritmo stabile come counting‐sort ha un tempo di O(d(n+k)). Se consideriamo d una costante, e k=n allora si riesce a fare un ordinamento in tempo O(n) Mediane e statistiche d’ordine Problema di diversa specie rispetto a quello dell’ordinamento è la selezione di un i‐esimo elemento di un array. Minimo e Massimo MIN&MAX (A, n) MIN ← A[1] MAX ← A[1] FOR i ← 2 TO n DO IF A[i ] < MIN THEN MIN ← A[i ] IF A[i ] > MAX THEN MAX ← A[i ] RETURN MIN e MAX L’algoritmo MIN&MAX riesce a trovare gli elementi minimo e massimo in tempo lineare. Selezione in tempo lineare Riusciamo a trovare un qualunque elemento dell’array in tempo lineare… RANDOMIZED‐PARTITION (A, p, r) i ← RANDOM ( p, r ) SCAMBIA A[i ] ↔ A[1] RETURN PARTITION (A, p, r) RANDOMIZED‐SELECT (A, p, r, i) IF p = r THEN RETURN A[ p] q ← RANDOMIZED − PARTITION ( A, p, r ) IF i < q + 1 THEN RANDOMIZED‐SELECT (A, p, q, i) ELSE RANDOMIZED‐SELECT (A, q+1, r, i) Questo è molto utile poichè possiamo utilizzarlo per una procedura ottimizzata del quicksort evitando di incappare nel caso peggiore. RANDOMIZED‐QUICKSORT (A, p, r) IF p < r THEN q ← RANDOMIZED − PARTITION ( A, p, r ) RANDOMIZED‐QUICKSORT (A, p, q) RANDOMIZED‐QUICKSORT (A, q+1, r) Select La nuova sfida è quella di trovare un algoritmo che riesce a trovare l’i‐esimo elemento di un array in tempo lineare anche nel caso peggiore. Il select riesce in questo utilizzando i seguenti passaggi: 1. Si dividono n elementi in gruppi di 5. 2. Applico INSERTION‐SORT su ogni gruppo. 3. Si trova il mediano dei mediani chiamando ricorsivamente l’algoritmo SELECT. 4. Partiziono l’array utilizzando come pivot l’elemento trovato. 5. Chiamo ricorsivamente SELECT sul sotto‐array contenente l’i‐esimo elemento che sto cercando. Con semplici passaggi riusciamo a dire che l’algoritmo opera in tempo lineare per input abbastanza grandi (nella dimostrazione si parla dell’ordine dei 140 e con una costante c≥20a)