作者:@小萌新
专栏:@数据结构进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:模拟实现高阶数据结构 哈希桶
首先对关键码集合使用哈希函数来计算哈希地址 具有相同地址的关键码归于同一子集
每一个子集称为一个桶
每个桶通过一个单链表连接起来 各链表的头节点存放在哈希表中
例如我们使用除留余数法将序列{1, 6, 15, 60, 88, 7, 40, 5, 10}插入到表长为10的哈希表中
当发生哈希冲突的时候我们采取开散列的形式 将哈希地址相同的元素连接到同一个哈希桶下
抽象图大概是这样子
闭散列解决哈希冲突是一种比较野蛮的方式 即我的位置被抢了就去抢别人的位置
而开散列的解决方式就是我们的位置被抢了也没关系 我可以挂在下面
和闭散列不同的是 开散列将相同哈希地址通过单链表连接起来 不会影响与自己不同的哈希地址的增删查改效率 因此开散列的负载因子相比于闭散列而言可以稍微大一点
因此在实际中 开散列的哈希桶比闭散列的哈希表更加实用
下面我们来看一下哈希桶的极端情况
假设我们要插入的元素集合是这样子的{3, 13, 23, 33, 43, 53, 63, 73, 83}
那么我们看看会变成什么样子
是不是直接退化成单链表了 那么这个时候查找元素的时间复杂度就退化成O(N)了
但是呢 这个时候我们可以将这个单链表转化成红黑树
这样子是不是时间复杂度又被我们优化到Log(N)了 这就是一种底线思维 即考虑最差的情况
为了避免上面说的这种极端情况的出现 一般桶中的节点个数超过8时 我们会将其转化为红黑树
当桶中的节点个数被删除至小于等于八时 我们会将其转化为单链表
在开散列的哈希表中 哈希表的每个位置存储的实际上是某个单链表的头结点 即每个哈希桶中存储的数据实际上是一个节点类型 该节点类型除了储存所给数据之外 还要指向下一个节点
节点类的代码如下
template
struct HashNode
{pair _kv;HashNode* _next;HashNode(const pair& kv) // 列表初始化构造函数:_kv(kv),_next(nullptr){}
};
和闭散列不同的是 我们不需要为我们哈希表的每一个位置设置一个状态
因为在开散列当中我们将所有经过哈希函数计算后相同的地址放在同一个桶中
我们只需要去桶中寻找 如果桶中没有就是没有了
哈希表的开散列实现形式也需要根据负载因子来判断是否需要扩容 所以我们应该增加一个数据来存储哈希表中的有效元素个数 当负载因子过大时候进行扩容
哈希表的结构大体如下
template
class HashTable
{typedef HashNode Node;
public:
private:vector _table;int _n;};
哈希表插入的步骤如下
其中哈希表的调整方式如下
与上一篇文章中的插入不同的是 我们这里不需要复用插入函数
只需要遍历原节点的时候改变每个节点的位置就可以了
将键值对插入哈希表的方式如下
bool Insert(const pair& kv){// 1. 查看哈希表中是否存在该键值对Node* ret = find(kv.first);if (ret){return false; // 这里就说明找到了这个节点 如是直接返回false 表示插入失败}// 2. 判断是否需要调整哈希表的大小if (_n == _table.size()) // 哈希表的大小为0 此时负载因子我们定义为1{vector newtable;size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;newtable.resize(newsize);// 将原哈希表中的所有节点插入到新哈希表中for (size_t i = 0; i < _table.size(); i++){if (_table[i])// 单链表存在{Node* cur = _table[i];while (cur){Node* next = cur->_next;size_t index = cur->_kv.first % newtable.size();// 将该节点的头插到新哈希表中编号为index的哈希桶中cur->_next = newtable[index];//第一次是空 后面就是上面的插入的节点newtable[index] = cur;cur = next;}_table[i] = nullptr;}}_table.swap(newtable);}// 开始插入size_t index = kv.first % _table[index];Node* newnode = new Node(kv);newnode->_next = _table[index];_table[index] = newnode;_n++;return true;}
哈希表的查找分为以下几步
代码表示如下
Node* Find(const K& key){// 首先判断大小是否为0if (_table.size()==0){return nullptr;}// 其次经过哈希函数计算找到函数地址size_t index = key % _table.size();Node* cur = _table[index];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}
哈希表的删除实际上就是单链表的删除
大体步骤如下
代码表示如下
bool Erase(const K& key){// 首先找编号size_t index = key % _table.size();// 之后开始找被删除阶段Node* prev = nullptr;Node* cur = _table[index];while (cur) // cur为空说明遍历完了还没找到直接返回false{if (cur->_kv.first == key){if (prev == nullptr){_table[index] = nullptr;}else{prev->_next = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;}