Nel vasto e complesso mondo dell'informatica, la gestione efficiente dei dati è un pilastro fondamentale per lo sviluppo di software performanti e affidabili. L’uso delle strutture dati è cruciale in questo ambito, soprattutto quando si tratta di memorizzare, gestire e organizzare i dati in modo rapido ed efficiente. Ogni sviluppatore deve considerare la possibilità di comprendere a fondo la struttura dei dati, perché può migliorare in modo significativo le proprie competenze. Tra le diverse strutture dati avanzate, l'heap si distingue per la sua specializzazione e l'ampia gamma di applicazioni, in particolare per l'implementazione e l'ordinamento delle code di priorità.
Che Cos'è un Heap? Introduzione alla Struttura Dati Fondamentale
In informatica, un heap (lett. "mucchio") è una struttura dati basata sugli alberi che soddisfa la "proprietà di heap": se A è un genitore di B, allora la chiave (il valore) di A è ordinata rispetto alla chiave di B conformemente alla relazione d'ordine applicata all'intero heap. Questa relazione d'ordine è ciò che definisce la tipologia specifica dell'heap. Di conseguenza, gli heap possono essere suddivisi in "max heap" e "min heap", ognuno con caratteristiche e scopi ben definiti. L’heap è una struttura dati ad albero specializzata e comprende il nodo più in alto chiamato radice (padre). I nodi successivi vengono riempiti da sinistra a destra, garantendo una forma specifica all'albero sottostante.

La scelta di utilizzare un tipo di heap anziché l'altro è data dal tipo di impiego che se ne vuole fare. Gli heap sono essenziali negli algoritmi della teoria dei grafi, come l'algoritmo di Dijkstra, o negli algoritmi di ordinamento, come l'heapsort. L'heap è, inoltre, una delle implementazioni più efficienti di un tipo di dato astratto chiamato coda di priorità. Le code di priorità, come vedremo, sono strutture dati contenenti oggetti con priorità, dove ogni oggetto o articolo ha una priorità prestabilita per esso. I programmatori hanno utilizzato gli heap nei sistemi operativi di programmazione dei lavori per garantire che i datori di lavoro possano chiamare le persone in base alla priorità, dimostrando la loro versatilità in contesti reali.
Il Max Heap: Definizione e Proprietà Chiave
Il max heap è una delle due principali tipologie di heap e si distingue per una proprietà fondamentale che ne regola la struttura e il comportamento. In un max heap, le chiavi di ciascun nodo sono sempre maggiori o uguali di quelle dei figli, e la chiave dal valore massimo appartiene alla radice (detta anche nodo root). Questo significa che il nodo genitore DEVE avere un valore maggiore o uguale a quello dei nodi figlio sinistro e destro. Se questo non viene rispettato, non si ha un max heap. Ogni elemento presente in un max heap tende ad agire secondo questa proprietà, ovvero la chiave del nodo genitore è sempre maggiore o uguale a quella del nodo figlio.
Questo principio garantisce che compiendo un qualsiasi percorso che parte da un nodo dell'albero e scendendo nella struttura verso le foglie, si attraversano nodi con chiave sempre maggiore o uguale rispetto all'ultima foglia visitata su quel percorso. In pratica, la radice dell’albero è l’elemento più grande, mentre le foglie tendono ad essere le più piccole, o comunque non più grandi dei loro rispettivi genitori e antenati. Inoltre, non esiste un ordine specifico per i nodi foglia, quindi l’unica garanzia è che il nodo radice sia il maggiore e che i suoi nodi figli abbiano valori minori o uguali al suo.

Per contro, il min heap è l'opposto del max heap, con la radice come valore più piccolo e nodi successivi che aumentano di valore, in cui ogni nodo figlio ha un valore maggiore o uguale al suo genitore. Nella struttura Min Heap, il nodo radice ha un valore uguale o inferiore ai figli su quel nodo, e questo nodo heap contiene il valore minimo. Nella struttura di Max Heap, il nodo genitore o radice ha un valore uguale o maggiore dei suoi figli nel nodo, e questo nodo contiene il valore massimo. La scelta tra max heap e min heap dipende esclusivamente dall'esigenza di trovare rapidamente l'elemento massimo o minimo in un insieme di dati. Gli heap tendono a essere incredibilmente efficienti quando si tratta di trovare l’elemento massimo o minimo presente negli array, e sono utili anche negli algoritmi di selezione e nelle statistiche.
Panoramica sull'inserimento dell'heap min/max binario
Implementazione del Max Heap Tramite Array: L'Albero Binario e la Sua Rappresentazione
Una delle implementazioni più comuni e efficienti di un heap è l'heap binario, basato su un albero binario completo. Gli heap utilizzano alberi binari per evitare i buchi presenti nell’array. Un albero binario è costituito da nodi, ognuno dei quali può avere un massimo di 2 nodi figli. In dettaglio, un albero binario è costituito da un nodo genitore che può avere da 0 a 2 nodi, potendo avere un nodo figlio sinistro e/o un nodo figlio destro o nessun nodo. Un max heap (o maxheap) è, per definizione, un albero binario completo.

In un albero binario completo, tutti i nodi sono pieni tranne l'ultimo livello che può essere pieno, ma non è necessario che sia completamente pieno. L'ultimo livello, l'ennesimo livello (dove il primo è a n = 0 ed è la radice), può avere da 1 a 2^n nodi. È importante notare che l’ultima riga dell’heap è sempre riempita da sinistra a destra. Quindi la foglia più a sinistra è assegnata prima e quella più a destra per ultima.
Gli heap sono generalmente implementati tramite array (di dimensione fissa o variabile) e non richiedono puntatori fra gli elementi, il che li rende molto efficienti dal punto di vista dello spazio. Gli heap binari possono essere rappresentati in un modo molto efficiente utilizzando un solo array. La convenzione è la seguente:
- Il primo elemento dell'array rappresenta la radice.
- Per un heap, se la radice (nodo padre superiore dell'albero) è memorizzata nella posizione (indice)
n, viene definita per array,theArray, cometheArray[n]. - I nodi figlio sinistro e destro sono, quindi, rispettivamente in
theArray[2n+1]etheArray[2n+2]. - Per l'heap massimo, la radice è in
theArray[0].
Generalizzando per qualsiasi nodo all'indice n (considerando la radice a indice 0):
theArr[n]è il nodo padre.theArr[(2*n)+1]è il nodo figlio sinistro.theArr[(2*n)+2]è il nodo figlio destro.
Questa mappatura permette di navigare nell'albero utilizzando semplici calcoli aritmetici sugli indici dell'array, evitando la complessità e l'overhead dei puntatori tipici delle strutture ad albero collegate. Un esempio di array che rappresenta un max heap è:
[99, 51, 19, 13, 10, 5, 6, 3, 9]

Operazioni Chiave sul Max Heap
Per mantenere la proprietà di max heap durante le modifiche, sono necessarie diverse operazioni fondamentali. Queste operazioni garantiscono che la struttura rimanga valida anche dopo inserimenti o rimozioni.
Max-Heapify
L'algoritmo Max-Heapify viene utilizzato per garantire che un albero binario sia un max heap. Se ci troviamo su un nodo n, e i suoi nodi figlio, sinistro e destro, sono anch'essi max heap, allora la condizione è soddisfatta per quel sottoalbero. Se questo non è il caso in tutto l'albero, allora non abbiamo un max heap. Max-Heapify funziona su un solo nodo, assicurandosi che il sottoalbero radicato in quel nodo rispetti la proprietà del max heap, assumendo che i suoi sottoalberi figli siano già max heap. L’algoritmo prende in parametro l’array e l’indice del nodo fuori posizione ed opera ricorsivamente finché non si verificano le condizioni richieste.
In particolare, Max-Heapify opera secondo i seguenti passaggi:
- Assegna in
ll’indice del figlio sinistro del nodo da riposizionare e inrl’indice del figlio destro. - Nel primo controllo si verifica che l’indice
lrientri effettivamente nella dimensione dell’heap e si controlla inoltre se il valore inA[l]è maggiore diA[i], ovvero il nodo preso in esame. In caso affermativo, si memorizza nella variabilelargestl'indice del figlio sinistro, altrimenti si assegna a quest’ultima l’indice del nodoi. - Nel secondo controllo si verifica che l’indice
rrientri effettivamente nella dimensione dell’heap e si controlla inoltre se il valore inA[r]è maggiore diA[largest]. Se vero,largestviene aggiornato con l'indice del figlio destro. - Se
largestnon è uguale all'indice del nodo genitorei, significa che il genitore non è il più grande tra sé stesso e i suoi figli. Viene eseguito uno scambio tra il nodo genitore e il nodolargest. Successivamente, Max-Heapify viene chiamato ricorsivamente sulargest(la nuova posizione del nodo che era inizialmente il genitore) per assicurarsi che il sottoalbero sottostante mantenga la proprietà di max heap.
Se il requisito è che l'array sia un array max heap completo, tutti i sottoalberi devono essere convertiti in maxheap prima della radice, uno alla volta. L'algoritmo deve essere utilizzato su ogni nodo, partendo dagli ultimi nodi non foglia e risalendo fino alla radice. Questo verrà fatto su N/2 nodi, poiché le foglie rispetteranno intrinsecamente i requisiti di max heap.
Build Max-Heap
L'algoritmo Build Max-Heap è abbastanza semplice: è costituito da un ciclo che chiama al suo interno Max-Heapify. Il suo scopo è trasformare un array arbitrario in un max-heap. L'array viene "diviso" concettualmente in due parti, supponendo che la parte sinistra abbia gli elementi da ordinare e la parte destra sia formata dalle foglie dell'albero. L'algoritmo chiama Max-Heapify per gli elementi della parte destra dell’array, a partire dalla fine, risalendo verso l'inizio. In questo modo, riposiziona gli elementi dell’array partendo dalle foglie e risalendo, assicurando che ogni sottoalbero diventi un max-heap. Build Max-Heap individua l’indice di mezzo distinguendo le due metà di cui abbiamo parlato, poi chiama Max-Heapify con il primo elemento che necessita di essere riposizionato. Se un valore non rispetta le regole di un heap, i valori vengono scambiati con il figlio di valore maggiore.
Sift-up (o Heapify-up)
L'operazione di sift-up sposta un nodo in alto all'interno dell'albero ed è utilizzata per ripristinare la condizione di heap dopo l'inserimento di un nuovo elemento. Se si utilizza il metodo array per aggiungere un elemento appena inserito a un array, il metodo Sift-Up aiuta il nodo appena aggiunto a riposizionarsi nella sua nuova posizione. Questo algoritmo applica la formula Parent_Index = Child_Index / 2 per risalire l'albero, scambiando il nodo con il suo genitore se il valore del nodo è maggiore di quello del genitore, finché la proprietà di max heap è ripristinata o il nodo raggiunge la radice.
Aggiunta di Elementi (add())
L'operazione Add() posiziona un nuovo elemento in un heap. Per incorporare nuovi elementi in un cumulo, è possibile seguire questi passaggi: l'elemento viene generalmente aggiunto all'ultima posizione disponibile nell'array (o come ultima foglia nell'albero binario completo). Successivamente, viene invocato un algoritmo di "sift-up" o "heapify-up" per spostare l'elemento verso l'alto nell'albero, scambiandolo con il suo genitore, finché la proprietà di max heap non è soddisfatta per il sottoalbero interessato.
Rimozione di Elementi (remove(), poll())
Il metodo Remove() o Poll() consente di rimuovere il primo elemento dall'elenco dell'array, che in un max heap è sempre la radice (il valore massimo). Per rimuovere la radice, essa viene scambiata con l'ultima foglia dell'heap. Successivamente, l'ultima foglia (ora contenente il valore originario della radice) viene rimossa dall'array. Il nuovo elemento alla radice (il valore che era l'ultima foglia) viene poi fatto "sift-down" (o "heapify-down") nell'albero per ripristinare la proprietà di max heap. Questo processo prevede di scambiare il nodo con il suo figlio maggiore finché non è più grande di entrambi i suoi figli o raggiunge una foglia. Il metodo poll() viene utilizzato per rimuovere l'elemento head (valore massimo) utilizzando un ciclo, rimuovendo ogni volta l'elemento head nella nuova coda e diminuendo la dimensione della coda di uno ogni volta.
Altre Operazioni
Per trovare i valori più alti e più bassi in un insieme di dati, sono necessarie molte operazioni heap di base come trovare, eliminare e inserire.
peek(): Questa operazione restituisce l'elemento massimo (la radice) senza rimuoverlo dall'heap.contains(J): Utilizzato per scoprire se un elemento specifico,J, è presente in una coda.size(): Restituisce il numero di elementi nell'heap. È importante controllare se hai elementi da elaborare usandoIs-Empty(). La dimensione dell'heap aiuterà a individuare il max-heap o il min-heap.Unisc: Se si hanno due heap da combinare in uno solo, si usaunisci heapper riunire i valori dei due heap.Taglia: Restituisce la grandezza o la lunghezza dell'heap.

Complessità Temporale delle Operazioni
La comprensione della complessità temporale è fondamentale per valutare l'efficienza degli algoritmi che utilizzano gli heap. Nelle seguenti complessità temporali, O(f) è l'upper-bound asintotico, mentre Θ(f) è l'esatto ordine di grandezza.
- Max-Heapify: La complessità temporale per un nodo all'altezza
XèO(X). Poiché l'altezza massima di un heap binario conNnodi èlog(N), Max-Heapify ha una complessità diO(log N). - Build Max-Heap: L'algoritmo Build Max-Heap costruisce un max heap da un array di
Nelementi inO(N)tempo. Sebbene chiami Max-HeapifyN/2volte, l'altezza dei sottoalberi su cui opera Max-Heapify diminuisce man mano che si procede verso l'alto nell'albero, portando a una complessità totale più efficiente. - Inserimento (
add()): L'inserimento di un elemento e il conseguentesift-upha una complessità diO(log N), poiché nel caso peggiore l'elemento potrebbe dover essere spostato dalla foglia alla radice. - Rimozione del Massimo (
poll()): L'estrazione del massimo (la radice) e il successivosift-downha una complessità diO(log N). - Ricerca del Massimo (
peek()): Trovare il massimo richiedeO(1)tempo, poiché è sempre la radice. - Heapsort: È un algoritmo di ordinamento basato sul confronto in cui l'ordinamento avviene in ordine crescente trasformandolo prima in un heap massimo. La complessità temporale dell'heapsort è
O(N log N), doveNè la dimensione dell'array. A differenza della maggior parte degli algoritmi di ordinamento, heapsort utilizza lo spazioO(1)per le sue operazioni di ordinamento, rendendolo efficiente anche in termini di memoria. - Algoritmo Largest-k: Consideriamo un problema in cui è richiesto di trovare i
kelementi più grandi da un arrayAgià strutturato come max-heap di dimensionen. Estrarre il massimo da un Max-Heap richiede tempolog(n). Facendolokvolte, si ottengono tutti gli elementi desiderati in ordine inverso. Pertanto, la complessità dell'algoritmo Largest-k dovrebbe essereO(k log k). Un'analisi più dettagliata suggerisce che seAè un max-heap, e se2^e <= k < 2^(e+1)per qualche interoe, ikelementi più grandi diAsi trovano tra i primi2^(e+1)-1indici. Inserendo questi primi2^(e+1)-1elementi in un heap temporaneoTmp(che ha dimensioneO(k)), il primo loop viene eseguito in un tempoO(k). Successivamente, ordinandoTmpcon un Heapsort, si ha una complessità diO((2^e - 1) log (2^e - 1)) = O(2k log 2k) = O(k log k). Eseguendo poi un loopkvolte di operazioniO(1), il tempo impiegato èO(k). In totale, la complessità rimaneO(k log k).
Il Max Heap e le Code di Priorità
L’heap è una struttura di dati ad albero avanzata che i programmatori utilizzano principalmente per implementare e ordinare le code. La Coda prioritaria è una struttura dati astratta contenente oggetti con priorità, dove ogni oggetto o articolo ha una priorità prestabilita per esso. I programmatori progettano code di priorità basate sulle strutture dell’heap, poiché l'heap è l'implementazione più efficiente per questo tipo di struttura dati. Nel contesto di una coda di priorità implementata con un max heap, l'elemento con la priorità più alta (il valore maggiore) sarà sempre la radice, rendendo l'accesso e l'estrazione di tale elemento estremamente rapidi.

Implementazione del Max Heap in Linguaggi di Programmazione
Diversi linguaggi di programmazione offrono supporto integrato o librerie per l'implementazione degli heap.
Java e la Classe PriorityQueue
Il linguaggio Java (dalla versione 1.5) fornisce gli heap binari con la classe java.util.PriorityQueue<E> della Java Collections Framework. Gli heap in Java possono essere implementati utilizzando questa classe PriorityQueue, che si trova nel pacchetto java.util.. Le PriorityQueues vengono utilizzate per trovare l'elemento più o meno importante in una raccolta.
Gli elementi delle PriorityQueue devono essere oggetti confrontabili in modo che vengano inseriti in un ordine particolare nella coda. PriorityQueue può avere un comparatore in modo che venga effettuato un confronto tra gli oggetti e la coda formata in base a questo confronto. L'impostazione predefinita della classe PriorityQueue è l'heap minimo senza un comparatore. Min heap è l'opposto di max heap e quindi la radice è il valore più piccolo e i successivi nodi figlio sono maggiori o uguali alla radice e ai successivi nodi parentali. Per questo motivo, per creare un max heap, è necessario utilizzare reverseOrder() dal framework Collections di Java come comparatore. Ciò assicurerà di ottenere un heap massimo e non un heap minimo. Gli elementi vengono aggiunti alla coda e possono essere in qualsiasi ordine. La nuova coda PriorityQueue memorizzerà questi elementi come max heap in ordine inverso.
Quando la coda viene scritta (ovvero gli elementi vengono estratti uno per uno), l'ordine sarà decrescente: Root, Left-child con root come parent (Left-child1), Right-child con root come parent (Right-child1), Left-child con Left-child1 come parent (Left-child2), Figlio destro con figlio sinistro1 come genitore (Figlio destro2), Figlio sinistro con figlio destro1 come genitore (Figlio sinistro3), Figlio destro con figlio destro1 come genitore (Figlio destro3), Figlio sinistro con figlio sinistro2 come genitore (Left-child4), Right-child con Left-child2 come genitore (Right-child4), ecc.
Il codice seguente è un esempio di come viene creato un max heap (maxheap) in Java utilizzando PriorityQueue. La prima cosa da fare è riempire un array con i valori per i quali verrà creato l'heap massimo. Questo è chiamato theArray. Successivamente, viene creato un PriorityQueue, theQueue, e quindi gli elementi di theArray vengono aggiunti a questo. Questo utilizza il metodo add(), ad esempio theQueue.add(10) per aggiungere 10 alla coda.
Per illustrare alcune delle funzionalità della classe PriorityQueue, viene quindi utilizzato il metodo peek() per trovare l'intestazione dell'heap, che in un max heap è il valore massimo, in questo caso, 99. L'attività successiva consiste nel controllare la dimensione dell'heap usando size() che risulta essere 9 e questo viene stampato sulla console. Il metodo writeMaxHeap scrive gli elementi nella coda in ordine di root, figlio sinistro con root come genitore, figlio destro con root come genitore, ecc., con valori successivi usando i figli sinistro e destro come genitori nello stesso ordine di cui sopra. Il metodo PriorityQueue contains(J) viene utilizzato per scoprire se un elemento specifico, J, è in una coda. Nel nostro esempio, cerchiamo J = 10, e la console stampa vero. Un altro metodo PriorityQueue, remove(J), viene quindi utilizzato per rimuovere J = 10 da theQueue. Per illustrare meglio la funzionalità di PriorityQueue, il metodo poll() viene utilizzato per rimuovere l'elemento head (valore massimo) utilizzando un ciclo while, rimuovendo ogni volta l'elemento head nella nuova coda e diminuendo la dimensione della coda di uno. Questo accade nel metodo writeQueue chiamato dal main. Ogni volta che l'elemento rimosso viene stampato sulla console. Alla fine la coda originale non avrà più elementi. Gli elementi stampati sono l'heap massimo in ordine decrescente di valore, dove viene stampata ogni volta l'inizio della coda.
Esempio di Output:
Size of theQueue? 99Dimensioni della coda? 9theQueue scritta usando il ciclo for 99 51 19 13 10 5 6 3 9La coda contiene 10? verotheQueue scritto usando poll() 99 51 19 13 9 6 5 3Dimensioni della coda? 0Il codice che mostra come maxheapify un albero (un array) viene implementato creando un Array e riempiendolo di numeri. Un secondo array, newArray, conterrà il risultato del metodo maxHeapCreate, l'array max heap. Il metodo maxHeapCreate viene chiamato da main e qui un nuovo array, theNewArr, viene creato e riempito con i risultati di maxHeapify. Questo viene fatto eseguendo un ciclo su oltre la metà della dimensione dell'array di input. Per ogni passaggio del ciclo, viene chiamato il metodo maxHeapify, iniziando dall'elemento al centro dell'array e terminando con il primo. Per ogni chiamata di maxHeapify, il figlio sinistro e il figlio destro del nodo genitore i vengono trovati e vengono eseguiti controlli per trovare quale sia il più grande dei tre, definendolo come maxVal. Se maxVal non è uguale al nodo padre, viene eseguito uno scambio in modo che il nodo padre e maxVal vengano scambiati e quindi maxHeapify viene chiamato di nuovo questa volta su maxVal e vengono eseguiti gli stessi passaggi di prima. Alla fine verrà creato il max heap e non ci saranno più iterazioni da eseguire. L'array aggiornato, array, viene ora restituito a main come newArray e quindi ogni elemento consecutivo viene stampato nella console. newArray ora è un max heap. Si nota che, come nell'esempio precedente usando PriorityQueue, i numeri sono scritti: root, figlio destro di root come genitore, figlio sinistro di root come genitore, figlio destro del primo figlio destro come genitore, figlio sinistro del primo figlio sinistro come genitore, figlio destro del primo figlio sinistro come genitore, figlio sinistro del primo figlio destro come genitore, ecc. Sono in un ordine leggermente diverso rispetto a quando si utilizza PriorityQueue perché il confronto viene eseguito tra elementi consecutivi, mentre nell'esempio maxheapify, il nodo viene confrontato con i successivi due elementi successivi nell'array e scambiato con il valore più grande. In breve, vengono utilizzati due diversi algoritmi.
C++ e Librerie di Heap
Fra le librerie C++ Boost c'è una libreria di heap che offre implementazioni efficienti per queste strutture dati, consentendo agli sviluppatori C++ di sfruttarne i vantaggi. L'uso di coppie (valore, indice) può essere utile quando si codifica in un linguaggio per recuperare l'indice giusto nell'array originale A e aggiungere i suoi figli tranquillamente con A[2i] e A[2i+1].
Verifica della Proprietà Max-Heap e Algoritmi Correlati
Un compito comune nell'analisi degli heap è verificare se una data struttura rispetta la proprietà di max-heap. Dato un albero binario rappresentato da un array di n numeri (con l’indicizzazione descritta sopra), occorre verificare se l’albero soddisfa le proprietà di un max-heap.
InputLa prima riga dell’input contiene un singolo intero n (1 ≤ n ≤ 100 000).La riga successiva contiene n numeri separati da spazio che rappresentano i valori degli elementi nell’heap.
OutputIl programma deve stampare Yes se l’albero binario fornito rispetta la proprietà di max-heap, altrimenti deve stampare No.
EsempiInput:
88 5 7 1 1 6 3 1Output:
YesSpiegazione: Tutti i nodi genitori hanno un valore maggiore o uguale ai loro nodi figli. Pertanto, la proprietà di heap è soddisfatta.
Input:
78 5 7 1 9 6 3Output:
NoSpiegazione: Il nodo con un valore di 9 è maggiore del suo nodo genitore, violando la proprietà del max-heap.
Quindi qui abbiamo esaminato l'heap massimo e come può essere creato con un algoritmo PriorityQueue o Max Heapify. L'uso di PriorityQueue con reverseOrder() è un modo accurato per farlo e il metodo consigliato per semplicità di implementazione.
L'heap è una struttura dati ad albero specializzata che rispetta la cosiddetta proprietà di heap. Ricordate che gli heap non sono sempre ordinati e seguono una condizione chiave in cui l’elemento più piccolo o più grande è presente nel nodo radice, a seconda che si tratti di un heap Min o Max. La gestione della memoria può essere piuttosto impegnativa con la memoria heap. Sebbene capire come funziona l’implementazione degli heap min e max sia un’ottima cosa, è necessario imparare anche il motivo per cui gli heap sono così importanti. I programmatori hanno utilizzato gli heap nei sistemi operativi di programmazione dei lavori per garantire che i datori di lavoro possano chiamare le persone in base alla priorità. Gli heap si trovano anche in vari algoritmi di ordinamento degli heap per l’implementazione di code di priorità. Questo vi aiuterà anche a farvi un’idea chiara dell’implementazione di min e max heap, assicurandovi di poterla utilizzare senza problemi.