Das respostas para (Quando) a consulta de tabela de hash O (1)? , Concluí que as tabelas de hash têm pior comportamento, pelo menos amortizado, quando os dados satisfazem certas condições estatísticas e existem técnicas para ajudar a tornar essas condições amplas.
No entanto, da perspectiva de um programador, não sei de antemão quais serão meus dados: geralmente vêm de alguma fonte externa. E eu raramente tenho todos os dados de uma só vez: geralmente inserções e exclusões acontecem a uma taxa que não fica muito abaixo da taxa de pesquisas, portanto, o pré-processamento dos dados para ajustar a função hash acaba.
Então, dando um passo à frente: dado algum conhecimento sobre a fonte de dados, como posso determinar se uma tabela de hash tem chance de ter operações e possivelmente quais técnicas usar na minha função de hash?
fonte
Respostas:
Existem várias técnicas que garantem que as pesquisas sempre exijam operações O (1), mesmo no pior caso.
O pior caso ocorre quando algum invasor mal-intencionado (Mallory) deliberadamente fornece dados que Mallory selecionou especificamente para tornar o sistema lento.
Depois de escolher uma função hash específica, é provavelmente otimista demais supor que Mallory nunca descobrirá qual função hash você escolheu. Depois que Mallory descobrir qual função de hash você escolheu, se você permitir que Mallory lhe forneça muitos dados a serem inseridos em sua tabela de hash usando essa função de hash, você estará condenado: Mallory pode gerar rapidamente internamente bilhões de itens de dados, hash-os com o seu A função hash para encontrar quais itens de dados provavelmente colidirão e, em seguida, fornecerá milhões de itens de dados que podem colidir, resultando em pesquisas que são muito mais lentas que O (1).
Todas as técnicas que garantem "pesquisas O (1) mesmo nos piores casos" evitam esse problema, fazendo um pouco de trabalho extra em cada inserção para garantir que, no futuro, todas as pesquisas possíveis tenham êxito no tempo O (1) . Em particular, assumimos (no pior caso) que Mallory descobrirá, mais cedo ou mais tarde, qual função de hash estamos usando; mas ele só tem a chance de inserir alguns itens de dados antes de escolher uma função de hash diferente - hash de tabulação ou algum outro hash universal - um que selecionamos especialmente para que todos os dados que temos até agora possam ser pesquisados em 2 ou 3 sondas - ou seja, O (1). Como selecionamos essa função aleatoriamente, podemos ter certeza de que Mallory não saberá qual função escolhemos por um tempo. Mesmo se Malloryimediatamente nos fornece dados que, mesmo com essa nova função de hash, colidem com dados anteriores, podemos escolher outra nova função de hash, de modo que, após a revisão, todos os dados anteriores que ele e todos os outros nos forneceram agora possam ser visualizados em 2 ou 3 sondas no pior caso - ou seja, O (1) pesquisas no pior caso.
É bastante fácil selecionar aleatoriamente uma nova função de hash e repetir toda a tabela com frequência suficiente para garantir que cada pesquisa seja sempre O (1). Embora isso garanta que cada pesquisa seja sempre O (1), essas técnicas, ao inserir o item N em uma tabela de hash que já contém itens N-1, ocasionalmente podem exigir tempo O (N) para essa inserção. No entanto, é possível projetar o sistema de forma que, mesmo quando Mallory deliberadamente forneça novos dados que, usando a nova função hash, colidem com dados anteriores, o sistema possa aceitar muitos itens de Mallory e outros antes de precisar fazer uma reconstrução total de O (N). As técnicas de tabela de hash que selecionam uma nova função e refazer a tarefa para garantir O (1) pesquisas, mesmo no pior caso, incluem:
Estruturas de dados / tabelas de hash
fonte
A pesquisa da tabela de hash sempre pode ser para conjuntos estáticos; consulte o artigo de 2002 de Arne Andersson e Mikkel Thorup: conjuntos ordenados dinâmicos com árvores de pesquisa exponenciaisO(1)
No caso geral, Andersson et al fornecem um algoritmo para estruturas de dados indexadas por hash que suportam pesquisas e atualizações em . Além disso, eles provam que esse limite é ótimo. Portanto, sabemos exatamente o quão perto podemos chegar de no caso geral.O(1)O(logn/loglogn−−−−−−−−−−−√) O(1)
fonte
Não sou especialista em estruturas de dados, mas a abordagem teórica usual para o hash é que se defina uma família de funções (por exemplo, ) e, em seguida, considere o comportamento em um na pior das hipóteses, um membro da família escolhido aleatoriamente , onde o adversário não conhece a escolha aleatória com antecedência. Isso é semelhante à forma como os algoritmos aleatórios são analisados também: a expectativa é assumida sobre as escolhas do algoritmo, não a distribuição de entrada.ha,b(x)=ax+bmodp
No passado, de acordo com um artigo da Usenix de Crosby e Wallach , as linguagens de programação comuns não faziam nada assim, deixando muitos aplicativos da Web (e outros servidores) abertos a um ataque de DoS com base em colisões de fabricação. (O artigo é de 2003, mas sugere que Dan Bernstein havia descoberto a mesma idéia um pouco antes).
Uma pesquisa rápida no Google afirma que o estado da arte em termos de implementações melhorou e não melhorou .
Outro aspecto é que, em um mundo de grande largura de banda, os ataques de tempo dificultam a localização de colisões online (em vez de offline, como sugere o link Crosby-Wallach). Parece que me lembro que Daniel Golovin teve resultados alguns anos atrás em estruturas de dados que não são vulneráveis a ataques de tempo, mas não sei se eles são amplamente utilizados.
fonte
A análise de casos médios para as tabelas de hash é feita sob o pressuposto usual de uniformidade das entradas, o que antes ocorre devido à navalha do occam.
Se você tiver conhecimento adicional sobre o domínio e a distribuição das chaves, poderá fazer a mesma análise de caso médio e substituir a distribuição uniforme por sua distribuição e recalcular as expectativas, pelo menos em teoria.
Obviamente, a dificuldade decorre do fato de que análises não uniformes de casos de avaérage 'são difíceis de resolver. E seu "conhecimento" pode não ser convenientemente expressável como uma distribuição que pode ser usada facilmente nessa análise.
Obviamente, a coisa mais fácil de fazer são simulações. Implemente as tabelas de hash e observe como elas se saem para o seu conjunto típico de entradas.
fonte
Permutações (de comprimento fixo), como um caso específico de conjuntos finitos conhecidos: é relativamente fácil atribuir números únicos às permutações, como neste artigo . Eu usei isso (em uma implementação um pouco menos horrível) para mapear permutações de comprimento em uma matriz de tamanho. Mas eu poderia fazer isso porque acabaria precisando de toda permutação; se você estiver usando apenas um subconjunto, precisará de uma função personalizada para esse subconjunto ou de uma matriz esparsa eficiente.n !n n!
fonte