【C++】哈希表模拟:开散列技术与哈希冲突处理
C++语法 | 相关知识点 | 可以通过点击 | 以下链接进行学习 | 一起加油! |
---|---|---|---|---|
命名空间 | 缺省参数与函数重载 | C++相关特性 | 类和对象-上篇 | 类和对象-中篇 |
类和对象-下篇 | 日期类 | C/C++内存管理 | 模板初阶 | String使用 |
String模拟实现 | Vector使用及其模拟实现 | List使用及其模拟实现 | 容器适配器Stack与Queue | Priority Queue与仿函数 |
模板进阶-模板特化 | 面向对象三大特性-继承机制 | 面向对象三大特性-多态机制 | STL 树形结构容器 | 二叉搜索树 |
AVL树 | 红黑树 | 红黑树封装map/set | 哈希-开篇 | 闭散列-模拟实现哈希 |
在上篇中,我们使用闭散列技术解决了哈希冲突并实现了哈希表。然而,我们发现闭散列并不理想,因此本篇将探讨如何通过开散列方法来处理哈希冲突。
🌈个人主页:是店小二呀
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈初阶数据结构专栏: 初阶数据结构
🌈高阶数据结构专栏: 高阶数据结构
🌈Linux专栏: Linux
🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
文章目录
- 一、开散列(哈希桶)
- 二、实现哈希表
- 2.1 哈希表基本构架
- 2.2 哈希表插入数据
- 2.3 哈希表析构函数
- 2.4 哈希表扩容
- 2.5 哈希表删除数据
- 2.6 哈希桶中查找
- 三、HashTable.h
一、开散列(哈希桶)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
二、实现哈希表
2.1 哈希表基本构架
template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash *= 131;hash += ch;}return hash;}
};
namespace hash_buck
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V> _next;};template<class K, class V,class Hash = HashFunc<K>>class HashTable{public:typedef HashNode<K, V> Node;HashTable(){_tables.resize(10,nullptr);}private:vector<Node*> _tables;//vector<list<..>> _tables;size_t _n = 0;};
}
这里如果想实现哈希桶结构,可以使用一个个节点连接起来或者直接套上一个链表容器。这里为了更好地实现迭代器,我们选择第一种方式,第二种方式实现迭代器有一丝麻烦。
开散列中每个桶中放的都是发生哈希冲突的元素 ,并没有顺序之分。
2.2 哈希表插入数据
bool Insert(const pair<K, V>& kv)
{Hash hs;size_t hashi = hs(kv.first) % _tables.size();//进行头插操作Node* newnode = new Node(kv);_tables[hashi]->_next = newnode;_tables[hashi] = newnode;
}
这里链表节点只需要单纯地存储数据,那么只需要设计单链表即可,没有必要设双向链表。对于单链表插入数据,一般推荐采用头插,由于尾插需要找尾比较麻烦。
2.3 哈希表析构函数
这里涉及堆上空间资源的开辟,一般需要涉及析构函数进行资源处理。
由于vector容器元素为内置类型,析构函数对内置类型不进行处理,那么就导致指向空间没有得到释放,需要显式写析构函数,完成资源释放。
~HashTable()
{for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}}
}
2.4 哈希表扩容
由于哈希桶去解决哈希冲突对于哈希表空间需要相对比较小,不同于开发定址法去解决哈希冲突占用表内空间,而是表中一个指针指向一个空间。
【负载因子满足1即可扩容】:_n == _tables.size()
【第一个方案】:复用Insert完成CV操作
//扩容逻辑if (_n == _tables.size()){//这里可以忽略类型HashTable NewTable;NewTable._tables.resize(n * 10);//CV工作for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){NewTable.Insert(_tables[i]._kv);cur = cur->_next;}}_tables.swap(NewTable);}
【缺陷】:如果存在一万个节点,意味着需要复制一万个节点又要释放一万个节点,显得很浪费
【第二个方案】:直接将节点拿下来
if (_n == _tables.size())
{vector<Node*> NewTable(n * 10, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;cur->_next = _tables[i];_tables[i] = cur;cur = cur->_next;}_tables[i] = nullptr;}_tables.swap(NewTable);
}
关于这段代码是比较难懂,每个指针指向一块空间,先cur->next = newTables[hashi]
抓住新表的位置,newTables[hashi] = cur
新表将旧表节点拿下来,可以好好理解拿下来的逻辑,得到了这个地址,这个地址指向一块节点。
这里会导致newTables[hashi]
和_tables[i]
共同指向一块空间,虽然不会去使用旧表去影响到新表,为了保险起见,可以将旧表这块位置指针置空。
2.5 哈希表删除数据
bool Erase(const K& key)
{Hash hs;size_t hashi = hs(kv.first) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;
}
这里删除数据,无非需要考虑两种情况的删除,一种是删除第一个节点,另一种是删除其他节点prev->_next = cur->_next
。在删除节点需要前后兼顾,保存下前驱指针指向节点。
2.6 哈希桶中查找
Node* Find(const K& key)
{Hash hs;size_t hashi = hs(kv.first) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return &cur->_kv.first;}else{cur = cur->_next;}}return nullptr;
}
三、HashTable.h
template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash *= 131;hash += ch;}return hash;}
};namespace hash_bucket
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V> _next;};template<class K, class V,class Hash = HashFunc<K>>class HashTable{public:typedef HashNode<K, V> Node;HashTable(){_tables.resize(10,nullptr);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}}}bool Insert(const pair<K, V>& kv){Hash hs;size_t hashi = hs(kv.first) % _tables.size();//扩容逻辑//if (_n == _tables.size())//{// //这里可以忽略类型// HashTable NewTable;// NewTable._tables.resize(n * 10);// // //CV工作// for (size_t i = 0; i < _tables.size(); i++)// {// Node* cur = _tables[i];// while (cur)// {// NewTable.Insert(_tables[i]._kv);// cur = cur->_next;// }// }// _tables.swap(NewTable);//}if (_n == _tables.size()){vector<Node*> NewTable(n * 10, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 头插新表的位置size_t hashi = hs(kot(cur->_data)) % NewTable.size();cur->_next = NewTable[hashi];NewTable[hashi] = cur;cur = cur->_next;}_tables[i] = nullptr;}_tables.swap(NewTable);}//进行头插操作Node* newnode = new Node(kv);_tables[hashi]->_next = newnode;_tables[hashi] = newnode;++_n;}Node* Find(const K& key){Hash hs;size_t hashi = hs(kv.first) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return &cur->_kv.first;}else{cur = cur->_next;}}return nullptr;}bool Erase(const K& key){Hash hs;size_t hashi = hs(kv.first) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<Node*> _tables;size_t _n = 0;};
}
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!