哈希表入门教程:从零搭建完整结构
一、什么是哈希表?
1.核心定义
哈希表 = 数组 + 哈希函数 + 冲突解决
哈希表是一种通过哈希函数将「键(Key)」映射到「索引(Index)」,从而实现O (1) 平均时间复杂度查找、插入、删除的数据结构。
2.核心三要素
(1)键(key) :要存储的数据标识(如数字、字符串、对象)。
(2)哈希函数:把 Key 转换成数组下标的函数(核心!)
(3)哈希表底层:数组(利用数组随机访问特性)
3.核心优势
(1)平均:查找 / 插入 / 删除 = O(1)
(2)最坏:O (n)
(3)对比:数组查找 O (n),链表查找 O (n),二叉搜索树查找 O (logn),哈希表是最快的查找数据结构!
二、哈希函数和哈希冲突
1.哈希函数(Hash Function)
作用:key -> 数组索引
公式:index = hash(key)% 数组长度
2.哈希函数(高频坑点)
(1)不处理负数key->下标越界崩溃
// ❌ 错误哈希函数
int bad_hash1(HashTable* ht, int key) {
return key % ht->size; // 负数key → 负数下标!
}
错误原因:key = -1 % 10 = -1 → 数组下标不能为负,程序直接崩溃。
正确代码:
// ✅ 正确哈希函数
int good_hash(HashTable* ht, int key) {
if (key < 0) key = -key; // 必须先转正数
return key % ht->size;
}
(2)哈希函数分布不均 -> 冲突爆炸
// ❌ 错误哈希函数:所有key都返回同一个下标
int bad_hash2(HashTable* ht, int key) {
return 0; // 所有元素都存在下标0 → 链表变成长链,O(n)复杂度
}
错误原因:冲突严重,哈希表退化为链表。
正确代码:
// 正确哈希函数:保证分布均匀 + 处理负数
int hash(HashTable* ht, int key) {
// 1. 处理负数key,避免负数下标越界
if (key < 0) {
key = -key;
}
// 2. 取余运算:将key映射到[0, size-1]的合法下标
// 配合质数大小的哈希表,能最大程度保证分布均匀
return key % ht->size;
}
(3)哈希表大小用偶数 -> 冲突增多
// ❌ 不推荐
HashTable* ht = createHashTable(10); // 偶数,取余分布不均
// ✅ 推荐:质数大小
HashTable* ht = createHashTable(11); // 质数,分布均匀,冲突最少
3.哈希冲突
定义:两个不同的 Key,通过哈希函数算出了同一个索引。无法完全避免,只能解决冲突。
4.哈希冲突的解决办法
(1)链地址法(链表+数组)
数组每个位置存一个链表 / 红黑树
冲突时,把元素追加到链表后面
优点:实现简单,无空间浪费
(2)开放寻址法(纯数组)
冲突时,向后找空位置存放(线性探测 / 二次探测)
优点:无链表,内存连续
缺点:删除麻烦,容易堆积
三、哈希表基本操作总结
1.链表节点
typedef struct Node{
int key; //键
int value; //值
struct Node* next; //用于解决哈希冲突
}Node;
2.哈希表结构
typedef struct HashTable{
int size; //数组长度
Node** table; //指针数组(存链表头)
}HashTable;
3.哈希函数(核心公式)
正确哈希函数:abs(key) % size
作用:把任意整数 → 合法下标 [0, size-1]
必须处理负数!否则下标越界崩溃
int hash(HashTable* ht, int key) {
if (key < 0) key = -key; // 负数转正数
return key % ht->size; // 取余映射下标
}
4.创建哈希表
HashTable* createHashTable(int size) {
HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
ht->size = size;
ht->table = (Node**)malloc(sizeof(Node*) * size);
// 初始化所有链表为空
for (int i = 0; i < size; i++) {
ht->table[i] = NULL;
}
return ht;
}
5.创建节点
Node* createNode(int key, int value) {
Node* node = (Node*)malloc(sizeof(Node));
node->key = key;
node->value = value;
node->next = NULL;
return node;
}
6.插入(冲突处理)
void insert(HashTable* ht, int key, int value) {
int index = hash(ht, key); // 计算下标
Node* head = ht->table[index];
// 遍历:key已存在则覆盖
Node* cur = head;
while (cur) {
if (cur->key == key) {
cur->value = value;
return;
}
cur = cur->next;
}
// 头插法:新节点放链表头部
Node* newNode = createNode(key, value);
newNode->next = head;
ht->table[index] = newNode;
}
7.查找
int search(HashTable* ht, int key) {
int index = hash(ht, key);
Node* cur = ht->table[index];
// 遍历链表查找
while (cur) {
if (cur->key == key) return cur->value;
cur = cur->next;
}
return -1; // 未找到
}
8.删除
void deleteKey(HashTable* ht, int key) {
int index = hash(ht, key);
Node* prev = NULL;
Node* cur = ht->table[index];
// 查找节点
while (cur && cur->key != key) {
prev = cur;
cur = cur->next;
}
if (!cur) return; // 不存在直接返回
// 删除节点
if (!prev) ht->table[index] = cur->next; // 删头节点
else prev->next = cur->next; // 删中间节点
free(cur);
}
9.销毁(防止内存泄露)
void destroyHashTable(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
Node* cur = ht->table[i];
while (cur) {
Node* temp = cur;
cur = cur->next;
free(temp);
}
}
free(ht->table);
free(ht);
}
10.一般经常会使用到的方法
int hash[N]={0} 来进行下标映射。
四、哈希表算法分类
哈希表的核心思想只有一个:把 key 转成下标,然后快速存、快速查。
数组哈希:做题比较常用,它把 “链表” 扔掉,直接用一个数组当下标映射,思想还是哈希表:key → 下标 → 存值。
1.两数之和(查找配对)
// 两数之和(数组哈希版,适合数据范围小的场景)
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
*returnSize = 2;
int* res = (int*)malloc(2 * sizeof(int));
// 数组哈希:key=数值,value=下标(偏移量处理负数)
int hash[200001];
memset(hash, -1, sizeof(hash)); // 初始化为-1(表示未访问)
for (int i = 0; i < numsSize; i++) {
int complement = target - nums[i];
int idx = complement + 100000;
// 找到配对的数,返回下标
if (hash[idx] != -1) {
res[0] = hash[idx];
res[1] = i;
return res;
}
// 把当前数存入哈希
int cur_idx = nums[i] + 100000;
hash[cur_idx] = i;
}
return res;
}
一次遍历,哈希表 O (1) 查找,时间 O (n)。
2.频率统计(计数)
// 小写字母统计(26大小)
int hash[26] = {0};
for (int i = 0; i < strlen(s); i++) {
hash[s[i] - 'a']++; // 字符转下标,计数+1
}
// 全ASCII字符统计(128大小,万能)
int hash[128] = {0};
for (int i = 0; i < strlen(s); i++) {
hash[(unsigned char)s[i]]++;
}
3.去重
// 数字去重(带偏移量处理负数)
int hash[200001] = {0}; // 覆盖-100000~100000
for (int i = 0; i < numsSize; i++) {
int idx = nums[i] + 100000; // 负数偏移转正数下标
if (hash[idx]) return true; // 已存在→有重复
hash[idx] = 1; // 标记已访问
}
return false;
4.键值映射
bool isIsomorphic(char* s, char* t) {
int map_s[128], map_t[128];
memset(map_s, -1, sizeof(map_s));
memset(map_t, -1, sizeof(map_t));
int len = strlen(s);
for (int i = 0; i < len; i++) {
unsigned char c1 = s[i], c2 = t[i];
// 双向映射不一致,返回false
if (map_s[c1] != map_t[c2]) return false;
// 建立双向映射
map_s[c1] = i;
map_t[c2] = i;
}
return true;
}
5.前缀和+哈希表
int subarraySum(int* nums, int numsSize, int k) {
// 数组哈希:记录前缀和出现的次数(偏移量处理负数)
int hash[200001] = {0};
hash[100000] = 1; // 初始前缀和为0,出现1次
int preSum = 0, count = 0;
for (int i = 0; i < numsSize; i++) {
preSum += nums[i];
// 查找preSum - k出现的次数
int target = preSum - k;
int idx = target + 100000;
if (idx >= 0 && idx < 200001) {
count += hash[idx];
}
// 更新当前前缀和的次数
int cur_idx = preSum + 100000;
hash[cur_idx]++;
}
return count;
}
6.滑动窗口+去重
int lengthOfLongestSubstring(char* s) {
int hash[128]; // 记录字符最后出现的位置
memset(hash, -1, sizeof(hash));
int left = 0, maxLen = 0;
int len = strlen(s);
for (int right = 0; right < len; right++) {
unsigned char c = s[right];
// 字符已在窗口内,移动左指针到重复位置的下一位
if (hash[c] >= left) {
left = hash[c] + 1;
}
// 更新字符的最新位置
hash[c] = right;
maxLen = (right - left + 1) > maxLen ? (right - left + 1) : maxLen;
}
return maxLen;
}
五、高频出错点
1.C 语言必须手动 free → 内存泄漏
先 free 链表 → 再 free 数组 → 最后 free 哈希表
2.直接访问数组下标 → 空指针崩溃
ht->table[index]->key:错误
必须用search()函数安全查找:正确
3.插入重复 key 不覆盖 → 数据错误
插入前必须遍历检查,相同 key 覆盖 value
4.认为哈希表永远 O (1) → 认知错误
哈希冲突严重时 = O (n)
更多推荐


所有评论(0)