Come viene implementato set ()?


152

Ho visto persone dire che gli setoggetti in Python hanno il controllo dell'appartenenza O (1). Come vengono implementati internamente per consentire ciò? Che tipo di struttura dati utilizza? Quali altre implicazioni ha questa implementazione?

Ogni risposta qui è stata davvero illuminante, ma posso accettarne solo una, quindi andrò con la risposta più vicina alla mia domanda originale. Grazie a tutti per le informazioni!

Risposte:


139

Secondo questa discussione :

In effetti, i set di CPython sono implementati come qualcosa di simile ai dizionari con valori fittizi (le chiavi sono i membri del set), con alcune ottimizzazioni che sfruttano questa mancanza di valori

Quindi fondamentalmente a setusa un hashtable come struttura di dati sottostante. Questo spiega il controllo dell'iscrizione O (1), poiché la ricerca di un elemento in una tabella hash è un'operazione O (1), in media.

Se sei così incline, puoi anche sfogliare il codice sorgente di CPython per un set che, secondo Achim Domma , è per lo più un taglia e incolla dall'implementazione dict.


18
IIRC, l' setimplementazione originale era in realtà dictcon valori fittizi e in seguito è stata ottimizzata.
dan04,

1
Big O non è lo scenario peggiore? Se riesci a trovare un'istanza in cui il tempo è O (n), allora è O (n) .. Non capisco nulla in questo momento da tutti quei tutorial.
Claudiu Creanga,

4
No, il caso medio è O (1) ma il caso peggiore è O (N) per la ricerca della tabella hash.
Justin Ethier,

4
@ClaudiuCreanga questo è un vecchio commento, ma solo per chiarire: la notazione big-O ti dice limiti superiori sul tasso di crescita delle cose, ma puoi legare in alto la crescita delle prestazioni medie del caso e puoi separare in alto separatamente la crescita nel caso peggiore prestazione.
Kirk Boyer,

79

Quando le persone dicono che i set hanno un controllo dell'iscrizione O (1), stanno parlando del caso medio . Nel caso peggiore (quando tutti i valori con hash si scontrano) il controllo dell'appartenenza è O (n). Guarda la wiki di Python sulla complessità temporale .

L' articolo di Wikipedia afferma che la migliore complessità temporale per una tabella hash che non viene ridimensionata è O(1 + k/n). Questo risultato non si applica direttamente ai set Python poiché i set Python utilizzano una tabella hash che si ridimensiona.

Un po 'più avanti sull'articolo di Wikipedia afferma che per il caso medio , e assumendo una semplice funzione di hashing uniforme, la complessità del tempo è O(1/(1-k/n)), dove k/npuò essere delimitata da una costante c<1.

Big-O si riferisce solo al comportamento asintotico come n → ∞. Poiché k / n può essere limitato da una costante, c <1, indipendente da n ,

O(1/(1-k/n))non è più grande di quello O(1/(1-c))che equivale a O(constant)= O(1).

Quindi, supponendo un hashing semplice uniforme, in media , il controllo dell'appartenenza per i set Python lo è O(1).


14

Penso che sia un errore comune, la setricerca (o hashtable per quella materia) non sono O (1).
da Wikipedia

Nel modello più semplice, la funzione hash è completamente non specificata e la tabella non viene ridimensionata. Per la migliore scelta possibile della funzione hash, una tabella di dimensione n con indirizzamento aperto non ha collisioni e contiene fino a n elementi, con un solo confronto per una ricerca riuscita, e una tabella di dimensione n con concatenamento e tasti k ha il massimo minimo (0, kn) collisioni e confronti di O (1 + k / n) per la ricerca. Per la peggiore scelta della funzione hash, ogni inserimento provoca una collisione e le tabelle hash degenerano in ricerca lineare, con confronti ammortizzati Ω (k) per inserimento e fino a k confronti per una ricerca riuscita.

Correlati: un hashmap Java è davvero O (1)?


4
Ma richiedono sempre tempo per cercare gli elementi: python -m timeit -s "s = set (range (10))" "5 in s" 10000000 loop, meglio di 3: 0.0642 usec per loop <--> python - m timeit -s "s = set (range (10000000))" "5 in s" 10000000 loop, meglio di 3: 0,0634 usec per loop ... ed è il set più grande che non genera MemoryErrors
Jochen Ritzel,

2
@ THC4k Tutto ciò che hai dimostrato è che la ricerca di X viene eseguita in tempo costante, ma ciò non significa che il tempo di ricerca di X + Y richiederà lo stesso tempo, ovvero O (1).
Shay Erlichmen,

3
@intuited: sì, ma il test eseguito sopra non dimostra che puoi cercare "5" nello stesso tempo in cui puoi cercare "485398", o qualche altro numero che potrebbe trovarsi in un orribile spazio di collisione. Non si tratta di cercare lo stesso elemento in un hash di dimensioni diverse allo stesso tempo (in realtà, ciò non è affatto necessario), ma piuttosto se è possibile accedere a ciascuna voce nello stesso periodo di tempo nella tabella corrente - qualcosa che è praticamente impossibile da realizzare per le tabelle hash poiché generalmente ci saranno sempre delle collisioni.
Nick Bastin,

3
In altre parole, il tempo necessario per effettuare una ricerca dipende dal numero di valori memorizzati, poiché ciò aumenta la probabilità di collisioni.
intuito il

3
@intuited: no, non è corretto. Quando il numero di valori memorizzati aumenta, Python aumenta automaticamente la dimensione dell'hashtable e il tasso di collisione rimane approssimativamente costante. Supponendo un algoritmo hash O (1) distribuito uniformemente, la ricerca hashtable viene ammortizzata O (1). Potresti voler guardare la presentazione video "The Mighty Dictionary" python.mirocommunity.org/video/1591/…
Lie Ryan,

13

Tutti abbiamo un facile accesso alla fonte , dove il commento precedente set_lookkey()dice:

/* set object implementation
 Written and maintained by Raymond D. Hettinger <python@rcn.com>
 Derived from Lib/sets.py and Objects/dictobject.c.
 The basic lookup function used by all operations.
 This is based on Algorithm D from Knuth Vol. 3, Sec. 6.4.
 The initial probe index is computed as hash mod the table size.
 Subsequent probe indices are computed as explained in Objects/dictobject.c.
 To improve cache locality, each probe inspects a series of consecutive
 nearby entries before moving on to probes elsewhere in memory.  This leaves
 us with a hybrid of linear probing and open addressing.  The linear probing
 reduces the cost of hash collisions because consecutive memory accesses
 tend to be much cheaper than scattered probes.  After LINEAR_PROBES steps,
 we then use open addressing with the upper bits from the hash value.  This
 helps break-up long chains of collisions.
 All arithmetic on hash should ignore overflow.
 Unlike the dictionary implementation, the lookkey function can return
 NULL if the rich comparison returns an error.
*/


...
#ifndef LINEAR_PROBES
#define LINEAR_PROBES 9
#endif

/* This must be >= 1 */
#define PERTURB_SHIFT 5

static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)  
{
...

2
Questa risposta potrebbe beneficiare di C evidenziazione della sintassi . L'evidenziazione della sintassi Python del commento sembra davvero negativa.
user202729

Per quanto riguarda il commento "Questo ci lascia con un ibrido di sondaggio lineare e indirizzamento aperto", il sondaggio lineare non è una sorta di risoluzione delle collisioni nell'indirizzamento aperto, come descritto in en.wikipedia.org/wiki/Open_addressing ? Pertanto, il sondaggio lineare è un sottotipo di indirizzamento aperto e il commento non ha senso.
Alan Evangelista,

2

Per enfatizzare un po 'di più la differenza tra set'se dict's, ecco un estratto dalle setobject.csezioni dei commenti, che chiarisce la principale differenza tra set e dicts.

I casi d'uso per gli insiemi differiscono notevolmente dai dizionari in cui è più probabile che siano presenti chiavi di ricerca. Al contrario, i set riguardano principalmente i test di appartenenza in cui la presenza di un elemento non è nota in anticipo. Di conseguenza, l'implementazione impostata deve essere ottimizzata sia per il caso trovato che per quello non trovato.

fonte su github

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.