Fundamentos de Tabelas Hash
Tabelas hash são estruturas de dados primariamente utilizadas para verificar rapidamente a existência de um elemento em uma coleção. O princípio envolve uma função hash que mapeia um dado (como um nome de aluno) a um índice em uma tabela. Consultar esse índice permite determinar de forma ágil se o dado está presente.
Ocorre colisão hash quando diferentes dados são mapeados para o mesmo índice. As abordagens comuns para resolver colisões incluem o método de encadeamento (chaining) e o método de sondagem linear (linear probing).
- Encadeamento: Elementos que colidem são armazenados em uma lista ligada associada ao índice.
- Sondagem Linear: Garante que o tamanho da tabela seja maior que a quantidade de dados inseridos para minimizar colisões e garantir a inserção.
Estruturas hash comuns incluem arrays, conjuntos (sets) e mapas (maps).
| Estrutura | Implementação Subjacente | Ordenado | Valores Duplicados | Modificável | Eficiência de Consulta | Eficiência de Inserção/Remoção |
|---|---|---|---|---|---|---|
std::set |
Árvore Rubro-Negra | Sim | Não | Não (Chave) | O(log n) | O(log n) |
std::multiset |
Árvore Rubro-Negra | Sim | Sim | Não (Chave) | O(log n) | O(log n) |
std::unordered_set |
Tabela Hash | Não | Não | Não (Chave) | O(1) em média | O(1) em média |
Árvores rubro-negras mantêm os elementos ordenados, mas chaves não podem ser modificadas diretamente para preservar a estrutura da árvore; operações de modificação envolvem remoção e inserção.
| Estrutura | Implementação Subjacente | Ordenado | Chaves Duplicadas | Modificável (Chave) | Eficiência de Consulta | Eficiência de Inserção/Remoção |
|---|---|---|---|---|---|---|
std::map |
Árvore Rubro-Negra | Por chave | Não | Não | O(log n) | O(log n) |
std::multimap |
Árvore Rubro-Negra | Por chave | Sim | Não | O(log n) | O(log n) |
std::unordered_map |
Tabela Hash | Não | Não | Não | O(1) em média | O(1) em média |
Para solucionar problemas que exigem a verificação rápida de existência em uma coleção, prefira std::unordered_set devido à sua eficiência O(1). Se a ordenação for um requisito, utilize std::set ou std::multiset.
Mapas (std::map, std::unordered_map) são estruturas chave-valor. Enquanto as chaves têm restrições (únicas, não modificáveis diretamente), os valores são flexíveis. A eficiência de std::unordered_map é ideal para buscas rápidas.
Em resumo, a abordagem de hash é indicada quando a necessidade é verificar rapidamente a presença de um elemento. Ela opera sob o princípio de troca de espaço por tempo, utilizando estruturas auxiliares como arrays, sets ou maps para obter agilidade nas consultas.
Exemplos de Aplicação
1. Anagrama Válido
Para determinar se duas strings são anagramas, podemos usar um array como tabela hash. Assumindo que as strings contenham apenas letras minúsculas do alfabeto inglês, um array de tamanho 26 é suficiente. Cada índice do array corresponderá a uma letra (por exemplo, 'a' mapeia para o índice 0, 'b' para 1, etc.).
Percorrremos a primeira string s, incrementando a contagem no índice correspondente à cada caractere. Em seguida, percorremos a segunda string t, decrementando a contagem para cada caractere. Se, ao final, todos os elementos do array forem zero, as strings são anagramas. Qualquer valor diferente de zero indica uma discrepância na contagem de caracteres.
A complexidade de tempo é O(n), onde n é o comprimento das strings. A complexidade de espaço é O(1), pois o array auxiliar tem tamanho constante.
class Solution {
public:
bool isAnagram(string s, string t) {
// Array para contar a frequência de cada caractere ('a' a 'z')
int char_counts[26] = {0};
// Incrementa a contagem para caracteres em s
for (char c : s) {
char_counts[c - 'a']++;
}
// Decrementa a contagem para caracteres em t
for (char c : t) {
char_counts[c - 'a']--;
}
// Verifica se todas as contagens são zero
for (int count : char_counts) {
if (count != 0) {
return false; // Se alguma contagem for diferente de zero, não são anagramas
}
}
return true; // Todas as contagens são zero, são anagramas
}
};
2. Interseção de Dois Arrays
Este problema requer a identificação de elementos comuns entre dois arrays, com a particularidade de que o resultado deve conter apenas elementos únicos. A limitação de valores pode influenciar a escolha entre usar um array como hash ou uma estrutura mais flexível como std::unordered_set.
Quando os valores não são restritos ou são muito dispersos, std::unordered_set é preferível para evitar desperdício de espaço. A necessidade de remover duplicatas no resultado final reforça a escolha por um conjunto.
O algoritmo consiste em:
- Popular um
std::unordered_setcom os elementos do primeiro array (nums1) para permitir consultas rápidas e deduplicação automática. - Iterar sobre o segundo array (
nums2). Para cada elemento, verificar se ele existe no conjunto criado a partir denums1. - Se um elemento de
nums2for encontrado no conjunto, adicioná-lo a um segundo conjunto (result_set) para garantir a unicidade do resultado. - Converter o
result_setfinal em umstd::vectorpara retornar.
#include <vector>
#include <unordered_set>
class Solution {
public:
std::vector<int> intersection(std::vector<int>& nums1, std::vector<int>& nums2) {
// Conjunto para armazenar os elementos únicos da interseção
std::unordered_set<int> result_set;
// Conjunto para armazenar os elementos únicos de nums1 para busca rápida
std::unordered_set<int> nums1_set(nums1.begin(), nums1.end());
// Itera sobre os elementos de nums2
for (int num : nums2) {
// Se o elemento de nums2 existe em nums1_set
if (nums1_set.count(num)) { // Usando count() para verificar existência
result_set.insert(num); // Adiciona ao conjunto de resultados (deduplica automaticamente)
}
}
// Converte o conjunto de resultados para um vetor e retorna
return std::vector<int>(result_set.begin(), result_set.end());
}
};
3. Número Feliz
A característica chave deste problema é a detecção de ciclos. Se, ao calcular repetidamente a soma dos quadrados dos dígitos de um número, encontramos um valor que já foi visto anteriormente (e esse valor não é 1), significa que entramos em um ciclo e o número não é feliz. Novamente, a verificação de elementos já vistos sugere o uso de uma estrutura hash, como std::unordered_set.
O processo envolve:
- Manter um conjunto (
seen_sums) para registrar as somas intermediárias calculadas. - Em um loop, calcular a soma dos quadrados dos dígitos do número atual
n. - Se a soma for 1, o número é feliz e retornamos
true. - Se a soma já estiver presente em
seen_sums, detectamos um ciclo e retornamosfalse. - Caso contrário, adicionamos a soma a
seen_sums, atualizamosnpara essa soma e continuamos o loop.
É crucial ter uma função auxiliar (get_digit_square_sum) para calcular a soma dos quadrados dos dígitos.
#include <unordered_set>
class Solution {
private:
// Função auxiliar para calcular a soma dos quadrados dos dígitos
int get_digit_square_sum(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
public:
bool isHappy(int n) {
// Conjunto para rastrear as somas já calculadas
std::unordered_set<int> seen_sums;
// Loop continua até que o número seja 1 ou um ciclo seja detectado
while (n != 1 && seen_sums.find(n) == seen_sums.end()) {
seen_sums.insert(n); // Adiciona a soma atual ao conjunto de vistos
n = get_digit_square_sum(n); // Calcula a próxima soma
}
// Se n for 1, é um número feliz; caso contrário, um ciclo foi encontrado
return n == 1;
}
};
4. Soma de Dois Números (Two Sum)
Este problema clássico pede para encontrar dois números em um array que somados resultem em um valor alvo. Para resolver eficientemente, precisamos não apenas saber se um número complementar existe, mas também qual o seu índice. Isso exige uma estrutura que armazene pares chave-valor, como std::unordered_map.
O mapa armazenará o número como chave e seu índice como valor. Ao iterar pelo array:
- Para cada número
nums[i], calculamos o complemento necessário:complement = target - nums[i]. - Verificamos se o
complementjá existe como chave no mapa. - Se o
complementto encontrado, significa que encontramos o par. Retornamos o índice docomplement(armazenado no mapa) e o índice atuali. - Se o
complementnão for encontrado, inserimos o número atualnums[i]e seu índiceino mapa para futuras consultas.
O uso de auto para o tipo do iterador simplifica a declaração e adapta-se a diferentes tipos de mapas.
#include <vector>
#include <unordered_map>
#include <utility> // Para std::pair
class Solution {
public:
std::vector<int> twoSum(std::vector<int>& nums, int target) {
// Mapa para armazenar o número e seu índice
// Chave: número, Valor: índice
std::unordered_map<int, int> num_to_index;
// Itera sobre o array nums
for (int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
// Tenta encontrar o complemento no mapa
auto it = num_to_index.find(complement);
// Se o complemento foi encontrado no mapa
if (it != num_to_index.end()) {
// Retorna os índices do complemento e do número atual
return {it->second, i};
}
// Se o complemento não foi encontrado, insere o número atual e seu índice no mapa
// Usamos insert para evitar sobrescrever índices se houver duplicatas e
// garantir que estamos sempre pegando o índice mais antigo, se necessário
// No entanto, para Two Sum padrão, apenas adicionar o par é suficiente.
num_to_index[nums[i]] = i; // Forma mais concisa de inserir/atualizar
// Alternativa com insert: num_to_index.insert({nums[i], i});
}
// Se nenhum par for encontrado (o que não deve acontecer se houver solução garantida)
return {};
}
};
APIs comuns de std::unordered_map incluem:
operator[]: Acessa ou insere um elemento.erase(key): Remove um elemento pela chave.size(): Retorna o número de elementos.clear(): Remove todos os elementos.begin(),end(): Iteradores para percorrer o mapa.