Esercizio 20duplicati vicini con una mappa
In questa pagina 6
Testo (scritto del 10/09/2026, seconda parte, esercizio 1, 5 punti). Sia un array di interi e sia un intero positivo. Si vuole progettare un algoritmo iterativo efficiente hasNearbyDuplicate(A, k) che restituisca true se esistono due indici distinti e tali che e , e false altrimenti.
(a) Spiegare a parole l'idea alla base della soluzione proposta (suggerimento: l'utilizzo di una mappa permette di semplificare l'algoritmo e migliorare l'efficienza nel tempo), illustrandone il funzionamento mediante l'esempio , .
(b) Scrivere lo pseudocodice di hasNearbyDuplicate(A, k), specificando chiaramente input e output.
(c) Analizzare la complessità asintotica al caso pessimo e/o al caso medio in funzione di e/o , giustificando la risposta.
(d) Discutere come cambierebbe la complessità utilizzando diverse implementazioni della mappa.
Soluzione banale
Per ogni confrontare con : confronti, quindi se è vicino a . Si può fare meglio ricordando, per ogni valore, dove è stato visto per ultimo.
(a) Idea
Si scorre l'array da sinistra a destra con una mappa (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) che associa a ogni valore già incontrato l'indice della sua ultima occorrenza. All'indice :
- se non è nella mappa, lo si inserisce con valore ;
- se è nella mappa con ultimo indice : se si restituisce
true; altrimenti si aggiorna l'indice a (le occorrenze future saranno più vicine a che a , quindi non serve più).
Basta guardare l'ultima occorrenza: se esiste una coppia vicina con valore , in particolare lo sono due occorrenze consecutive di .
Esempio (indici da ), . Le prime quattro iterazioni inseriscono , , , . All'indice il valore è già nella mappa con indice e : l'algoritmo restituisce true. Con la stessa istanza restituirebbe false: all'indice , , si aggiorna e l'indice () non è nella mappa.
(b) Pseudocodice
Algoritmo hasNearbyDuplicate(A, k)
Input: array A[0..n-1] di interi, intero k >= 1
Output: true se esistono i != j con A[i] = A[j] e |i - j| <= k, false altrimenti
M <- mappa vuota (chiave: valore di A; valore: ultimo indice)
for i <- 0 to n-1 do
j <- M.get(A[i]) (null se A[i] non è in M)
if j != null AND i - j <= k then return true
M.put(A[i], i) (inserisce o aggiorna con l'indice più recente)
return falseCorrettezza. Invariante: all'inizio dell'iterazione , per ogni valore comparso in , è l'indice dell'ultima occorrenza di in , e nessuna coppia di occorrenze vicine esiste in . Se esiste una coppia vicina con e minima, l'occorrenza di immediatamente precedente è anche vicina (è a distanza ) e la contiene: viene trovata.
(c) Complessità
Il ciclo ha iterazioni, ciascuna con un get e un put sulla mappa. Con una tabella hash (vedi Tabelle hashTabella hash per implementare una mappa: funzione hash = hash code + compression function, bucket array, separate chaining; hash code per numeri e stringhe (polynomial, cyclic shift), division e MAD; load factor, complessità al caso pessimo Theta(n) e medio O(1+lambda), rehashing; esempio svolto con inserimenti e collisioni.Tabelle hash →) ogni operazione costa in media (hashing uniforme, costante), quindi:
- caso medio: ;
- caso pessimo: ogni operazione può costare con chiavi presenti (collisioni), quindi .
Lo spazio è (al più un'entry per valore distinto). Il tempo non dipende da .
(d) Altre implementazioni della mappa
| Mappa | get/put |
Tempo totale |
|---|---|---|
| tabella hash (concatenamento) | medio, pessimo | medio, pessimo |
| albero binario di ricerca bilanciato o (2,4) | garantito, | anche al caso pessimo |
| albero binario di ricerca non bilanciato | , fino a | al caso pessimo |
| lista non ordinata |
L'albero bilanciato dà una garanzia deterministica (utile se non ci si fida delle collisioni); la hash è più veloce in pratica. Un'alternativa che non usa la mappa, per piccolo, è mantenere una finestra degli ultimi elementi. (Algoritmo verificato su 3000 coppie casuali confrontandolo con il controllo di tutte le coppie.)
Errori comuni
- Controllare solo l'uguaglianza dei valori senza confrontare gli indici: restituirebbe
trueper qualunque duplicato. - Non aggiornare l'indice quando la distanza supera : poi le occorrenze successive vengono confrontate con un indice troppo vecchio. Esempio: con deve dare
true(gli ultimi due sono adiacenti); senza aggiornare, il in posizione non sostituisce quello in posizione e in posizione si confronta : risposta sbagliatafalse. - Dichiarare al caso pessimo con la hash: il caso pessimo è quadratico, lineare in media.
- Usare una struttura ordinata (albero) senza dire che le chiavi, interi, sono ordinabili.
Versione ripasso
Testo. hasNearbyDuplicate(A, k): true se esistono con e . Idea con esempio , , pseudocodice, complessità, altre mappe.
- Idea (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →): mappa valore ultimo indice; all'indice , se è in con indice e ⇒
true; altrimentiM.put(A[i], i). - Esempio: ; all'indice : ⇒
true; con ⇒false. - Pseudocodice:
M <- mappa vuota; per :j <- M.get(A[i]); senulle restituiscetrue;M.put(A[i], i); fine ⇒false. - Complessità (hash, vedi Tabelle hashTabella hash per implementare una mappa: funzione hash = hash code + compression function, bucket array, separate chaining; hash code per numeri e stringhe (polynomial, cyclic shift), division e MAD; load factor, complessità al caso pessimo Theta(n) e medio O(1+lambda), rehashing; esempio svolto con inserimenti e collisioni.Tabelle hash →): in media, al caso pessimo; spazio ; indipendente da .
- Altre mappe: albero bilanciato/(2,4) garantito; ABR non bilanciato o lista .
- Pseudocodice:
M <- mappa vuota; per :j <- M.get(A[i]); senulle ⇒true;M.put(A[i], i); fine ⇒false. - Perché l'aggiornamento: , : senza aggiornare il in pos. 3 il confronto in pos. 4 darebbe (sbagliato); aggiornando ⇒
true. - Tabella delle mappe: hash medio / pessimo; albero bilanciato o (2,4) ; ABR non bilanciato o lista .
- Errori: indici non confrontati; indice non aggiornato; al caso pessimo con hash.