题目描述
请设计一个哈希表,要求不能使用任何哈希表库函数。
具体而言,你需要实现如下三个函数:
put(key, value)
:在哈希表中插入一对(key, value),如果key已经在哈希表中,则更新对应的value值;get(key)
:返回key对应的哈希值,如果key不存在,则返回-1;remove(key)
:删除key,以及key对应的哈希值;
注意:
- 所有的key和value都在区间
[0, 1000000]
中; - 总操作次数在区间
[1, 10000]
中; - 不能使用任何哈希表库函数;
样例
MyHashMap hashMap = new MyHashMap();
hashMap.put(1, 1);
hashMap.put(2, 2);
hashMap.get(1); // 返回 1
hashMap.get(3); // 返回 -1 (未找到)
hashMap.put(2, 1); // 更新现有的值
hashMap.get(2); // 返回 1
hashMap.remove(2); // 删除 2 及其对应的值
hashMap.get(2); // 返回 -1 (未找到)
算法1
(拉链法) $O(1)$
哈希表的基本思想都是先开一个大数组,然后用某种哈希函数将key映射到数组的下标空间。
不同算法的区别在于如何处理下标冲突,即当两个不同的key被映射到同一下标时,该怎么办。
一般有两种方式处理冲突:拉链法和开放寻址法。
首先我们来介绍拉链法。它的思想很简单,在哈希表中的每个位置上,用一个链表来存储所有映射到该位置的元素。
- 对于
put(key, value)
操作,我们先求出key的哈希值,然后遍历该位置上的链表,如果链表中包含key,则更新其对应的value;如果链表中不包含key,则直接将(key,value)插入该链表中。 - 对于
get(key)
操作,求出key对应的哈希值后,遍历该位置上的链表,如果key在链表中,则返回其对应的value,否则返回-1。 - 对于
remove(key)
,求出key的哈希值后,遍历该位置上的链表,如果key在链表中,则将其删除。
时间复杂度分析:最坏情况下,所有key的哈希值都相同,且key互不相同,则所有操作的时间复杂度都是 $O(n)$。但最坏情况很难达到,每个操作的期望时间复杂度是 $O(1)$。
空间复杂度分析:一般情况下,初始的大数组开到总数据量的两到三倍大小即可,且所有链表的总长度是 $O(n)$ 级别的,所以总空间复杂度是 $O(n)$。
C++ 代码
class MyHashMap {
public:
/** Initialize your data structure here. */
const static int N = 20011;
vector<list<pair<int,int>>> hash;
MyHashMap() {
hash = vector<list<pair<int,int>>>(N);
}
list<pair<int,int>>::iterator find(int key)
{
int t = key % N;
auto it = hash[t].begin();
for (; it != hash[t].end(); it ++ )
if (it->first == key)
break;
return it;
}
/** value will always be non-negative. */
void put(int key, int value) {
int t = key % N;
auto it = find(key);
if (it == hash[t].end())
hash[t].push_back(make_pair(key, value));
else
it->second = value;
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
int get(int key) {
auto it = find(key);
if (it == hash[key % N].end())
return -1;
return it->second;
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
void remove(int key) {
int t = key % N;
auto it = find(key);
if (it != hash[t].end())
hash[t].erase(it);
}
};
/**
* Your MyHashMap object will be instantiated and called as such:
* MyHashMap obj = new MyHashMap();
* obj.put(key,value);
* int param_2 = obj.get(key);
* obj.remove(key);
*/
算法2
(开放寻址法) $O(1)$
开放寻址法的基本思想是这样的:如果当前位置已经被占,则顺次查看下一个位置,直到找到一个空位置为止。
- 对于
put(key, value)
操作,求出key的哈希值后,顺次往后找,直到找到key或者找到一个空位置为止,然后将key放到该位置上,同时更新相应的value。 - 对于
get(key)
操作,求出key的哈希值后,顺次往后找,直到找到key或者-1为止(注意空位置有两种:-1和-2,这里找到-1才会停止),如果找到了key,则返回对应的value。 - 对于
remove(key)
操作,求出key的哈希值后,顺次往后找,直到找到key或者-1为止,如果找到了key,则将该位置的key改为-2,表示该数已被删除。
注意:当我们把一个key删除后,不能将其改成-1,而应该打上另一种标记。否则一个连续的链会从该位置断开,导致后面的数查询不到。
时间复杂度分析:最坏情况下,所有key的哈希值都相同,且key互不相同,则所有操作的时间复杂度都是 $O(n)$。但实际应用中最坏情况难以遇到,每种操作的期望时间复杂度是 $O(1)$。
空间复杂度分析:一般来说,初始大数组开到总数据量的两到三倍,就可以得到比较好的运行效率,空间复杂度是 $O(n)$。
C++ 代码
class MyHashMap {
public:
/** Initialize your data structure here. */
const static int N = 20011;
int hash_key[N], hash_value[N];
MyHashMap() {
memset(hash_key, -1, sizeof hash_key);
}
int find(int key)
{
int t = key % N;
while (hash_key[t] != key && hash_key[t] != -1)
{
if ( ++t == N) t = 0;
}
return t;
}
/** value will always be non-negative. */
void put(int key, int value) {
int t = find(key);
hash_key[t] = key;
hash_value[t] = value;
}
/** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
int get(int key) {
int t = find(key);
if (hash_key[t] == -1) return -1;
return hash_value[t];
}
/** Removes the mapping of the specified value key if this map contains a mapping for the key */
void remove(int key) {
int t = find(key);
if (hash_key[t] != -1)
hash_key[t] = -2;
}
};
/**
* Your MyHashMap object will be instantiated and called as such:
* MyHashMap obj = new MyHashMap();
* obj.put(key,value);
* int param_2 = obj.get(key);
* obj.remove(key);
*/
这里第二个get的时候不能直接调用find 吧
如果has_key[t] = -2 的话 应该是继续往下找而不是返回这个下标
没问题的,因为本题的数据范围保证所有查询的数都是非负的,所以如果
has_key[t] == -2
,那么它一定和key
不相等,不会返回的。vector[HTML_REMOVED]>> hash 这里面的List 好像没有用啊
用到了,每个位置上可能会存多个数,要用链表存起来。
线性开放寻址法存在两个问题:-1标记空,-2标记被删除。-1单向变为-2,不会再有-1生成
1、put()时新元素若可以放入-2标志, 可能会导致remove()删除不干净
2、put()时新元素若不可以放入-2标志,会导致-2永远不会再次被利用。导致堆聚现象,性能变差
上面给出的两个做法是在算法题中常用的算法,在实际应用中的哈希表比这个要复杂很多。
对于-2这类的问题,在工程上有个常用的处理方式:每隔一段时间,或者当-2的个数达到一定程度的时候,系统就自动启动清理进程,把所有-2的位置回收一遍。
请问为啥会导致remove删除不干净呢?
这里不会导致删除不干净的,只是当被删元素太多时对查询效率有一定影响。
就是初始大数组的size一般定义成质数比较好,不过你知道为什么么?以前听老师讲过,但是忘记了
一般定义成离2的整次幂比较远的质数,这样取模之后冲突的概率比较低,但证明不太记得了。
其实要支持全int范围做key的话,不一定需要新开一个数组(消耗O(n)空间),比如可以同样用-1表示被删除的key,0表示empty slot,然后用两个bool,两个int分别表示-1/0是否在hash-map中,以及在的话映射为多少。
说得有道理!
只用开两个变量即可,不用多开一个数组。
这道题key和value都是int,用key的值对应下标,不会有冲突问题吧?
好问题。这题比较取巧,因为题目中指明所有value都是非负整数,所以可以用-1表示key不存在。
一般情况下,可以新开一个数组,表示每个位置的状态。