一、何为哈希表
哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。有两种不同类型的哈希表:哈希集合和哈希映射:
- 哈希集合是集合数据结构的实现之一,用于存储非重复值。(C++ unordered_set)
- 哈希映射是映射数据结构的实现之一,用于存储(key, value)键值对。(C++ unordered_map)
在标准模板库的帮助下,哈希表是易于使用的。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能。
二、哈希表原理
哈希表的关键思想是使用哈希函数将键映射到存储桶。更确切地说,
- 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
- 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
桶的实现主要有一下几个结构:
- 数组
查找O(N),插入和删除最坏也是O(N),因为插入和删除可能需要前后移动 - 链表
查找O(N),插入和删除是O(1) - 二叉搜索树BTS
查找、删除、插入O(logN),主要缺点是可能退化成链表 - 高度平衡的二叉搜索树:AVL树
三、哈希操作
- 插入
哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;插入操作需要注意存在冲突的情况。
- 搜索
哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
- 删除
首先搜索元素,然后在元素存在的情况下从相应位置移除元素。
- 哈希表插入、搜索、删除操作示例
在示例中,我们使用 y = x%5 作为哈希函数,让我们使用这个例子来完成插入和搜索策略:
-
插入:通过哈希函数解析得到键值,此处的键值就是桶的索引,就可以知道key要放入哪个桶中。
例如,1987 分配给桶 2,而 24 分配给桶 4 -
搜索:通过相同的哈希函数解析键值,并仅在特定存储桶中搜索
(1)如果搜索 1987,我们将使用相同的哈希函数将1987 映射到 2。因此我们在桶 2 中搜索,我们在那个桶中成功找到了 1987。
(2)如果搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。 -
删除:通过哈希函数得到键值,在对应的桶中查找对应的元素,如果有就删除,没有就什么也不做。
在设计哈希表时,需要注意2个最重要的基本因素:哈希函数和哈希冲突
四、哈希函数
哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶,常见的哈希函数是hashkey = key%N,其中key是键值,N是桶的数量,hashkey是桶的索引。在使用取余作为哈希函数时,取质数作为N是一个明智的选择,可以减少潜在的碰撞。
哈希函数将取决于键值的范围:key和桶的数量:N,常见的哈希函数有以下几种示例:
五、哈希冲突
如果设计的哈希函数是完美的一对一映射,我们将不需要处理冲突,不幸的是在大多数情况下,冲突几乎是不可避免的。例如,在我们之前的哈希函数(y = x%5)中,1987和2都分配给了桶 2,这是一个冲突。
冲突解决算法应该解决以下几个问题:
(1)如何组织在同一个桶中的值?
(2)如果为同一个桶分配了太多的值,该怎么办?
(3)如何在特定的桶中搜索目标值?
解决冲突有一下几种方法:
1.开放地址方法
哈希表中的空闲单元(即为a)既可以被哈希地址为a的关键字使用,也可以被发生冲突的其他关键字使用。谁先找到这个单元谁先占用。每当有碰撞, 则根据我们探查的策略找到一个空的槽为止,开放地址法主要有以下几种:
(1)线性探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上往后加一个单位,直至不发生哈希冲突。
(2)再平方探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
(3)伪随机探测
按顺序决定哈希值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来哈希值的基础上加上随机数,直至不发生哈希冲突。
2.链式地址法
对于相同的散列值,我们将它们放到一个桶中,每个桶是相互独立的,链式地址法的桶通常设计为链表的形式,主要有下面两种方法:
(1)单链表桶
单链表的删除和增加操作都是O(1),但是查找效率最坏是O(N)
(2)二叉搜索树桶
可以利用搜索二叉树(BST)作为桶的设计,这样查找的效率是O(logN)。
(3)平衡二叉树(ALV)桶
3.再散列法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
六、哈希表性能
哈希表的特性决定了其高效的性能,大多数情况下查找或者插入元素的时间复杂度可以达到O(1), 时间主要花在计算hash值上, 然而也有一些极端的情况,最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表,例如下面的图片:
当hash表变成图2的情况时,时间复杂度会变为O(n),效率瞬间低下,所以,设计一个好的哈希表尤其重要,如HashMap在jdk1.8后引入的红黑树结构就很好的解决了这种情况。
内置哈希表的原理
内置哈希表的典型设计是
- 键值可以是任何可哈希化的类型。并且属于可哈希类型的值将具有哈希码。此哈希码将用于映射函数以获取存储区索引。
- 每个桶包含一个数组,用于在初始时将所有值存储在同一个桶中。
- 如果在同一个桶中有太多的值,这些值将被保留在一个高度平衡的二叉树搜索树中。
插入和搜索的平均时间复杂度仍为 O(1)。最坏情况下插入和搜索的时间复杂度是 O(logN),使用高度平衡的BST。这是在插入和搜索之间的一种权衡。
七、简单哈希表实现(链式桶)
1.哈希集合
class Bucket{
public:
Bucket(){}
void insert(int key){
if (bucket.end() == find(bucket.begin(), bucket.end(), key)){
bucket.push_front(key);
}
}
void remove(int key){
bucket.remove(key);
}
bool isExist(int key){
if (bucket.end() != find(bucket.begin(), bucket.end(), key)){
return true;
} else {
return false;
}
}
private:
list<int> bucket;
};
const int N = 5001;
class MyHashSet {
public:
/** Initialize your data structure here. */
MyHashSet() {}
int hash(int key){
return key % N;
}
void add(int key) {
int hashValue = hash(key);
buckets[hashValue].insert(key);
}
void remove(int key) {
int hashValue = hash(key);
buckets[hashValue].remove(key);
}
/** Returns true if this set contains the specified element */
bool contains(int key) {
int hashValue = hash(key);
return buckets[hashValue].isExist(key);
}
private:
Bucket buckets[N];
};
2.哈希映射
class Bucket {
public:
Bucket(){}
void put(int key, int value) {
auto beg = bucket.begin();
auto end = bucket.end();
for (; beg != end; beg++) {
/* 键对应的值已存在,更新 */
if (beg->first == key) {
beg->second = value;
return;
}
}
/* 在桶中没有找到对应的key,插入 */
if (beg == end) {
bucket.push_front({ key,value });
}
}
int get(int key) {
auto beg = bucket.begin();
auto end = bucket.end();
for (; beg != end; beg++) {
/* 返回给定的键所对应的值 */
if (beg->first == key) {
return beg->second;
}
}
/* 映射中不包含这个键,返回-1 */
return -1;
}
void remove(int key) {
bucket.remove_if([key](pair<int,int> p) {
return key == p.first;
});
}
private:
/* 桶的基本数据结构 */
list<pair<int,int>> bucket;
};
const int N = 10000;
class MyHashMap {
public:
/** Initialize your data structure here. */
MyHashMap() {}
int hash(int key) {
return key % N;
}
/** 向哈希映射中插入(键,值)的数值对,如果键对应的值已经存在,更新这个值. */
void put(int key, int value) {
int hashKey = hash(key);
buckets[hashKey].put(key, value);
}
/** 返回给定的键所对应的值,如果映射中不包含这个键,返回-1 */
int get(int key) {
int hashKey = hash(key);
return buckets[hashKey].get(key);
}
/** 如果映射中存在这个键,删除这个数值对 */
void remove(int key) {
int hashKey = hash(key);
buckets[hashKey].remove(key);
}
private:
Bucket buckets[N];
};