哈希表又称散列表,存储键值对,主要操作有插入,删除,查找。哈希表底层是一个数组,首先将key通过哈希函数计算哈希值,也即数组下标,然后对位置上的元素执行插入删除,查询。三种操作时间复杂度均为 O ( 1 ) O(1) O(1),耗时操作在哈希函数计算和哈希冲突的处理上。
哈希函数(*)
哈希函数的要求是,映射均匀,降低哈希冲突的几率;计算简单,节省时间。
- 直接定址法
使用key的线性函数作为哈希函数,H(key) = a*key + b,其中a和b为常数。 - 除留取余法
key值对不大于数组长度m的数值p取余。H(key) = key % p, (p<=m)。 - 数字分析
当关键字的位数大于数组下标的位数,可以取key值中分布比较均匀的几位作为哈希值。如学号201714378,前四位是入学时间,后四位(编号)可以做哈希值。 - 平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
哈希冲突
在大量关键字中,哈希函数难免对不同的关键字产生相同的哈地址,也即哈希冲突。
- 拉链法
把地址冲突的键值对用链表存储起来,链表节点存储关键字和值。查找时首先定位到数组位置,然后把待查询的key和链表节点一次比较,如果找到就返回值,如果遍历到链表结束就表示关键字不再数组中。当冲突非常严重,所有元素存在同一条链表时,查找的时间复杂度达到 O ( n ) O(n) O(n),当然也可以提高查询效率用红黑树代替链表存储。插入和删除也要首先做查找操作,不再赘述。 - 开放寻址法
2.1 线性探测法
插入操作:如果出现冲突,就从数组当前位置向后寻找,遇到第一个空闲位置就把键值对存储在该处;如果没有空闲位置继续从数组开始处寻找。
查询操作:从冲突的位置向后寻找,将待查关键字与数组元素依次比较,相等就返回,如果找到空闲位置还没匹配,就表示关键字不再数组中。
删除操作:找到键值对存放位置后,删除元素,并把当前位标记为detected而不能变成空闲位,否则会影响其他元素的查找。(其他元素本来放在当前元素后面,现在由于删除操作在当前位置产生空闲位置,使其他元素的查找在当前位置终止而产生误判)。查找的话遇到detect继续查找。
极端情况下,需要探测整个表,最坏时间复杂度 O ( n ) O(n) O(n)
2.2 二次探测法
探测的步长以
d
2
,
d
=
1
,
2
,
3...
d^2,d=1,2,3...
d2,d=1,2,3...递增,探测位置为
a
r
r
[
k
+
1
2
]
,
a
r
r
[
k
+
2
2
]
.
.
.
arr[k+1^2],arr[k+2^2]...
arr[k+12],arr[k+22]...。缺点:数组容量为偶数时无法探测整个数组空间。
2.3 双重散列
出现冲突后,再使用一个哈希函数计算哈希地址,并加在当前位置上成为新的哈希地址。
d
=
(
h
a
s
h
1
(
k
e
y
)
+
i
∗
h
a
s
h
2
(
k
e
y
)
)
%
m
d=(hash1(key) + i*hash2(key))\%m
d=(hash1(key)+i∗hash2(key))%m
拉链法和开放寻址法比较
- 拉链法冲突代价更低。冲突后开放寻址要遍历整个数组,而拉链法只遍历当前位置上的链表,复杂度要小很多。
- 拉链法内存利用率更高。拉链法的负载因子可以大于1,因为开放寻址的负载因子在接近1时产生大量冲突,所以上限不能太大(必须<=1),造成数组很多空闲位置。
负载因子:又称装载因子, 哈 希 表 中 元 素 总 数 / 数 组 大 小 哈希表中元素总数/数组大小 哈希表中元素总数/数组大小
rehash
当 哈 希 表 中 元 素 总 数 > 负 载 因 子 ∗ 数 组 大 小 哈希表中元素总数> 负载因子 * 数组大小 哈希表中元素总数>负载因子∗数组大小时,冲突会非常明显影响查询效率。所以要rehash,首先申请一个两倍容量的新数组,然后把原数组中的键值对拷贝到新数组中。由于全部拷贝影响服务器性能,redis渐进式rehash,定义rehash的键值对索引rehashidx ,初始值-1,每执行一次插入删除或查询操作,rehashidx+1,并把该位置处的键值对拷贝到新数组中。每次查询时都要查询新旧两个哈希表。
stl中unordered_map
template<class Key, class Ty, //分别为键和值的类型
class Hash = std::hash<Key>, //hash函数,最好是函数对象,方便内联
class Pred = std::equal_to<Key> // 哈希冲突时的用来区分键值对,最好是函数对象,方便内联
> class unordered_map
自定义关键字时要定义hash函数,equal_to函数。
struct Edge
{
int w;
Edge(int i): w(i){}
};
struct EdgeHash {
size_t operator()(const Edge& e)
{
return hash<int>()(e.from.id) ^ hash<int>()(e.to.id);
}
};
struct EdgeCmp {
bool operator()(const Edge& e1, const Edge& e2)
{
return e1.from.id == e2.from.id && e1.to.id == e2.to.id;
}
};
unordered_map<int, Node> nodes;
unordered_set<Edge, EdgeHash, EdgeCmp> edges = //注意模板参数传的是类型,而不是对象EdgeHash();
{Edge(1), Edge(2), Edge(3)};
参考:
https://zhuanlan.zhihu.com/p/77533501
redis rehash
自定义key