Procediamo ora allo studio del tempo di esecuzione dell’algoritmo Heapify.
Un primo risultato dell’analisi si può ottenere osservando che ogni chiamata dell’algoritmo può effettuare al massimo una chiamata ricorsiva, o sul sottoalbero destro o sul sinistro.
Questo significa che, nel caso peggiore, una esecuzione di Heapify può al massimo generare tante chiamate ricorsive quanti sono i nodi lungo il percorso più lungo dell’albero su cui è eseguito.
La lunghezza del percorso più lungo in un albero corrisponde all’altezza dell’albero.
Ogni chiamata ricorsiva effettua localmente un numero di operazioni elementari costante.
Quindi, detta h l’altezza dell’albero su cui è chiamato Heapify, il tempo di esecuzione in funzione dell’altezza dell’albero sarà, alla peggio, proporzionale a h, cioè T(h) = O(h).
Poiché gli alberi su cui opera Heapify sono alberi completi, l’altezza di questi alberi è uguale al logaritmo del numero dei nodi che contengono, otteniamo che il tempo di esecuzione in funzione di n sarà T(n) = O(log2n).
Nelle slide successive procederemo al calcolo del tempo di esecuzione di Heapify provando ad applicare le tecniche di analisi per gli algoritmi ricorsivi introdotte nella Lezione 3.
Inizieremo con la definizione dell’equazione di ricorrenza indotta dall’algoritmo.
Osservate che, guardando semplicemente l’algoritmo, non è immediato individuare la relazione tra la dimensione dell’albero (l’input) di una chiamata e quello che essa passa alla chiamata ricorsiva che eventualmente genera.
Detto n il numero di elementi nel sottoalbero su cui Heapify è chiamato, l’equazione di ricorrenza risulterebbe quindi della forma:
T(n) = T(?) + O(1)
Al fine di poter calcolare il tempo nel caso peggiore, è necessario individuare la relazione tra n e il numero massimo di nodi su cui Heapify può essere richiamato ricorsivamente.
Si può dimostrare che, nel caso peggiore, ad ogni chiamata ricorsiva Heapify viene eseguito su un numero di nodi che è minore dei 2/3 del numero di nodi correnti n.
Assumendo, quindi, che il sottoalbero più grande sia il sinistro, il numero di nodi ns del sottoalbero su cui Heapify è chiamato ricorsivamente è al più
(analogo ragionamento si può fare se fosse il destro)
Nelle tre slide successive mostreremo che questi è il caso.
L’albero completo che massimizza il rapporto è riportato nella figura a fianco.
Detta h l’altezza dell’albero, entrambi i sottoalberi sinistro e destro sono alberi pieni, rispettivamente di altezza h-1 e h-2.
Per verificare l’asserto precedente, è sufficiente osservare quanto segue.
Per ricavare la relazione, nel caso peggiore, tra la dimensione dell’input ricevuto dalla chiamata ricorsiva di Heapify rispetto alla dimensione dell’input di partenza, è sufficiente osservare che: se l’altezza dell’albero in figura è h, allora l’altezza del sottoablero sinistro è h-1 e quella del sottoalbero destro è h-2, quindi
.
Inoltre avremo quindi che
il rapporto desiderato è
poiché quando vale
.
Possiamo quindi concludere che se n è il numero di nodi della chiamata principale ad Heapify, allora la chiamata ricorsiva verrà eseguita su di un sottoalbero che conterrà non più di nodi.
T(n) = max(O(1),max(O(1),T(?) + O(1))
………..
………..
Posto
T’(n) = T’(2n/ 3) + θ(1)
risolvendo l’equazione di ricorrenza per T’(n) con gli alberi di ricorrenza otteniamo:
da cui, essendo T’(n) un estremo superiore asintotico per T(n), otteniamo:
T(n) = O(log n)
Il passo successivio è quello della costruzione di uno Heap a partire da una sequenza arbitraria in input.
L’algoritmo Costruisci-Heap(A)
utilizza l’algoritmo Heapify
per inserire ogni elemento dell’array A
in uno Heap, risistemando sul posto gli elementi tramite opportuni scambi.
Osserviamo innanzitutto che, data la corrispondenza tra un array ed un albero completo:
L’algoritmo seguente implementa le intuizioni appena esposte:
Costruisci-Heap(A)
heapsize[A] = length[A]
FOR i = _length[A]/2_ DOWNTO 1
DO Heapify(A,i)
La variabile heapsize[A]
conterrà la dimensione dello Heap, inizialmente pari alla dimensione n=length[A]
della sequenza in input.
Possiamo facilmente calcolare un limite superiore asintotico al tempo di esecuzione di Costruisci-Heap
, osservando che:
Possiamo quindi concludere che T(n) = O(n log2n ).
Un’analisi più attenta ci permette di ottenere un limite asintotico più stretto e preciso.
Possimo, infatti, osservare che chimate diverse a Heapify
da parte di CostruisciHeap
sono effettuate su sottoalberi diversi, molti dei quali hanno altezze differenti e notevolmente inferiori a quella dell’albero completo.
In altre parole, l’analisi effettuata è basata su un’approssimazione piuttosto grossolana.
Notiamo, infatti, che la maggior parte delle chiamate a Heapify
sono effettuate su sottoalberi radicati sui padri delle foglie, che hanno altezza 1. Il numero di tali sottoalberi è circa n/4 (la metà delle foglie).
Analogamente, n/8 chiamate a Heapify sono effettuate su sottoalberi radicati sui padri dei padri delle foglie, che hanno altezza 2.
In generale avremo che Costruisci-Heap
chiama Heapify
:
Ricordando che il tempo di esecuzione di Heapify è al più lineare sull’altenzza dell’albero di cui è chiamato, possiamo quindi affermare che:
Ricordando che:
E che, quando ,
converge alla quantità costante
, possimo quindi concludere che:
Poiché almeno n/2 operazioni elementari vengono effettuate per l’esecuzione della testa del ciclo for, otteniamo che .
L’algoritmo HeapSort
è una variante di SelectSort
in cui la ricerca dell’elemento massimo è facilitata dal mantenimento della sequenza in uno Heap:
A[1]
dello Heap
è sempre il massimo.L’algoritmo procede scorrendo tutte le posizioni dell’array a partire dall’ultima e ad ogni iterazione:
A[1]
viene scambiata con l’elemento nell’ultima posizione corrente dello Heap;Heapify
sulla radice A[1]
.Di seguito riportiamo l’algoritmo
HeapSort(A)
....CostruisciHeap(A)
...FOR i = length[A] DOWNTO 2
........DO /* elemento massimo in A[1] */
........."scambia A[1] e A[i]"
........./* ripristina lo heap */
.........heapsize[A] = heapsize[A]-1
.........Heapify(A,1)
È facile individuare la somiglianza con l’algoritmo SelectionSort
riportato qui di seguito:
SelectSort(A)
....FOR i = length[A] DOWNTO 2
......DO max = Findmax(A,i)
........."scambia A[max] e A[i]"
Come si può intuire, la chiamata di HeapSort
a CostruisciHeap
corrisponde alla prima chiamata di SelectSort
a Findmax
, mentre ciascuna chiamata a Heapify
corrisponde ad una qualche chiamata a Findmax
, esculsa la prima.
Per quel che riguarda l’analisi del tempo di esecuzione, possiamo osservare che HeapSort chiama, nel caso peggiore,
Avremo quindi che:
T(n) = max(O(n), (n-1) · max(O(1), T(Heapify)))
……..= max(O(n), max(O(n), O(n · log2n)))
Concludiamo quindi che:
T(n) = O(n · log2n)