数据结构:哈希基础、6种哈希函数构造方法、4种解决哈希冲突的方法和哈希扩展(一致性哈希+虚拟节点+布隆过滤器)
一、哈希表基础概念与原理
1.1 什么是哈希表
哈希表是一种通过哈希函数将键(key)映射到数组中特定位置进行数据存储的数据结构。理想情况下,哈希表的插入、删除和查找操作的时间复杂度都是O(1)。
核心思想:键 → 哈希函数 → 数组索引 → 直接访问
1.2 哈希表关键术语
键值对(Key-Value Pair):存储的基本单位
哈希函数(Hash Function):将任意长度输入转换为固定长度输出
哈希冲突(Hash Collision):不同键映射到相同位置
负载因子(Load Factor):元素数量/表大小,影响性能
桶(Bucket):存储键值对的单元
二、6种哈希函数构造方法
2.1 直接定址法
原理:直接使用键值或键值的线性函数作为哈希值
int direct_hash(int key, int base) { return key - base; }
应用场景:键值分布连续且范围较小
记忆技巧:"直接对应",键与位置一一对应
2.2 除留余数法(最常用)
原理:用键值除以表大小,取余数作为哈希值
int mod_hash(int key, int size) { return key % size; }
应用场景:通用场景,表大小为质数效果更好
记忆技巧:"除法取余",最简单实用的方法
2.3 数字分析法
原理:分析键值的数字分布,选取分布均匀的若干位
int digit_hash(int key) { return (key / 100) % 100; }
应用场景:键值数字分布已知且不均匀
记忆技巧:"分析数字",挑选合适的数字位
2.4 平方取中法
原理:键值平方后取中间几位作为哈希值
int mid_square_hash(int key) {long square = (long)key * key;return (square / 100) % 1000;
}
应用场景:不知道键值分布规律
记忆技巧:"平方中间",扩大差异取中间
2.5 折叠法
原理:将键值分成几部分,叠加求和后取哈希
int fold_hash(int key, int part) {int sum = 0;while(key > 0) {sum += key % part;key /= part;}return sum % part;
}
应用场景:键值位数较多
记忆技巧:"拆分叠加",分而治之的思想
2.6 随机数法
原理:使用键值作为随机数种子生成哈希值
#include <stdlib.h>
int random_hash(int key) {srand(key);return rand();
}
应用场景:对安全性有要求的场景
记忆技巧:"随机映射",利用随机性
三、4种解决哈希冲突的方法
3.1 链地址法(Separate Chaining)
原理概念
将哈希到同一位置的所有元素存储在同一个链表中,每个数组位置都是一个链表头指针。
实现过程:
计算键的哈希值得到索引
在对应链表中执行操作(插入、查找、删除)
链表节点包含键、值和下一节点指针
应用场景:元素数量不确定、频繁插入删除、负载因子可能大于1
完整代码实现
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
typedef struct Node {int key;int val;struct Node* next;
} Node;
typedef struct HashTable {Node** table;int size;
} HashTable;
HashTable* create_hash() {HashTable* ht = (HashTable*)malloc(sizeof(HashTable));ht->size = TABLE_SIZE;ht->table = (Node**)calloc(TABLE_SIZE, sizeof(Node*));return ht;
}
int hash_func(int key) {return key % TABLE_SIZE;
}
void insert_node(HashTable* ht, int key, int val) {int idx = hash_func(key);Node* new_node = (Node*)malloc(sizeof(Node));new_node->key = key;new_node->val = val;new_node->next = ht->table[idx];ht->table[idx] = new_node;printf("插入: 键=%d, 值=%d, 位置=%d\n", key, val, idx);
}
int find_node(HashTable* ht, int key) {int idx = hash_func(key);Node* curr = ht->table[idx];while(curr != NULL) {if(curr->key == key) {return curr->val;}curr = curr->next;}return -1;
}
void remove_node(HashTable* ht, int key) {int idx = hash_func(key);Node* curr = ht->table[idx];Node* prev = NULL;while(curr != NULL) {if(curr->key == key) {if(prev == NULL) {ht->table[idx] = curr->next;} else {prev->next = curr->next;}free(curr);printf("删除键: %d\n", key);return;}prev = curr;curr = curr->next;}
}
void print_table(HashTable* ht) {printf("\n哈希表内容:\n");for(int i = 0; i < ht->size; i++) {printf("位置%d: ", i);Node* curr = ht->table[i];while(curr != NULL) {printf("(%d,%d) -> ", curr->key, curr->val);curr = curr->next;}printf("NULL\n");}
}
void free_table(HashTable* ht) {for(int i = 0; i < ht->size; i++) {Node* curr = ht->table[i];while(curr != NULL) {Node* temp = curr;curr = curr->next;free(temp);}}free(ht->table);free(ht);
}
int main() {HashTable* ht = create_hash();insert_node(ht, 1, 100);insert_node(ht, 11, 200);insert_node(ht, 21, 300);insert_node(ht, 2, 400);print_table(ht);printf("查找键11: %d\n", find_node(ht, 11));remove_node(ht, 11);print_table(ht);free_table(ht);return 0;
}
3.2 开放定址法(Open Addressing)
3.2.1 线性探测法(Linear Probing)
原理:发生冲突时,顺序查找下一个空闲位置
int linear_probe(int key, int i, int size) {return (key % size + i) % size;
}
优点:实现简单,缓存友好
缺点:容易产生聚集现象
3.2.2 二次探测法(Quadratic Probing)
原理:使用二次函数作为探测步长
int quadratic_probe(int key, int i, int size) {return (key % size + i * i) % size;
}
优点:减少聚集现象
缺点:可能无法探测所有位置
3.2.3 随机探测法(Random Probing)
原理:使用随机数序列作为探测步长
#include <stdlib.h>
int random_probe(int key, int i, int size) {srand(key);return (key % size + rand() % size) % size;
}
优点:避免聚集现象
缺点:实现复杂,性能不稳定
3.3 各种冲突解决方法比较
方法 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
链地址法 | 链表连接冲突元素 | 实现简单,负载因子可>1 | 需要额外指针空间 | 通用场景,元素数量不确定 |
线性探测 | 顺序查找空位 | 缓存友好,实现简单 | 容易聚集 | 负载因子较低,元素数量固定 |
二次探测 | 平方步长查找 | 减少聚集现象 | 可能探测不全 | 对性能要求较高的场景 |
随机探测 | 随机步长查找 | 完全避免聚集 | 实现复杂,性能不稳 | 特殊需求场景 |
选择指南:
链地址法:首选,通用性强
线性探测:元素数量固定,追求缓存性能
二次探测:对聚集敏感的场景
随机探测:理论研究或特殊需求
四、哈希扩展技术
4.1 一致性哈希(Consistent Hashing)
原理概念
解决传统哈希在分布式系统中节点增减时大量数据重新映射的问题。
核心思想:将哈希空间组织成环,数据和节点都映射到环上
实现过程:
创建虚拟的哈希环(0 ~ 2^32-1)
节点哈希到环上的位置
数据键哈希到环上,顺时针找到第一个节点
#include <string.h>
unsigned int consistent_hash(const char* key, unsigned int ring_size) {unsigned int hash = 0;for(int i = 0; key[i] != '\0'; i++) {hash = (hash << 5) + hash + key[i];}return hash % ring_size;
}
应用场景:分布式缓存、负载均衡
4.2 虚拟节点(Virtual Nodes)
原理概念
在实际节点和哈希环之间增加虚拟节点层,每个实际节点对应多个虚拟节点。
解决的问题:一致性哈希中节点分布不均、负载不平衡
#define VIRTUAL_NODES_PER_SERVER 100
int get_server(const char* key, int server_count) {unsigned int min_hash = -1;int target_server = -1;for(int i = 0; i < server_count; i++) {for(int j = 0; j < VIRTUAL_NODES_PER_SERVER; j++) {char vnode[50];sprintf(vnode, "server%d_vnode%d", i, j);unsigned int hash = consistent_hash(vnode, 1 << 32);if(hash < min_hash) {min_hash = hash;target_server = i;}}}return target_server;
}
优势:负载更均衡,节点增减影响更小
4.3 布隆过滤器(Bloom Filter)
原理概念
概率型数据结构,用于快速判断元素是否在集合中,可能有误判但不会漏判。
核心思想:使用k个哈希函数和位数组,插入时设置多个位,查询时检查所有位
#include <stdbool.h>
#define FILTER_SIZE 1000
#define HASH_COUNT 3
typedef struct BloomFilter {bool* bits;int size;
} BloomFilter;
BloomFilter* create_bloom() {BloomFilter* bf = (BloomFilter*)malloc(sizeof(BloomFilter));bf->size = FILTER_SIZE;bf->bits = (bool*)calloc(FILTER_SIZE, sizeof(bool));return bf;
}
unsigned int hash1(const char* str) {unsigned int hash = 5381;int c;while((c = *str++)) hash = ((hash << 5) + hash) + c;return hash % FILTER_SIZE;
}
unsigned int hash2(const char* str) {unsigned int hash = 0;int c;while((c = *str++)) hash = c + (hash << 6) + (hash << 16) - hash;return hash % FILTER_SIZE;
}
unsigned int hash3(const char* str) {unsigned int hash = 0;int c;while((c = *str++)) hash = (hash * 131) + c;return hash % FILTER_SIZE;
}
void bloom_insert(BloomFilter* bf, const char* item) {bf->bits[hash1(item)] = true;bf->bits[hash2(item)] = true;bf->bits[hash3(item)] = true;
}
bool bloom_check(BloomFilter* bf, const char* item) {return bf->bits[hash1(item)] && bf->bits[hash2(item)] && bf->bits[hash3(item)];
}
void free_bloom(BloomFilter* bf) {free(bf->bits);free(bf);
}
应用场景:缓存击穿防护、爬虫URL去重、垃圾邮件过滤
特点:
空间效率极高 查询时间恒定
可能假阳性,不会假阴性 不支持删除操作(可使用Counting Bloom Filter)
五、哈希技术综合比较与应用选择
5.1 各种哈希技术对比总结
技术 | 核心思想 | 空间复杂度 | 时间复杂度 | 主要优势 | 典型应用 |
---|---|---|---|---|---|
传统哈希表 | 键值映射到数组 | O(n) | O(1)平均 | 快速访问 | 字典、缓存 |
一致性哈希 | 哈希环映射 | O(n) | O(log n) | 节点增减影响小 | 分布式系统 |
虚拟节点 | 多对一映射 | O(kn) | O(k log n) | 负载均衡 | 负载均衡器 |
布隆过滤器 | 多哈希位图 | O(m) | O(k) | 空间效率高 | 存在性检测 |
5.2 应用场景选择指南
单机键值存储:链地址法哈希表
分布式缓存:一致性哈希 + 虚拟节点
存在性检查:布隆过滤器
负载均衡:一致性哈希
数据分片:范围哈希或一致性哈希
六、哈希相关常见面试题
6.1 基础概念题
1. 什么是哈希冲突?如何解决?
哈希冲突:不同键映射到相同位置
解决方法:链地址法、开放定址法、再哈希法等
2. 负载因子是什么?为什么重要?
负载因子 = 元素数量 / 表大小
重要性:影响哈希表性能,通常保持在0.7-0.8
3. 为什么哈希表大小通常取质数?
减少哈希冲突,使键值分布更均匀
6.2 代码实现题
1. 实现LRU缓存(使用哈希表+双向链表)
typedef struct LRUNode {int key;int value;struct LRUNode* prev;struct LRUNode* next;
} LRUNode;
typedef struct {int capacity;int size;LRUNode* head;LRUNode* tail;LRUNode** hash;
} LRUCache;
2. 找出数组中两数之和等于目标值
int* twoSum(int* nums, int numsSize, int target) {int* result = (int*)malloc(2 * sizeof(int));int hash[100000] = {0};for(int i = 0; i < numsSize; i++) {int complement = target - nums[i];if(hash[complement + 50000] != 0) {result[0] = hash[complement + 50000] - 1;result[1] = i;return result;}hash[nums[i] + 50000] = i + 1;}return result;
}
6.3 系统设计题
1. 如何设计分布式缓存系统?
使用一致性哈希进行数据分片 添加虚拟节点保证负载均衡
设置副本机制保证高可用 使用布隆过滤器减少缓存穿透
2. 如何检测海量数据中的重复URL?
使用布隆过滤器进行初步过滤
对可能重复的数据进行精确比对
采用分治策略处理海量数据
6.4 性能优化题
1. 哈希表在什么情况下会退化?如何避免?
退化情况:哈希函数差、负载因子过高、大量冲突
避免方法:选择好的哈希函数、动态扩容、监控负载因子
2. 如何选择哈希函数?
考虑因素:键的类型、分布特征、性能要求
通用选择:除留余数法(表大小为质数)
字符串:BKDR、DJB2等经典字符串哈希
通过系统学习哈希技术的原理、实现和应用,结合实际的代码练习和面试题准备,希望大家能够全面掌握这一重要的数据结构,在工程实践中灵活运用。