-
HashTable
HashTable
(散列表,又称哈希表),是根据关键码值(key, value
)来进行快速访问的一种数据结构。它通过把关键码值(key
)映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(哈希函数),存放记录的数组叫做散列表。[摘自百度百科]更通俗来讲,哈希表就是我们预先开辟的一个数组空间,它存储了我们的关键码值,但是与数组不同的是,哈希表的下标
index
是由键key
值通过哈希函数计算得到的。由此也可以看出不同的关键码可能对应相同的位置,这就是哈希碰撞。 -
哈希碰撞:不同的关键码对应相同的位置,常见解决方法有散列法和拉链法
- 散列法:又分开放寻址法和再散列法(按照不同的哈希函数再次计算哈希值)
- 拉链法:在产生哈希碰撞的位置建立链表,所有的冲突值都链入链表(常用方法,后文示例也是基于拉链法)
哈希碰撞主要与负载因子和哈希函数有关,良好的哈希函数能够有效地降低哈希碰撞并提升插入和查找数据的性能。
-
负载因子:哈希表中已有元素个数与哈希表长度的比值。基于性能和空间的选择,在
JDK
标准库中将负载因子阈值定为0.75
-
哈希函数:关键码到哈希表的映射关系函数。哈希函数是非常重要的,对于哈希函数的设计需要考虑一下两点:
1.尽可能降低哈希碰撞,计算出的哈希值越分散越好;
2.因为是高频操作,所以算法要高效常见的哈希函数有:
1.直接定值法:Hash(key) = a·key + b
该方法不会产生哈希碰撞,但是空间复杂度较高,这里不再做具体介绍。
2. 除法散列法:Hash(key) = key % base
base
代表哈希表的长度。这是一种最为常用的哈希函数。对于哈希表长度我们经常取2的幂次方,因为key % 2^n = key & (2^n - 1)
,而&
计算相比取模操作要快很多。如果要考虑key
是负数的情况,则可以采用如下哈希函数:Hash(key) = (key & 0x7fffffff) & (base - 1)
3.斐波那契散列法:
Hash(key) = (key * base) >> 28
对于16位整数base取40503;对于32位整数base取2654435769;对于64位整数,base取11400714819323198485
-
扩容:当哈希表的元素数量逐渐增加,如果不对增加哈希表长度,哈希表将退化为链表,此时哈希表的性能将会降低。本文主要介绍基于除法散列法的扩容策略:
1.在元素个数 >= 哈希长度 * 负载因子时对哈希表进行扩容,扩容后的长度为上次的2倍
2.扩容后需要对已存储数据重新计算哈希值,并放入新的哈希表中 -
一个哈希表应有以下几个接口:
put(key, value)
:新增数据,如果哈希表中已有key
存在,则覆盖原来的数据
get(key)
:查找数据
remove(key)
:删除数据
free()
:释放内存
为了更好的展示哈希表设计原理,本文将以C
语言来完成设计。
// 定义链表,存放key value
typedef struct ListNode_ {
int key;
int value;
struct ListNode_* next;
} __ListNode;
void ListPush(__ListNode* head, int key, int value) {
__ListNode* node = (__ListNode*)malloc(sizeof(__ListNode));
node->key = key;
node->value = value;
node->next