Como o set () é implementado?

151

Eu já vi pessoas dizerem que setobjetos em python têm O (1) verificação de associação. Como eles são implementados internamente para permitir isso? Que tipo de estrutura de dados ele usa? Que outras implicações essa implementação tem?

Todas as respostas aqui foram realmente esclarecedoras, mas só posso aceitar uma, por isso vou com a resposta mais próxima da minha pergunta original. Obrigado a todos pela informação!

Daenyth
fonte

Respostas:

139

De acordo com este tópico :

De fato, os conjuntos do CPython são implementados como algo como dicionários com valores fictícios (as chaves são os membros do conjunto), com algumas otimizações que exploram essa falta de valores

Então, basicamente, um setusa uma hashtable como sua estrutura de dados subjacente. Isso explica a verificação de associação O (1), uma vez que procurar um item em uma tabela de hash é uma operação O (1), em média.

Se você é tão inclinado, pode até procurar o código-fonte CPython para o conjunto que, de acordo com Achim Domma , é principalmente um recorte e cola da dictimplementação.

Justin Ethier
fonte
18
IIRC, a setimplementação original estava dict com valores fictícios e foi otimizada mais tarde.
dan04
1
O grande O não é o pior cenário? Se você pode encontrar uma instância em que o tempo é O (n), então é O (n). Eu não entendo nada agora de todos esses tutoriais.
Claudiu Creanga
4
Não, o caso médio é O (1), mas o pior caso é O (N) para pesquisa de tabela de hash.
23616 Justin Ethier
4
@ClaudiuCreanga este é um comentário antigo, mas apenas para esclarecer: a notação big-O indica limites superiores na taxa de crescimento das coisas, mas você pode limitar o crescimento do desempenho médio do caso e pode limitar separadamente o crescimento do pior caso desempenho.
22418 Kirk Boyer
79

Quando as pessoas dizem que os conjuntos têm O (1) verificação de associação, estão falando sobre o caso médio . Na pior das hipóteses (quando todos os valores de hash colidem), a verificação de associação é O (n). Veja o wiki do Python sobre complexidade de tempo .

O artigo da Wikipedia diz que a melhor complexidade de tempo para uma tabela de hash que não é redimensionada é O(1 + k/n). Esse resultado não se aplica diretamente aos conjuntos Python, pois os conjuntos Python usam uma tabela de hash que é redimensionada.

Um pouco mais adiante, no artigo da Wikipedia, diz que, para o caso médio , e assumindo uma função simples de hash uniforme, a complexidade do tempo é O(1/(1-k/n))onde k/npode ser limitada por uma constante c<1.

Big-O refere-se apenas ao comportamento assintótico como n → ∞. Como k / n pode ser limitado por uma constante, c <1, independente de n ,

O(1/(1-k/n))não é maior O(1/(1-c))que o equivalente a O(constant)= O(1).

Portanto, assumindo um hash simples e uniforme, em média , a verificação de associação para conjuntos Python é O(1).

unutbu
fonte
14

Eu acho que é um erro comum, setpesquisa (ou hashtable para esse assunto) não são O (1).
da Wikipedia

No modelo mais simples, a função hash é completamente não especificada e a tabela não é redimensionada. Para a melhor escolha possível da função de hash, uma tabela de tamanho n com endereçamento aberto não tem colisões e suporta até n elementos, com uma única comparação para uma pesquisa bem-sucedida, e uma tabela de tamanho n com chaves encadeamento e k tem o mínimo máximo Colisões (0, kn) e comparações O (1 + k / n) para pesquisa. Para a pior escolha da função hash, toda inserção causa uma colisão e as tabelas de hash degeneram para a pesquisa linear, com Ω (k) comparações amortizadas por inserção e até k comparações para uma pesquisa bem-sucedida.

Relacionado: Um hashmap Java é realmente O (1)?

Shay Erlichmen
fonte
4
Mas eles levam um tempo constante para procurar itens: python -m timeit -s "s = set (range (10))" "5 in s" 10000000 loops, melhor de 3: 0,0642 usec por loop <--> python - m timeit -s "s = set (range (10000000))" "5 in s" 10000000 loops, o melhor de 3: 0,0634 usec por loop ... e esse é o maior conjunto que não lança MemoryErrors
Jochen Ritzel,
2
@ THC4k Tudo que você provou é que procurar X é feito em tempo constante, mas isso não significa que o tempo para procurar X + Y levará a mesma quantidade de tempo, que é O (1).
Shay Erlichmen
3
@intuited: Sim, mas o teste acima não prova que você pode procurar "5" ao mesmo tempo em que pode procurar "485398", ou algum outro número que possa estar em um horrível espaço de colisão. Não se trata de procurar o mesmo elemento em um hash de tamanho diferente ao mesmo tempo (na verdade, isso não é necessário), mas sim se você pode acessar cada entrada na mesma quantidade de tempo na tabela atual - algo que é basicamente impossível para as tabelas de hash, pois geralmente sempre haverá colisões.
Nick Bastin
3
Em outras palavras, o tempo para fazer uma pesquisa depende do número de valores armazenados, porque isso aumenta a probabilidade de colisões.
intuited
3
@ intuited: não, isso está incorreto. Quando o número de valores armazenados aumentar, o Python aumentará automaticamente o tamanho da hashtable e a taxa de colisão permanecerá aproximadamente constante. Assumindo um algoritmo de hash O (1) distribuído uniformemente, a pesquisa de hashtable é amortizada O (1). Você pode assistir à apresentação em vídeo "O Dicionário Poderoso" python.mirocommunity.org/video/1591/…
Lie Ryan
13

Todos temos acesso fácil à fonte , onde o comentário anterior set_lookkey()diz:

/* set object implementation
 Written and maintained by Raymond D. Hettinger <[email protected]>
 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)  
{
...
gimel
fonte
2
Esta resposta poderia beneficiar C destaque de sintaxe . O realce da sintaxe em Python do comentário parece muito ruim.
user202729
Com relação ao comentário "Isso nos deixa com um híbrido de sondagem linear e endereçamento aberto", a sondagem linear não é um tipo de resolução de colisão no endereçamento aberto, conforme descrito em en.wikipedia.org/wiki/Open_addressing ? Portanto, a análise linear é um subtipo de endereçamento aberto e o comentário não faz sentido.
Alan Evangelista
2

Para enfatizar um pouco mais a diferença entre set'se dict's, eis um trecho das setobject.cseções de comentários, que esclarecem a principal diferença entre sets e ditados.

Os casos de uso para conjuntos diferem consideravelmente dos dicionários em que as chaves pesquisadas têm maior probabilidade de estar presentes. Por outro lado, os conjuntos são principalmente sobre teste de associação, onde a presença de um elemento não é conhecida antecipadamente. Consequentemente, a implementação do conjunto precisa otimizar para o caso encontrado e o não encontrado.

fonte no github

user1767754
fonte