链地址法(哈希桶)
解决冲突的思路
开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表 中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
扩容
开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈 希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;stl中 unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中
h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1,h(24) = 2,h(96) = 88
哈希桶实现代码:
namespace hash_bucket
{
//仿函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//string用得多,特化一下
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
}
return hash;
}
};
template<class K,class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;//别定义成pair了
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K,class V,class Hash=HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
:_tables(__stl_next_prime(0))
,_n(0)
{}
//vector里面存的是内置类型Node*而不是自定义类型,所以要自己处理
//如果析构需要自己写那么拷贝构造和赋值重载也得自己写
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;//
}
}
Hash hash;//
Node* Find(const K& key)
{
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (key == cur->_kv.first)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;//要记录
while (cur)
{
if (key == cur->_kv.first)
{
//分两种情况处理
if (prev == nullptr)//要删除的是头结点
{
_tables[hashi] = cur->_next;
}
else//删除的不是头结点
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
bool Insert(const pair<K,V>& kv)
{
if (_n == _tables.size())
{
vector<Node*> newtables(__stl_next_prime(_tables.size())+1);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);//
}
//头插
size_t hashi = hash(kv.first) % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
private:
vector<Node*> _tables;
size_t _n=0;
//自己造轮子
/*struct Data
{
ListNode* _head;
RBTreeNode* _root;
size_t len;
};*/
//套两个结构在里面
/*struct Date
{
list<pair<K, V>> _list;
map<pair<K, V>> _map;
size_t len;
};
vector<Data> _tables;
size_t _n = 0;*/
};
Tips:
- 在扩容时,我们不再像开放定址法为了复用Insert而创建一个HashTable而是创建一个新vector,因为Insert里要创建新节点,然后swap之后带走旧表时又要释放这些结点,一来一回消耗比较大;直接用新vector,把原vector挂的结点直接“拿”到新表就可以了。
- 如果觉得挂的链表太长了,可以换成比如_n>8就换挂红黑树,创建一个结构体Data,里面存储的可以是自己“造轮子”的也可以直接用list和map这两个结构(就可以直接用自带的接口)。如果减到比如8个以下了再换回链表。
- vector里面存的是内置类型Node*而不是自定义类型,所以要自己处理,否则不会被处理;如果析构函数需要自己实现,那么赋值重载和拷贝构造也需要自己实现,深拷贝。