CodeGym /Corsi /Swift SELF /Approfondiamo il lavoro con Set

Approfondiamo il lavoro con Set

Swift SELF
Livello 22, Lezione 1
Disponibile

1. Quando serve Set: controlli rapidi «c'e` l'elemento»

Quando inizi a programmare, sembra che l'array sia la risposta universale a tutte le domande. Servono dati? Un array. Devi controllare se c'e` un elemento? Beh... scorriamo l'array e confrontiamo.

Con dati piccoli tutto funziona, e il cervello si rilassa. Il problema inizia quando «scorriamo» diventa centinaia di migliaia di confronti, poi milioni, e il tuo codice comincia a suonare come un portatile che sta decollando.

Il controllo di membership e` qualsiasi domanda del tipo: «c'e` l'elemento X nell'insieme?». E queste domande compaiono continuamente: non aggiungere di nuovo un tag, non registrare due volte lo stesso e-mail, non elaborare due volte lo stesso ID, trovare interessi in comune, verificare se un valore e` consentito.

Per questo tipo di compiti Set in Swift e` uno strumento di base: conserva il fatto della presenza di un elemento e rende questi controlli veloci.

2. Set in Swift: unicita`, Hashable e operazioni di base

Se la diciamo in modo molto semplice, Set e` una collezione che conserva elementi unici. Cioe` «dentro non ci sono duplicati»: due valori identici in Set non possono convivere, anche se glielo chiedi con grande insistenza. Proprio per questo Set e` perfetto per la domanda «abbiamo gia` visto questo valore?».

Perche` a volte si parla di «HashSet»? Perche` sotto il cofano una struttura del genere di solito usa l'hashing per trovare rapidamente gli elementi. In Swift non andiamo a guardare dentro l'implementazione (e facciamo bene — non stiamo scrivendo un compilatore nel fine settimana), ma il risultato pratico e` importante: operazioni come contains e insert in genere scalano bene su grandi dati.

Un altro punto importante: gli elementi di Set devono essere Hashable. Per noi, a questo livello, e` una regola semplice: Int, String, Character e molti altri tipi base vanno bene, mentre i «tipi complessi definiti da noi» li vedremo piu` avanti. Per ora basta sapere questo: se un tipo si puo` mettere in Set, significa che Swift sa confrontarlo e hasharlo in modo abbastanza coerente da usarlo come una «chiave».

Creare un insieme e contains

import Foundation

let bannedWords: Set<String> = ["spam", "scam", "ads"]

print(bannedWords.contains("spam"))   // true
print(bannedWords.contains("swift"))  // false

Qui Set si legge letteralmente come «insieme di parole vietate». Il controllo contains suona come una normale domanda umana.

Insert e «e` stato inserito o c'era gia`»

import Foundation

var ids: Set<Int> = [10, 20, 30]

let r1 = ids.insert(20)
print(r1.inserted) // false

let r2 = ids.insert(40)
print(r2.inserted) // true

insert restituisce un risultato in cui si puo` vedere se abbiamo aggiunto un nuovo elemento o semplicemente «provato ad aggiungere cio` che c'era gia`». Spesso questo aiuta a scrivere codice senza troppi if.

3. Pattern pratici: seen set e deduplicazione

C'e` un piacere particolare nel sostituire cicli annidati con un solo passaggio. E` come togliere i mobili inutili da una stanza: lo stesso significato, meno inciampi. Il pattern seen set e` proprio questo: scorri i dati da sinistra a destra e tieni in Set tutto cio` che hai gia` visto. Se incontri un elemento che e` gia` nel Set, allora e` un duplicato.

Questo pattern e` utile non solo per «trovare un duplicato». Funziona benissimo per «verificare che tutti i valori siano unici», «fermarsi al primo duplicato», «contare il numero di unici», «trovare il primo valore che si ripete». La cosa importante e` che smetti di fare il confronto «tutti contro tutti» e inizi a fare il confronto «ognuno — con Set».

Piccolo schema del processo (e` uno schema, non Swift):

flowchart TD
    A[Prendiamo il prossimo elemento x] --> B{seen contains x?}
    B -- yes --> C[E` un duplicato: reagiamo]
    B -- no --> D["seen.insert(x)"]
    D --> A

Seen set: primo duplicato in un array

import Foundation

let nums = [5, 1, 4, 1, 9]

var seen: Set<Int> = []
var firstDuplicate: Int? = nil

for x in nums {
    if seen.contains(x) {
        firstDuplicate = x
        break
    }
    seen.insert(x)
}

print(firstDuplicate as Any) // Optional(1)

Fai attenzione all'ordine: prima controlliamo contains, poi insert. Cosi` non «permettiamo» a un elemento di coincidere con se stesso all'interno della stessa iterazione.

Deduplicazione tramite Set: veloce, ma l'ordine non e` garantito

Quando senti la parola «deduplicazione», la mano va verso la soluzione piu` corta:

let unique = Set(array)

Ed e` davvero un trucco utile: ottenere un insieme di elementi unici da un array si puo` fare letteralmente con una sola riga. Ma c'e` un dettaglio importante: Set non e` obbligato a mantenere l'ordine degli elementi. Quindi, se per te e` importante «in che ordine sono comparsi», un solo Set(array) non basta.

E non e` una «particolarita` di Swift oggi», ma l'idea stessa di insieme: si occupa dell'unicita`, non dell'ordine. Per questo, nei problemi del tipo «elimina i duplicati ma mantieni l'ordine della lista originale», spesso si usa una combinazione: Set per controllare «visto/non visto» e Array per il risultato.

Tra l'altro, nei materiali sui miglioramenti della libreria standard si sottolinea separatamente che dopo alcune operazioni (per esempio, lazy.filter) puoi ottenere non un Set, ma un «wrapper-collezione», e allora a volte conviene raccogliere di nuovo esplicitamente Set(...), per tornare proprio all'insieme e alle sue operazioni.

import Foundation

let tags = ["swift", "cli", "swift", "set"]
let uniqueTags = Set(tags)

print(uniqueTags) // l'ordine puo` essere qualsiasi

Il risultato puo` arrivare in un ordine diverso, ed e` normale.

Deduplicazione mantenendo l'ordine

import Foundation

let tags = ["swift", "cli", "swift", "set"]

var seen: Set<String> = []
var orderedUnique: [String] = []

for t in tags {
    if !seen.contains(t) {
        seen.insert(t)
        orderedUnique.append(t)
    }
}

print(orderedUnique) // ["swift", "cli", "set"]

Questo e` uno dei metodi piu` pratici: Set e` responsabile dell'«unicita`», l'array — dell'«ordine».

4. Matematica degli insiemi nel codice

Set ha anche un altro superpotere: sa esprimere la «matematica degli insiemi» direttamente nel codice. E questo, all'improvviso, rende le soluzioni piu` leggibili: invece di «scorri un array, poi l'altro, poi confronta...», scrivi «intersezione» o «unione», e il significato si vede subito.

Le operazioni piu` utili:

  • intersection — elementi in comune;
  • union — tutto insieme senza duplicati;
  • subtracting — elementi del primo insieme senza quelli del secondo;
  • symmetricDifference — elementi presenti in uno solo dei due insiemi (come un «XOR», ma per collezioni).

Si`, suona un po' «matematico», ma in pratica sono domande del tipo «quali tag sono in comune tra due libri» oppure «quali utenti ci sono nella lista A ma non nella lista B».

Intersezione degli interessi

import Foundation

let alice: Set<String> = ["swift", "music", "travel"]
let bob: Set<String> = ["java", "music", "swift"]

let common = alice.intersection(bob)
print(common) // {"music", "swift"} (l'ordine non conta)

Differenze e symmetricDifference

import Foundation

let alice: Set<String> = ["swift", "music", "travel"]
let bob: Set<String> = ["java", "music", "swift"]

let onlyAlice = alice.subtracting(bob)
let onlyBob = bob.subtracting(alice)
let exactlyOne = alice.symmetricDifference(bob)

print(onlyAlice)   // {"travel"}
print(onlyBob)     // {"java"}
print(exactlyOne)  // {"travel", "java"}

Con operazioni del genere Set smette di essere «solo un contenitore» e diventa un modo per formulare un pensiero in modo compatto.

5. Mini esempio: una piccola libreria e i sottoinsiemi

Non siamo ancora arrivati ai tipi personalizzati tramite struct, quindi usiamo con sincerita` cio` che possiamo gia` usare: tuple, array e insiemi. Immaginiamo un mini catalogo di libri come un array di tuple (id, title, tags). Poi facciamo due funzioni utili: deduplicare i tag su tutta la libreria e controllare se un libro contiene tutti i tag richiesti.

Dati «cosi` come sono»

import Foundation

let books: [(id: Int, title: String, tags: [String])] = [
    (1, "Swift Basics", ["swift", "beginner", "cli"]),
    (2, "Algorithms", ["arrays", "set", "dictionary"]),
    (3, "Swift CLI Tools", ["swift", "cli", "set"])
]

Raccogliere tutti i tag unici della libreria

import Foundation

func allUniqueTags(in books: [(id: Int, title: String, tags: [String])]) -> Set<String> {
    var result: Set<String> = []
    for b in books {
        for t in b.tags {
            result.insert(t)
        }
    }
    return result
}

print(allUniqueTags(in: books))

Qui usiamo Set come «contenitore dell'unicita`»: non importa quante volte un tag appare, importa che ci sia.

Verificare che un libro contenga tutti i tag richiesti

import Foundation

func hasAllTags(bookTags: [String], required: Set<String>) -> Bool {
    let tagSet = Set(bookTags)
    return required.isSubset(of: tagSet)
}

print(hasAllTags(bookTags: ["swift", "cli", "set"], required: ["swift", "set"])) // true
print(hasAllTags(bookTags: ["swift", "cli"], required: ["swift", "set"]))       // false

Qui c'e` nascosto un importante «bonus di leggibilita`»: trasformiamo l'array di tag in Set, perche` un insieme ha un'operazione naturale isSubset(of:). E` proprio il caso in cui scrivi il codice «come un pensiero»: «required e` un sottoinsieme dei tag del libro».

6. Prestazioni e limiti reali

Viene voglia di dire: «Set e` magia, e tutto funziona all'istante». Ma e` meglio tenere un quadro realistico. Di solito Set.contains e Set.insert funzionano velocemente, perche` all'interno si usa l'hashing: dal valore si calcola una «impronta» e, grazie a questa, si trova rapidamente il posto nella struttura. Anche se ci sono collisioni (quando valori diversi producono impronte simili), la struttura sa comunque distinguere correttamente gli elementi tramite il confronto.

Pero` «veloce» non significa «sempre istantaneo allo stesso modo con qualunque dato». E` un modello «in media», su input normali. In pratica, per noi basta questa regola: se fai molte domande «c'e` l'elemento» su dati grandi, l'array puo` cominciare a rallentare, mentre Set di solito se la cava molto meglio.

Per fissare la scelta della struttura, e` utile tenere una piccola tabella:

Domanda del problema Cosa scegliere Perche` semanticamente
«C'e` un elemento del genere?»
Set
conserva il fatto della presenza
«Eliminare i duplicati» Set oppure Set + Array Set elimina, Array mantiene l'ordine
«Trovare gli elementi in comune di due liste»
Set
ci sono intersection e amici
«Quante volte compare?»
Dictionary<Key, Int>
serve un contatore, non solo il fatto della presenza

Se riesci a prendere questo stile di pensiero «a domanda», comincerai a scegliere la struttura dati quasi automaticamente.

7. Errori tipici quando si usa Set per i controlli di membership

Errore n. 1: aspettarsi che Set conservi l'ordine.
Una trappola molto comune: fai Set(array) e poi ti stupisci perche` gli elementi si sono «mescolati». Set e` pensato per l'unicita` e per controlli rapidi, mentre l'ordine non fa parte del suo contratto. Se l'ordine conta, conserva il risultato in un array e usa seen set come filtro.

Errore n. 2: risolvere il problema del «primo unico» solo con Set.
Set risponde alla domanda «c'e` l'elemento in generale», ma non conserva «quante volte e` apparso» e «in quale punto e` apparso per la prima volta». Per questo, per problemi come «trova il primo simbolo unico», di solito serve un dizionario delle frequenze + una seconda passata. Set qui puo` essere parte della soluzione, ma non tutta la logica.

Errore n. 3: fare il controllo di membership in un array dentro un ciclo, quando i controlli sono molti.
Un codice del tipo «per ogni elemento controllo array.contains(...)» spesso diventa in modo silenzioso una complessita` quadratica, soprattutto se dentro c'e` un altro ciclo. Se dentro un ciclo chiedi regolarmente «l'abbiamo gia` visto?», quasi sempre conviene introdurre Set.

Errore n. 4: confondere «deduplicazione» con «unificazione con mantenimento dell'ordine».
Se il problema suona come «rimuovi i duplicati», non avere fretta. A volte si intende «rimuovi i duplicati, ma lascia l'ordine come nella lista originale». Un solo Set non mantiene l'ordine, quindi usa la coppia: Set per il controllo e un array per il risultato.

Errore n. 5: provare a conservare in Set dati che hanno bisogno di informazioni aggiuntive.
Se capisci che non ti serve solo «c'e` l'elemento», ma anche «quante volte», «in quali posizioni», «con quale contesto», allora Set diventa un contenitore troppo povero. In quel momento e` meglio passare a Dictionary, dove il valore puo` essere un contatore, un array di posizioni o qualsiasi altro payload utile.

1
Compito
Swift SELF, livello 22, lezione 1
Bloccato
Filtro delle parole vietate
Filtro delle parole vietate
1
Compito
Swift SELF, livello 22, lezione 1
Bloccato
Controllo degli accessi
Controllo degli accessi
1
Compito
Swift SELF, livello 22, lezione 1
Bloccato
Tag puliti del post
Tag puliti del post
1
Compito
Swift SELF, livello 22, lezione 1
Bloccato
Confronto tra due insiemi
Confronto tra due insiemi
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION