Salta al contenuto
Note per Studenti Esercizio 13 · costruzione di uno heap top-down e bottom-up

Esercizio 13costruzione di uno heap top-down e bottom-up

Esame
In questa pagina 5

Testo (esempio di tema d'esame, scritti del 19/09/2023 e del 03/07/2025, parti 1).

  1. Si consideri l'array di entry P=[(10,∗),(8,∗),(6,∗),(7,∗),(5,∗),(2,∗)]P = [(10,*), (8,*), (6,*), (7,*), (5,*), (2,*)] e l'approccio top-down per la costruzione di uno heap a partire da PP. Mostrare l'albero associato all'array alla fine di ogni iterazione dell'approccio top-down (è sufficiente mostrare le chiavi).
  2. Si consideri l'array P=[(11,∗),(9,∗),(7,∗),(13,∗),(3,∗),(4,∗)]P = [(11,*), (9,*), (7,*), (13,*), (3,*), (4,*)] e l'approccio bottom-up. Mostrare l'albero associato all'array alla fine di ogni iterazione.
  3. Analizzare la complessità (solo O(⋅)O(\cdot)) della costruzione top-down di uno heap a partire da un array P[1..n]P[1..n], il cui pseudocodice è:
for j <- 2 to n do
    i <- j
    while (i > 1) AND (P[floor(i/2)].getKey() > P[i].getKey()) do
        swap(P[i], P[floor(i/2)])
        i <- floor(i/2)
  1. Dimostrare che la complessità di nn inserimenti in una coda con priorità implementata con uno heap e inizialmente vuota è Θ(nlog⁡n)\Theta(n \log n).

1. Top-down: P=[10,8,6,7,5,2]P = [10, 8, 6, 7, 5, 2]

Si tiene come heap il prefisso P[1..j]P[1..j] e si fa salire P[j]P[j] (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →).

Iterazione Si inserisce Array (heap P[1..j]P[1..j] + resto) Scambi
inizio — [10][10] —
j=2j = 2 88 [8,10][8, 10] 8<108 < 10: scambio con il padre
j=3j = 3 66 [6,10,8][6, 10, 8] 6<86 < 8 (padre P[1]P[1]): scambio
j=4j = 4 77 [6,7,8,10][6, 7, 8, 10] padre P[2]=10>7P[2] = 10 > 7: scambio; padre P[1]=6<7P[1] = 6 < 7: stop
j=5j = 5 55 [5,6,8,10,7][5, 6, 8, 10, 7] padre P[2]=7P[2] = 7: scambio; padre P[1]=6P[1] = 6: scambio
j=6j = 6 22 [2,6,5,10,7,8][2, 6, 5, 10, 7, 8] padre P[3]=8P[3] = 8: scambio; padre P[1]=5P[1] = 5: scambio

Albero finale: radice 22; figli 66 e 55; sotto 66: 1010 e 77; sotto 55: 88. È uno heap valido.

2. Bottom-up: P=[11,9,7,13,3,4]P = [11, 9, 7, 13, 3, 4]

n=6n = 6, ⌊n/2⌋=3\lfloor n/2 \rfloor = 3: si parte da P[3]P[3] e si va fino a P[1]P[1] con il down-heap.

Iterazione Nodo trattato Array Che cosa succede
inizio — [11,9,7,13,3,4][11, 9, 7, 13, 3, 4]
j=3j = 3 P[3]=7P[3] = 7 [11,9,4,13,3,7][11, 9, 4, 13, 3, 7] unico figlio P[6]=4<7P[6] = 4 < 7: scambio
j=2j = 2 P[2]=9P[2] = 9 [11,3,4,13,9,7][11, 3, 4, 13, 9, 7] figli 1313 e 33: minore 3<93 < 9: scambio; 99 in pos. 5, senza figli
j=1j = 1 P[1]=11P[1] = 11 [3,9,4,13,11,7][3, 9, 4, 13, 11, 7] figli 33 e 44: scambio con 33; in pos. 2 figli 13,913, 9: minore 9<119 < 11: scambio

Albero finale: radice 33; figli 99 e 44; sotto 99: 1313 e 1111; sotto 44: 77. Controllo: ogni nodo ≤\le i figli ✓.

3. Complessità del top-down

Limite superiore. L'iterazione jj esegue al più tante iterazioni del while quanti sono gli antenati di P[j]P[j] nell'albero, cioè la profondità di jj, pari a ⌊log⁡2j⌋≤log⁡2n\lfloor \log_2 j \rfloor \le \log_2 n. Il costo di una iterazione è O(log⁡j)O(\log j) (un numero costante di operazioni per scambio). Perciò

t(n)=O(∑j=2nlog⁡2j)≤O((n−1)log⁡2n)=O(nlog⁡n).t(n) = O\Big(\sum_{j=2}^{n} \log_2 j\Big) \le O\big((n - 1) \log_2 n\big) = O(n \log n).

4. nn inserimenti in uno heap vuoto: Θ(nlog⁡n)\Theta(n \log n)

L'inserimento numero jj avviene su uno heap con j−1j - 1 entry e costa al più ⌊log⁡2j⌋\lfloor \log_2 j \rfloor scambi più un numero costante di operazioni (vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →), quindi il totale è O(nlog⁡n)O(n \log n) come sopra. Limite inferiore: se le chiavi sono inserite in ordine decrescente, ogni nuova chiave è minore di tutte quelle già presenti e sale fino alla radice, con ⌊log⁡2j⌋\lfloor \log_2 j \rfloor scambi. Il costo totale è almeno

∑j=⌈n/2⌉n⌊log⁡2j⌋ ≥ n2⌊log⁡2n2⌋=n2(⌊log⁡2n⌋−1)∈Ω(nlog⁡n),\sum_{j=\lceil n/2 \rceil}^{n} \lfloor \log_2 j \rfloor \ \ge\ \frac{n}{2} \big\lfloor \log_2 \tfrac n2 \big\rfloor = \frac{n}{2}\big( \lfloor \log_2 n \rfloor - 1 \big) \in \Omega(n \log n),

perché sono almeno n2\frac n2 termini ciascuno ≥⌊log⁡2n2⌋\ge \lfloor \log_2 \frac n2 \rfloor. Essendo O(nlog⁡n)O(n \log n) e Ω(nlog⁡n)\Omega(n \log n), la complessità è Θ(nlog⁡n)\Theta(n \log n) (al caso pessimo). Il bottom-up costruisce invece lo stesso heap da un array in Θ(n)\Theta(n).

Errori comuni

  • Nel bottom-up partire da j=nj = n o da j=n/2+1j = n/2 + 1 (sono foglie) invece di ⌊n/2⌋\lfloor n/2 \rfloor; oppure procedere con jj crescente.
  • Scambiare nel down-heap con il figlio sbagliato (si sceglie il minore dei due).
  • Rispondere O(n)O(n) al punto 3: è il costo del bottom-up; il top-down è Θ(nlog⁡n)\Theta(n \log n).
  • Nel limite inferiore dimenticare di indicare l'istanza cattiva (ordine decrescente).

Versione ripasso

Testo. (1) Top-down su [10,8,6,7,5,2][10,8,6,7,5,2]; (2) bottom-up su [11,9,7,13,3,4][11,9,7,13,3,4], alberi a ogni iterazione; (3) analisi O()O() del top-down (for j <- 2 to n, up-heap); (4) nn insert in uno heap vuoto sono Θ(nlog⁡n)\Theta(n \log n).

Teoria collegata