不使用任何内建的哈希表库设计一个哈希映射
具体地说,你的设计应该包含以下的功能
put(key, value):向哈希映射中插入(键,值)的数值对。如果键对应的值已经存在,更新这个值。
get(key):返回给定的键所对应的值,如果映射中不包含这个键,返回-1。
remove(key):如果映射中存在这个键,删除这个数值对。
示例:
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 (未找到)
注意:
所有的值都在 [0, 1000000]的范围内。
操作的总数目在[1, 10000]范围内。
不要使用内建的哈希库。
1、暴力求解法
#include<iostream>
#include<vector>
#include <time.h>
using namespace std;
class Hashmap1 {
public:
Hashmap1() :data(vector<int>(N, -1))
{}
void put(int key, int value) {
data[key] = value;
}
int get(int key) {
if (data[key] != -1)//-1也就是这个键没有,初始的值就是-1
{
return data[key];
}
return -1;
}
void remove(int key) {
data[key] = -1;
}
private:
static const int N = 1000001;
vector<int> data;
};
int main()
{
clock_t start = clock();
Hashmap1 hash;
hash.put(1, 10);
hash.put(10000, 20);
cout << hash.get(10000) << endl;
hash.remove(10000);
cout << hash.get(10000) << endl;
clock_t ends = clock();
cout << "Running Time : " << (double)(ends - start) / CLOCKS_PER_SEC << endl;//运行时间为0.193秒
return 0;
}
分析一下:暴力求解法,我们的哈希函数就是数组存储位置下标=key,为了存储100000以内的key,我们创建了一个1000000长度的int数组,int数据范围足够存100000以内的数字。
符合题目要求。空间复杂度就是1000000,数组长度。时间复杂度就是o(1)
提交之后,发现空间消耗203MB,时间消耗193毫秒。
显然这是不够的,我们需要更加高效的解法。
2.二维数组,稀疏数组节省空间
先看代码:
#include<iostream>
#include<vector>
#include <time.h>
#include <cstring>
using namespace std;
class Hashmap1 {
public:
Hashmap1()
{
for(int i=1;i<1001;i++)
for (int j = 1; j < 1001; j++)
{
data[i][j] = -1;
}
}
auto getHashKey1(int key) {
return key % N;
}
auto getHashKey2(int key) {
return (key / N)+1;
}
void put(int key, int value) {
auto hashKey1 = getHashKey1(key);
auto hashKey2 = getHashKey2(key);
data[hashKey2][hashKey1] = value;
}
int get(int key) {
auto hashKey1 = getHashKey1(key);
auto hashKey2 = getHashKey2(key);
if (data[hashKey2][hashKey1] != -1)
{
return data[hashKey2][hashKey1];
}
return -1;
}
void remove(int key) {
auto hashKey1 = getHashKey1(key);
auto hashKey2 = getHashKey2(key);
data[hashKey2][hashKey1] = -1;
}
private:
static const int N = 1001;
int data[N][N];
};
int main()
{
Hashmap1* hash=new Hashmap1();
hash->put(1, 10);
hash->put(1001, 20);
cout << hash->get(1001) << endl;
hash->remove(1001);
cout << hash->get(1001) << endl;
delete(hash);
return 0;
}
我们使用10011001的二维数组存储,注意的一点是,
如果我们用Hashmap1 hash来创建一个新对象,会出现警告,因为这个是从栈中分配空间,深度是有限的适用于存放简单的临时变量,一旦存储空间过长,就会出现溢出。
我们可以用Hashmap1 hash=new Hashmap1();new一个对象,这是从堆中分配空间的,重要的是我们要手动释放空间delete(hash),并且调用函数时要用指针引用->.
不过好像并没有节省空间,和我的预期有差距,在leetcode没有通过,应该是空间太大。想了一下,一开始就定义了1001*1001并且初始化了,而且判断位置时更复杂了,应该在Put时才把那一行初始化为1,这样的话,有很多行就不用初始化,节省空间。
3、 开放定址法:线性探测,如果有冲突,就找下一个位置存储。发现空间优化了,只用了53MB,但是用时336ms,比暴力法增加了近一倍。
class MyHashMap {
public:
MyHashMap() {
hashTable = vector<pair<int, int>>(N, {-1, -1});
}
int find(int key) {
int k = key % N;
while (hashTable[k].first != key && hashTable[k].first != -1) {
k = (k + 1) % N;
}
return k;
}
void put(int key, int value) {
auto k = find(key);
hashTable[k] = {key, value};
}
int get(int key) {
auto k = find(key);
if (hashTable[k].first == -1) {
return -1;
}
return hashTable[k].second;
}
void remove(int key) {
auto k = find(key);
if (hashTable[k].first != -1) {
hashTable[k].first = -2; // Mark as deleted (use a different value with -1)
}
}
private:
const static int N = 20011;
vector<pair<int, int>> hashTable;
};
4、拉链法
#include<iostream>
#include<vector>
#include <time.h>
#include <cstring>
using namespace std;
struct MyListNode {
int key;
int val;
MyListNode* next;
MyListNode() : key(-1), val(-1), next(nullptr) {}
MyListNode(int _key, int _val) : key(_key), val(_val), next(nullptr) {}
};
class MyHashMap {
public:
MyHashMap() {
nums.resize(N, new MyListNode());//vv.resize(int n,element)表示调整容器vv的大小为1009,扩容后的每个元素的值为element,默认为0,这里是一个空的头结点
}
void put(int key, int value) {
auto hashKey = getHashKey(key);
auto& head = nums[hashKey];
auto p = head;
auto tail = p;
while (p != nullptr) {
if (p->key == key) {
p->val = value;
return;
}
tail = p;
p = p->next;
}
tail->next = new MyListNode(key, value);
}
int getHashKey(int key) {
return key % N;
}
int get(int key) {
auto hashKey = getHashKey(key);
auto& head = nums[hashKey];
auto p = head;
while (p != nullptr) {
if (p->key == key) {
return p->val;
}
p = p->next;
}
return -1;
}
void remove(int key) {
auto hashKey = getHashKey(key);
// Note: if use auto head will cause crash,
// we want to set head to nullptr if last element deleted
auto& head = nums[hashKey];
MyListNode* prev = head;
auto p = head->next;
while (p != nullptr) {
if (p->key == key) {
prev->next = p->next;
p->next = nullptr;
delete p;
return;
}
prev = p;
p = p->next;
}
}
private:
// The closest prime number around 1000 is 997 and 1009
const static int N = 1009;
vector<MyListNode*> nums;
};
这里面也是定义了一个vector数组,只不过数组的元素是链表。我们需要定义一个链表结构体,里面存放键、值、指向下一个节点的指针。
其时间和空间效率和开放定址法差不多,时间效率稍好。
个人觉得,在存储查找方面拉链法是有优势的。比如我们如果在位置1发生了哈希冲突,拉链法只需要增加一个节点,查找也只需要多查一次,而开放定址法在1发生冲突后,存储查找次数还要受到2,3,4,5等位置的影响,如果2,3,4,5都有元素,那么要一直到6才能存储并找到,效率低了。