标题:[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构
个人主页:@水墨不写bug
本文讲解的是实现开散列法的哈希,闭散列法的哈希参考本篇文章:
目录
正文开始:
一、开散列法
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数(哈希函数)计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
数据的存储
开散列法需要我们自己设计一个存储数据的节点:HashNode;节点里面存储数值域和指向下一个节点的指针,具体实现如下:
//实现为单链表的结构 template<class V> struct HashNode { typedef HashNode<V> Node; HashNode(const V& data) :_data(data) ,_next(nullptr) {} V _data;//结构的数值域 Node* _next;//指向下一个节点的指针 };
上面的仅仅是链表的一个节点,这些节点可以链接成链表。链表的头节点存储在一个顺序表中,使用vector;则vector里面存储的是节点的指针类型的头节点。
由于我们在《[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构(一)》 讲解过具体仿函数KeyOfV,Hash的具体作用,这里不再赘述;
哈希表类型的类设置如下:
template<class K,class V,class KeyOfV,class Hash = HashFunc<K>> class HashTable { typedef HashNode<V> Node; public: //默认构造,开10个存储位置,默认存放10个nullptr HashTable() :_n(0) { _table.resize(10,nullptr); } private: vector<Node*> _table; size_t _n; };
二、开散列的接口设计
1.Find
首先求得数据通过映射后在哈希表中属于的桶位置,遍历这个桶(单链表)同时对比key即可,如果找到,返回对应节点的指针;如果找不到,则返回空指针。
//根据key查找 Node* Find(const K& key) { Hash hs; KeyOfV kov; size_t hashi = hs(key) % _table.size(); Node* cur = _table[hashi]; while (cur) { if (kov(cur->_data) == key) return cur; cur = cur->_next; } return nullptr; }
仿函数的理解
但是其实重要的还是仿函数hs,kov的理解:
hs是Hash类实例化的一个对象:Hash类重载了operator()(),所以通过hs()调用就相当于调用Hash类内部的operator()()。
kov是KeyOfV类实例化的一个对象:KeyOfV上层传下来的一个类类型,其内部也重载了operator()(),通过kov()调用就相当于调用上层(也许是多个不同的类)的类内部的operator()()。
2.Erase
与闭散列的思路差别较大;首先需要通过映射找到元素在哈希表的对应的桶的位置,接下来遍历这个链表找目标节点即可,如果找到了,直接删除节点并返回true;如果没有找到,则返回false表示删除失败。
//根据key删除 bool Erase(const K& key) { Hash hs; KeyOfV kov; //找到对应的链表 size_t hashi = hs(key) % _table.size(); Node* cur = _table[hashi]; Node* prev = nullptr; while (cur) { //遍历桶(链表)若匹配成功,进入删除逻辑 if (kov(cur->_data) == key) { //删除特殊判断,cur如果是第一个节点,则直接让表存储cur下一个节点即可 if (prev == nullptr) { _table[hashi] = cur->_next; } else//否则,prev->next指向cur下一个节点 { prev = cur->_next; } //删除cur delete cur; --_n;//哈希表中节点数-- return true; } //匹配失败,cur向后移动 prev = cur; cur = cur->_next; } return false; }
3.Insert
基本的插入逻辑比较简单:按照映射找到元素在哈希表中对应的桶,对这个链表进行头插即可:
//插入逻辑 bool Insert(const V& data) { Hash hs; KeyOfV kov; //如果存在重复元素,则插入失败 if (Find(kov(data))) { return false; } //扩容 //... size_t hashi = hs(kov(data)) % _table.size(); Node* newNode = new Node(data); newNode->_next = _table[hashi]; _table[hashi] = newNode; ++_n; return true; }
扩容逻辑
对于开散列的哈希而言,是靠先寻址,再挂桶的过程插入的。如果一个桶内部的节点过多,也会降低哈希的查找效率,所以开散列的哈希也需要扩容。
这里我们需要根据哈希结构桶内部节点数的多少来决定扩容的。
对于理想状况,当每一个哈希地址处刚好挂一个桶时,是最优的结构,这时的查找效率最高。所以根据这样的情景,我们就在哈希内部桶的节点总数等于哈希表长度的时候选择扩容:
在扩容时,内部节点数是不变的;变的是节点的挂载位置。我们需要new一个新的vector,遍历旧表,同时计算旧表中节点在新表中的位置,将旧表中的数据挂载到新表即可:
//插入逻辑 bool Insert(const V& data) { Hash hs; KeyOfV kov; //如果存在重复元素,则插入失败 if (Find(kov(data))) { return false; } //扩容逻辑,根据桶内部节点的总数 决定什么时候扩容 if (_n == _table.size()) { vector<Node*> newTable; //两倍容量 newTable.resize(_table.size()*2,nullptr); //遍历旧表 for (size_t i = 0; i < _table.size(); ++i) { Node* cur = _table[i]; while (cur) { Node* next = cur->_next; //找到旧表中节点对应新表的位置 size_t hashi = hs(kov(cur->_data)) % newTable.size(); //插入新表 cur->_next = newTable[hashi]; newTable[hashi] = cur; cur = next; } _table[i] = nullptr;//旧表中节点置空 } //交换vector,不必再手动释放旧表 //旧表的vector被交换到newtable,而newtable是局部变量,出作用域自动销毁 _table.swap(newTable); } size_t hashi = hs(kov(data)) % _table.size(); Node* newNode = new Node(data); newNode->_next = _table[hashi]; _table[hashi] = newNode; ++_n; return true; }
完~
未经作者同意禁止转载