[Algoritmo e estrutura de dados 09] Tabela de hash - uma ferramenta poderosa para pesquisa eficiente


Prefácio:

Antes, estudamos tabelas lineares, arrays, strings e árvores. Todos eles têm um defeito tão grande que a pesquisa das condições numéricas dos dados exige o cruzamento de todos ou de parte dos dados . Então, há uma maneira de omitir o processo de comparação de dados, melhorando ainda mais a eficiência da busca por condições numéricas?

A resposta é, claro, sim! Nesta lição, apresentaremos um artefato de pesquisa eficiente: a tabela hash.

Insira a descrição da imagem aqui


Um, o que é uma mesa de hash

O nome da tabela hash é derivado de Hash e também pode ser chamado de tabela hash . A tabela de hash é uma estrutura de dados especial, que é muito diferente das estruturas de dados que aprendemos antes, como arrays, listas vinculadas e árvores.

1.1 Princípio da Tabela de Hash

Uma tabela hash é uma estrutura de dados que usa funções hash para organizar os dados para oferecer suporte à inserção e pesquisa rápidas . A ideia central de uma tabela hash é usar uma função hash para mapear as chaves para os depósitos . mais especificamente:

  • Quando inserimos uma nova chave, a função hash determinará para qual depósito a chave deve ser alocada e armazenará a chave no depósito correspondente;
  • Quando queremos pesquisar uma chave, a tabela hash usará a mesma função hash para encontrar o depósito correspondente e pesquisar apenas em um depósito específico.

Aqui está um exemplo simples, vamos entender:

Insira a descrição da imagem aqui
No exemplo, usamos y = x% 5 como a função hash. Vamos usar este exemplo para completar a estratégia de inserção e pesquisa:

  • Inserção: analisamos as chaves por meio da função hash e as mapeamos para os depósitos correspondentes. Por exemplo, 1987 é atribuído ao segmento 2 e 24 é atribuído ao segmento 4.
  • Pesquisa: analisamos as chaves por meio da mesma função hash e pesquisamos apenas em depósitos específicos. Por exemplo, se pesquisarmos 23, mapearemos 23 a 3 e pesquisaremos no intervalo 3. Descobrimos que 23 não está no intervalo 3, o que significa que 23 não está na tabela de hash.

1.2 Projetar uma função hash

A função hash é uma tabela hash dos componentes mais importantes, a tabela hash é usada para mapear as chaves para um determinado depósito . No exemplo anterior, usamos y = x% 5 como a função hash, onde x é o valor da chave ey é o índice do intervalo alocado .

A função hash dependerá do intervalo de valores-chave e do número de depósitos . Aqui estão alguns exemplos de
Insira a descrição da imagem aqui
funções hash : O design de funções hash é uma questão em aberto. A ideia é atribuir chaves a depósitos o máximo possível. Idealmente, a função de hash perfeita será um mapeamento um para um entre chaves e depósitos. No entanto, na maioria dos casos, a função hash não é perfeita. Ela requer uma compensação entre o número de depósitos e a capacidade do depósito.

Claro, também podemos personalizar algumas funções hash. Os métodos gerais são:

  • Método de personalização direta . A função hash é uma função linear de palavras-chave para endereços. Por exemplo, H (tecla) = a * tecla + b. Aqui, a e b são constantes definidas.
  • Método de análise digital . Suponha que cada chave no conjunto de chaves seja composta de números de s dígitos (k1, k2, ..., Ks) e vários bits uniformemente distribuídos sejam extraídos dela para formar um endereço hash.
  • O quadrado é o método chinês . Se cada dígito da palavra-chave tiver certos dígitos repetidos e a frequência for muito alta, podemos primeiro encontrar o valor do quadrado da palavra-chave, expandir a diferença pelo quadrado e, em seguida, usar os dígitos do meio como o endereço de armazenamento final.
  • Método de dobragem . Se a palavra-chave tiver muitos dígitos, você pode dividir a palavra-chave em várias partes de comprimento igual e obter o valor de sua sobreposição e (arredondar para cima) como o endereço hash.
  • Além do método de resto . Defina um número p antecipadamente e, em seguida, execute a operação restante com a palavra-chave. Ou seja, o endereço é a chave% p.

Dois, resolver conflitos de hash

Idealmente, se nossa função hash for um mapeamento um-para-um perfeito, não precisaremos lidar com conflitos. Infelizmente, na maioria dos casos, o conflito é quase inevitável. Por exemplo, em nossa função hash anterior (y = x% 5), 1987 e 2 são atribuídos ao intervalo 2, que é uma colisão de hash.

As seguintes questões devem ser consideradas para resolver conflitos de hash:

  • Como organizar os valores no mesmo balde?
  • E se muitos valores forem atribuídos ao mesmo intervalo?
  • Como pesquisar o valor alvo em um intervalo específico?

Portanto, quando ocorre um conflito, como o resolvemos?

Existem dois métodos comumente usados: método de endereçamento aberto e método de endereçamento em cadeia .

2.1 Método de endereçamento aberto

Ou seja, quando uma palavra-chave entra em conflito com outra palavra-chave, uma sequência de detecção é formada na tabela de hash usando uma determinada tecnologia de detecção e, em seguida, a sequência de detecção é pesquisada por sua vez. Quando uma célula vazia é encontrada, ela é inserida nela.

O método de detecção comumente usado é o método de detecção linear . Por exemplo, existe um conjunto de palavras-chave {12, 13, 25, 23} e a função hash usada é a chave% 11 . Ao inserir 12, 13, 25, ele pode ser inserido diretamente, os endereços são 1, 2 e 3, respectivamente. Quando 23 é inserido, o endereço hash é 23% 11 = 1.

No entanto, o endereço 1 já está ocupado, então siga o endereço 1 na seqüência até que o endereço 4 seja detectado e esteja vazio, então 23 é inserido nele. Como mostrado abaixo:
Insira a descrição da imagem aqui

2.2 Método de endereço de cadeia

Armazene os registros com o mesmo endereço hash em uma lista vinculada linear. Por exemplo, há um conjunto de palavras-chave {12,13,25,23,38,84,6,91,34} e a função hash usada é a chave% 11. Como mostrado abaixo:
Insira a descrição da imagem aqui

Três, a aplicação de tabela de hash

3.1 Operação básica da tabela hash

Em muitas linguagens de alto nível, as funções hash e os conflitos hash foram colocados em uma caixa preta na parte inferior e os desenvolvedores não precisam projetar a si próprios. Em outras palavras, a tabela hash completa o mapeamento de palavras-chave para endereços e os dados podem ser encontrados por meio de palavras-chave em um nível constante de complexidade de tempo.

Quanto aos detalhes de implementação, como qual função hash é usada, qual tratamento de conflito é usado e até mesmo o endereço hash de um determinado registro de dados, não é necessário que os desenvolvedores prestem atenção. A seguir, do ponto de vista do desenvolvimento real, vamos dar uma olhada na adição, exclusão e verificação de dados pela tabela hash.

A operação de adicionar e excluir dados na tabela hash não envolve o problema de deslocamento de dados após a adição ou exclusão (matrizes devem ser consideradas), portanto, o processamento é bom.

O processo detalhado da pesquisa da tabela hash é: para uma determinada chave, o endereço hash H (chave) é calculado por meio de uma função hash.

  • Se o valor correspondente ao endereço hash estiver vazio, a pesquisa será malsucedida.
  • Caso contrário, a pesquisa foi bem-sucedida.

Embora o processo detalhado de pesquisa de tabela hash seja ainda mais problemático, por causa do processamento de caixa preta de algumas linguagens de alto nível, os desenvolvedores não precisam realmente desenvolver o código subjacente, basta chamar as funções relevantes.

3.2 Vantagens e desvantagens das tabelas de hash

  • Vantagens : Ele pode fornecer operações de inserção-exclusão-pesquisa muito rápidas, não importa a quantidade de dados, valores de inserção e exclusão requerem quase um tempo constante . Em termos de pesquisa, a velocidade da tabela hash é maior do que a da árvore, e o elemento desejado pode ser encontrado em um instante.
  • Desvantagem : os dados na tabela hash não têm nenhum conceito de ordem , portanto, os elementos não podem ser percorridos de uma maneira fixa (como de pequeno a grande). Quando a ordem de processamento de dados é confidencial, escolher uma tabela hash não é uma boa solução. Ao mesmo tempo, as
    chaves na tabela hash não podem ser repetidas, e a tabela hash não é uma boa escolha para dados com repetibilidade muito alta.

Quarto, projete um mapa hash

4.1 Requisitos de projeto

Afirmação:

Projete um mapa de hash sem usar nenhuma biblioteca de tabelas de hash embutida. Especificamente, o design deve incluir as seguintes funções:

  • put (chave, valor) : Insira o par de valores de (chave, valor) no mapa hash. Se o valor correspondente à chave já existir, atualize este valor.
  • get (key) : Retorna o valor correspondente à chave fornecida, se a chave não estiver incluída no mapa, retorna -1.
  • remover (chave ): Se a chave existir no mapa, exclua o par de valores.

Exemplo:

MyHashMap hashMap = new MyHashMap();
hashMap.put(1, 1);          
hashMap.put(2, 2);         
hashMap.get(1);            // 返回 1
hashMap.get(3);            // 返回 -1 (未找到)
hashMap.put(2, 1);         // 更新已有的值
hashMap.get(2);            // 返回 1 
hashMap.remove(2);         // 删除键为2的数据
hashMap.get(2);            // 返回 -1 (未找到)

Nota:

 所有的值都在 [0, 1000000]的范围内。
 操作的总数目在[1, 10000]范围内。
 不要使用内建的哈希库。

4.2 Idéias de Design

A tabela de hash é uma estrutura de dados comum disponível em diferentes idiomas. Por exemplo, dict em Python, map em C ++ e Hashmap em Java. A característica da tabela hash é que o valor pode ser acessado rapidamente de acordo com a chave fornecida.

A ideia mais simples é usar a aritmética modular como o método hash. Para reduzir a probabilidade de colisões hash, o módulo de números primos geralmente é usado, como o módulo 2069.

Defina o array como espaço de armazenamento e calcule o subscrito do array pelo método hash. Para resolver a colisão de hash (ou seja, o valor da chave é diferente, mas o subscrito de mapeamento é o mesmo), um depósito é usado para armazenar todos os valores correspondentes. Buckets podem ser implementados como arrays ou listas vinculadas. Nas implementações específicas a seguir, arrays são usados ​​em Python.

Defina os métodos da tabela hash, get (), put () e remove (). O processo de endereçamento é o seguinte:

  • Para um determinado valor-chave, use o método hash para gerar o código hash do valor-chave e use o código hash para localizar o espaço de armazenamento. Para cada código hash, um depósito pode ser encontrado para armazenar o valor correspondente ao valor da chave.
  • Depois de encontrar um intervalo, verifique se o par de valores-chave já existe por meio de passagem .

Insira a descrição da imagem aqui

4.3 Caso prático

A implementação do Python é a seguinte:

class Bucket:
    def __init__(self):
        self.bucket = []

    def get(self, key):
        for (k, v) in self.bucket:
            if k == key:
                return v
        return -1

    def update(self, key, value):
        found = False
        for i, kv in enumerate(self.bucket):
            if key == kv[0]:
                self.bucket[i] = (key, value)
                found = True
                break

        if not found:
            self.bucket.append((key, value))

    def remove(self, key):
        for i, kv in enumerate(self.bucket):
            if key == kv[0]:
                del self.bucket[i]


class MyHashMap(object):

    def __init__(self):
        """
        Initialize your data structure here.
        """
        # better to be a prime number, less collision
        self.key_space = 2069
        self.hash_table = [Bucket() for i in range(self.key_space)]


    def put(self, key, value):
        """
        value will always be non-negative.
        :type key: int
        :type value: int
        :rtype: None
        """
        hash_key = key % self.key_space
        self.hash_table[hash_key].update(key, value)


    def get(self, key):
        """
        Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key
        :type key: int
        :rtype: int
        """
        hash_key = key % self.key_space
        return self.hash_table[hash_key].get(key)


    def remove(self, key):
        """
        Removes the mapping of the specified value key if this map contains a mapping for the key
        :type key: int
        :rtype: None
        """
        hash_key = key % self.key_space
        self.hash_table[hash_key].remove(key)


# Your MyHashMap object will be instantiated and called as such:
# obj = MyHashMap()
# obj.put(key,value)
# param_2 = obj.get(key)
# obj.remove(key)

Análise de complexidade:

  • Complexidade de tempo: a complexidade de tempo de cada método é O (N / K), onde N é o número de todos os valores-chave possíveis, K é o número de intervalos predefinidos na tabela hash, onde K é 2069. Aqui, assumimos que o valor da chave é distribuído uniformemente em todos os intervalos e o tamanho médio do intervalo é N / K. No pior caso, um intervalo completo precisa ser percorrido, então a complexidade do tempo é O (N / K).
  • Complexidade do espaço: O (K + M), onde K é o número de depósitos predefinidos na tabela hash e M é o número de valores-chave inseridos na tabela hash.

O compartilhamento de hoje acabou, espero que seja útil para o seu estudo!

Insira a descrição da imagem aqui

Desenvolva um hábito, goste primeiro e depois observe! Seu apoio é a maior motivação para minha criação!

Acho que você gosta

Origin blog.csdn.net/wjinjie/article/details/108773366
Recomendado
Clasificación