CodeGym /Cursos /Python SELF PT /Problemas de colisão e métodos para resolvê-los

Problemas de colisão e métodos para resolvê-los

Python SELF PT
Nível 54 , Lição 2
Disponível

7.1 Definição de colisões em tabela hash

Colisão em uma tabela hash acontece quando duas chaves diferentes são hashadas para o mesmo índice de array. Isso resulta em mais de um elemento tentando ocupar a mesma célula na tabela hash.

Exemplo de colisão:

Suponha que a gente tenha uma função de hash simples que retorna o resto da divisão da chave pelo tamanho da tabela. Para as chaves 5 e 15 com tamanho de tabela 10, ambas as chaves darão o mesmo índice: 5 % 10 = 5 e 15 % 10 = 5. Isso é uma colisão.

Outro exemplo:

Imagina que você está criando um dicionário de nomes, e você tem uma função de hash simples que retorna a primeira letra do nome. Mas descobre-se que 80% dos nomes começam com a letra "A". Não é só uma colisão, é um problemão.

7.2 Métodos de resolução de colisões – Encadeamento

Existem vários métodos para resolver colisões em tabelas hash. Os dois métodos mais comuns são encadeamento (chaining) e endereçamento aberto (open addressing).

Encadeamento (Chaining)

Vantagens:

  • Simplicidade na implementação.
  • Eficiente quando há poucas colisões.
  • Menor dependência da densidade de preenchimento da tabela hash.

Desvantagens:

  • Requer memória adicional para ponteiros na lista encadeada.
  • O desempenho pode cair com um grande número de colisões devido a cadeias longas.

Exemplo de implementação do método de encadeamento:

Você pode apenas estudar como a função insert trabalha, mas eu decidi mostrar o código completo para facilitar.


class HashTableChaining:
    def __init__(self, size):
        self.size = size
        self.table = [None] * size

    def hash_function(self, key):
        return hash(key) % self.size

    def insert(self, key, value):
        index = self.hash_function(key)
        if self.table[index] is None:
            self.table[index] = [(key, value)]
        else:
            for i, kv in enumerate(self.table[index]):
                k, v = kv
                if k == key:
                    self.table[index][i] = (key, value)
                    return
            self.table[index].append((key, value))

    def search(self, key):
        index = self.hash_function(key)
        if self.table[index] is None:
            return None
        for k, v in self.table[index]:
            if k == key:
                return v
        return None

    def delete(self, key):
        index = self.hash_function(key)
        if self.table[index] is None:
            return
        for i, kv in enumerate(self.table[index]):
            k, v = kv
            if k == key:
                del self.table[index][i]
                return

# Exemplo de uso:
hash_table = HashTableChaining(10)
hash_table.insert("apple", 1)
hash_table.insert("banana", 2)
hash_table.insert("cherry", 3)
print(hash_table.search("banana"))  # Saída: 2
hash_table.delete("banana")
print(hash_table.search("banana"))  # Saída: None

7.3 Método número dois – endereçamento aberto

O método de endereçamento aberto envolve a busca da próxima célula disponível no array quando ocorre uma colisão. Existem diferentes estratégias para encontrar uma nova célula: sondagem linear, sondagem quadrática e duplo hash.

Vantagens:

  • Não requer memória adicional para armazenar ponteiros.
  • Representação de dados mais compacta.

Desvantagens:

  • O desempenho pode cair significativamente com alta densidade de preenchimento da tabela.
  • Requer mais recursos computacionais para resolver colisões.

Exemplos de estratégias de endereçamento aberto

Sondagem linear (Linear Probing):

  • Sondagem com passo 1: index = (index + 1) % size.
  • Simples, mas pode levar à "clusterização".

Sondagem quadrática (Quadratic Probing):

  • Sondagem com passo aumentando ao quadrado: index = (index + i^2) % size.
  • Reduz o problema de clusterização, mas é mais difícil de implementar.

Duplo hash (Double Hashing):

  • Uso de uma segunda função de hash para calcular o passo: index = (index + i * hash2(key)) % size.
  • Menos problemas de clusterização, mas mais complexo na implementação.

Exemplo de implementação do método de endereçamento aberto (sondagem linear): Vou mostrar o código completo, mas para facilidade, você pode apenas examinar o funcionamento da função insert


class HashTableOpenAddressing:
    def __init__(self, size):
        self.size = size
        self.table = [None] * size

    def hash_function(self, key):
        return hash(key) % self.size

    def insert(self, key, value):
        index = self.hash_function(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index][0] == key:
                self.table[index] = (key, value)
                return
            index = (index + 1) % self.size
            if index == original_index:
                raise Exception("Hash table is full")
        self.table[index] = (key, value)

    def search(self, key):
        index = self.hash_function(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index][0] == key:
                return self.table[index][1]
            index = (index + 1) % self.size
            if index == original_index:
                break
        return None

    def delete(self, key):
        index = self.hash_function(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index][0] == key:
                self.table[index] = None
                return
            index = (index + 1) % self.size
            if index == original_index:
                break

# Exemplo de uso:
hash_table = HashTableOpenAddressing(10)
hash_table.insert("apple", 1)
hash_table.insert("banana", 2)
hash_table.insert("cherry", 3)
print(hash_table.search("banana"))  # Saída: 2
hash_table.delete("banana")
print(hash_table.search("banana"))  # Saída: None

7.4 Impacto das colisões na tabela hash

Impacto das colisões no desempenho.

Tempo médio de acesso:

  • Em condições ideais (sem colisões) o tempo médio de acesso aos elementos na tabela hash é O(1).
  • Colisões aumentam o tempo de acesso, pois é necessário lidar com cadeias ou sondagem de células, aumentando o número de operações.

Aumento das cadeias:

  • No método de encadeamento, o aumento das cadeias com muitas colisões pode levar a um tempo de acesso linear O(n) para operações de busca, inserção e exclusão.
  • Cadeias longas aumentam o número de comparações necessárias para encontrar ou inserir um elemento.

Complexidade espacial:

  • No método de encadeamento, é necessária memória adicional para armazenar ponteiros nas listas ligadas.
  • No método de endereçamento aberto, o preenchimento da tabela acima de certo nível (geralmente cerca de 70-80%) aumenta significativamente a quantidade de colisões e, consequentemente, o tempo de acesso.

Clusterização: No método de endereçamento aberto (especialmente na sondagem linear), ocorre clusterização quando várias células consecutivas ficam ocupadas, aumentando a probabilidade de colisões e piorando o desempenho.

Exemplo de análise do impacto das colisões no desempenho:

Isso é para os curiosos que querem se aprofundar


import time
import random

# Sondagem linear
class HashTableOpenAddressing:
    def __init__(self, size):
        self.size = size
        self.table = [None] * size

    def hash_function(self, key):
        return hash(key) % self.size

    def insert(self, key, value):
        index = self.hash_function(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index][0] == key:
                self.table[index] = (key, value)
                return
            index = (index + 1) % self.size
            if index == original_index:
                raise Exception("Hash table is full")
        self.table[index] = (key, value)

    def search(self, key):
        index = self.hash_function(key)
        original_index = index
        while self.table[index] is not None:
            if self.table[index][0] == key:
                return self.table[index][1]
            index = (index + 1) % self.size
            if index == original_index:
                break
        return None

# Gerando uma grande quantidade de dados para inserção
hash_table = HashTableOpenAddressing(1000)
keys = [random.randint(1, 10000) for _ in range(800)]

# Inserindo dados e medindo o tempo
start_time = time.time()
for key in keys:
    hash_table.insert(key, key)
print(f"Tempo de inserção: {time.time() - start_time:.6f} segundos")

# Buscando dados e medindo o tempo
start_time = time.time()
for key in keys:
    hash_table.search(key)
print(f"Tempo de busca: {time.time() - start_time:.6f} segundos")

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION