Salta al contenuto
Note per Studenti Esercizio 14 · k-esimo elemento più piccolo con uno heap

Esercizio 14k-esimo elemento più piccolo con uno heap

Esame
In questa pagina 6

Testo (esempio di tema d'esame, seconda parte, esercizio 1, 5 punti). Progettare un algoritmo che calcola il kk-esimo elemento più piccolo di un insieme di nn interi distinti, in tempo O(n+klog⁡n)O(n + k \log n) e senza ricorrere all'ordinamento.


Idea

Il termine nn suggerisce una costruzione bottom-up di uno heap, che costa Θ(n)\Theta(n); il termine klog⁡nk \log n suggerisce kk operazioni di removeMin, ciascuna Θ(log⁡n)\Theta(\log n). Il minimo estratto per kk-esimo è il kk-esimo più piccolo (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 → e 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 →). Ordinare tutto l'insieme costerebbe Θ(nlog⁡n)\Theta(n \log n), peggio quando kk è piccolo (ad esempio per la mediana si hanno k=n/2k=n/2; per il minimo k=1k=1: basta una scansione, Θ(n)\Theta(n)). È di fatto un heap sort fermato dopo kk passi (Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →).

Algoritmo

Algoritmo kEsimoMinimo(A, n, k)
Input: array A[1..n] di interi distinti, intero 1 <= k <= n
Output: il k-esimo elemento più piccolo di A
P <- A                                    (lavoro in loco sull'array)
for j <- floor(n/2) downto 1 do           (costruzione bottom-up di un min-heap)
    down-heap bubbling a partire da P[j]
last <- n
for t <- 1 to k - 1 do
    P[1] <- P[last]; last <- last - 1     (removeMin, scartando il minimo)
    down-heap bubbling a partire da P[1]
return P[1]                               (il k-esimo minimo: la radice)

L'ultima operazione non serve completarla: dopo k−1k - 1 rimozioni la radice dello heap è il kk-esimo più piccolo (equivalentemente, si può fare removeMin() kk volte e restituire l'ultimo valore estratto).

Correttezza. Lo heap contiene sempre gli elementi non ancora scartati. Dopo la costruzione la radice è il minimo (1° più piccolo). A ogni removeMin si toglie il minimo corrente, quindi dopo tt rimozioni sono stati tolti i tt elementi più piccoli e la radice è il (t+1)(t+1)-esimo. Dopo k−1k - 1 rimozioni la radice è il kk-esimo.

Esempio

A=[7,2,9,4,1,8]A = [7, 2, 9, 4, 1, 8], k=3k = 3.

  • Bottom-up (⌊6/2⌋=3\lfloor 6/2 \rfloor = 3): j=3j = 3: [7,2,8,4,1,9][7, 2, 8, 4, 1, 9]; j=2j = 2: [7,1,8,4,2,9][7, 1, 8, 4, 2, 9]; j=1j = 1: [1,2,8,4,7,9][1, 2, 8, 4, 7, 9] (il 77 scende prima scambiandosi con 11, poi con 22).
  • Rimozione 1 (si scarta 11): 99 in radice, [9,2,8,4,7]→[2,9,8,4,7]→[2,4,8,9,7][9, 2, 8, 4, 7] \to [2, 9, 8, 4, 7] \to [2, 4, 8, 9, 7].
  • Rimozione 2 (si scarta 22): 77 in radice, [7,4,8,9]→[4,7,8,9][7, 4, 8, 9] \to [4, 7, 8, 9].
  • La radice è 44: il terzo più piccolo, in accordo con l'ordinamento 1,2,4,7,8,91, 2, 4, 7, 8, 9.

Complessità

Totale: O(n+klog⁡n)O(n + k \log n). Per k=O(n/log⁡n)k = O(n / \log n) è lineare (perché allora klog⁡n=O(n)k\log n=O(n)). Per confronto con l'ordinamento (≈nlog⁡2n\approx n\log_2 n) ecco i numeri per n=1000n=1000:

Grafico interattivo: Costo per trovare il k-esimo minimo con n = 1000: heap n + k·log₂n contro ordinamento completo n·log₂n (≈ 9970); l'heap vince fino a k ≈ 900

Per k=10k=10 l'heap fa circa 1000+100=11001000+100=1100 operazioni contro ≈10 000\approx 10\,000 dell'ordinamento. Il punto di incrocio, dove n+klog⁡2n=nlog⁡2nn+k\log_2 n=n\log_2 n, è k=n log⁡2n−1log⁡2n≈0,9 nk=n\,\frac{\log_2n-1}{\log_2 n}\approx 0{,}9\,n. Se kk è vicino a nn non conviene rispetto all'ordinamento (O(nlog⁡n)O(n \log n) in entrambi i casi), ma la richiesta dell'esercizio è proprio questo limite.

Varianti

Con una coda con priorità su array non ordinato la costruzione è banale ma ogni removeMin costa Θ(n)\Theta(n): Θ(kn)\Theta(kn). Con lista ordinata si paga Θ(nlog⁡n)\Theta(n \log n) per ordinare. Lo heap bilancia i costi (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).

Errori comuni

  • Costruire lo heap con nn insert (top-down): Θ(nlog⁡n)\Theta(n \log n), che viola il limite richiesto.
  • Errore di uno: se si fanno kk estrazioni e si restituisce la radice dopo l'ultima, si ottiene il (k+1)(k+1)-esimo; vale o il valore estratto alla kk-esima estrazione, o la radice dopo k−1k - 1 estrazioni.
  • Usare un max-heap senza adattare kk (con un max-heap si cerca il kk-esimo più grande).
  • Dimenticare che gli elementi sono distinti: con ripetizioni "kk-esimo più piccolo" va ridefinito.

Versione ripasso

Testo. kk-esimo elemento più piccolo di nn interi distinti in O(n+klog⁡n)O(n + k \log n), senza ordinare.

Teoria collegata