哈希表详解及模拟实现(unordered_map)

目录

认识哈希表:

哈希冲突:

除留余数法--(常用)

 平方取中法--(了解)

折叠法--(了解)

随机数法--(了解)

泛型编程:

闭散列:

线性探测:

二次探测:

扩容:

查找:

插入:

 删除:

开散列:

扩容:

查找:

插入:

删除:

迭代器:

 全部代码:


认识哈希表:

        哈希表是一种数据结构,也称散列表,主要用于查找,且使用很频繁,可见它的效率相比其他用于查找的数据结构,肯定有优势。之前学习的顺序表和平衡二叉搜索树,查找的时间复杂度为O(n)和O(logn),它们两都需要通过key值一一比较不断缩小查找范围,进而查找到所需数据。而哈希表的优势在于无需比较,只需通过某种函数(哈希函数)计算关键码,通过映射关系可直接找到数据,近似O(1)的时间复杂度。

当向该结构中:

插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功

我们将它们放入哈希表key值如何取呢?一般数据是size_t或者能强转成size_t类型的key值直接取该数据,对于无法强转的例如string,则通过哈希函数转换,key值也不是直接当作下标存入哈希表内,不然如果一个数据过大,就要开很大的数组, 一般我们需要将 数据%数组大小来得到对应的下标,这种转换也是哈希函数, 举个例子:数据集合{1,7,6,4,5,9};

哈希冲突:

        还是上面的例子,这时我们有一个数据75要进入哈希表,通过哈希函数计算key值,75 % 10 = 5,但是发现下标为5的位置已经有数据,这就出现了不同的数据对应的相同的下标,这就是所谓的哈希冲突,又称哈希碰撞。

        注意哈希冲突是不能完全解决的,只能缓解,鸽巢定理可证,所有的字符串,肯定比size_t的最大值要大,因为不限长度字符串几乎是无限的,所以鸽子比巢多,肯定会有巢有两只以上的鸽子。

 缓解哈希冲突主要从两个方面:

1.,第一个方面就是对哈希函数入手,上面这个例子的哈希函数,是用key对数组大小取余,这是直接定址法,这种哈希函数出现哈希冲突的概率还是不小的,先来看看哈希函数的设计原则:

哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有m个地址时,其值
域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单

再来看看几种常见的哈希函数:

除留余数法--(常用)

假设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

 平方取中法--(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

折叠法--(了解)

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按哈希表表长,取后几位作为散列地址。

随机数法--(了解)

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。

 2.第二个方面就是对哈希表的存储结构入手,想必大家见过最多的哈希表结构就是顺序表+链表,其实哈希表也可以单纯用顺序表实现,两种不同的底层结构在于它们如何应对哈希冲突,C++的STL库中使用的是顺序表+链表的方式,没错这种方式的效率是更优的,但是单纯用顺序表的结构也是值得学习的,接下来的内容我会分别介绍并模拟实现这两种哈希表的底层结构。

泛型编程:

        在模拟实现中,我的my_unordered_set和my_unordered_map封装了一个哈希表HashTable,但set里面存的是一个数据K,而set里面存的是pair<K,T>,HashTable里面只接受一个data,这就导致了如果是set,data就是K类型,如果是map,data就是pair<K,V>,但我们只有一个哈希表,该怎么解决这种矛盾呢?

 仿函数:我们可以分别在set和map里创建一个类,在类里重载运算符(),然后在set中的()重载中直接返回K,在map中的()重载中返回pair中的K,也就是pair中的first,然后将这个类传给HashTable,在HashTable中使用data前就调用这个类的括号来取里面的数据:

set:

map: 

在HashTable中的使用:(哈希地址的计算中就用到了)

 HashFunc和上面讲的一样,主要作用是如果key为其他不是size_t的类型将它们强转成size_t和若为string则通过哈希函数转化成size_t,这种方法就是泛型编程的一种。

闭散列:

        闭散列,又称开放定址法,也就是上面提到的单纯使用顺序表的方法来实现哈希表,它应对哈希冲突的方法是如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,那么如何找到“下一个”空位置呢?

线性探测:

        回到最开始的例子,我们需要插入75,通过哈希函数计算下标为5,但下标为5的位置已经被占用。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

所以最后我们能找到8下标位置为空位置,然后进行插入数据。
注意往后探测过程中不能超过数组长度,所以我们每向后走一次就需要%数组长度,以保证当超过数组长度时,下标能回到数组开头。

如何判断这个位置是否为空,只需将每个位置里面存入一个状态值(枚举类型),总共有三个状态:EMPTY,EXIST,DELETE,分别表示空,存在数据,之前有数据但被删除

这部分代码只需理解线性探测思路就行,其他不理解的地方,需结合全部代码。

二次探测:

        虽然线性探测能解决哈希冲突,但可以发现这样冲突的数据大部分都聚在了一起,不离散,如图:

为了避免这种情况,线性探测时hashi每次向后走1步,我们采用二次探测,也就是每次向后走i的平方步,每次i++,也就是依次走1,4,9,16,25......步,实现很简单,这里不演示。

扩容:

        闭散列的扩容不是满了才扩容,我们先引入一个概念:负载因子,负载因子 = 存在的元素/数组的容量,简单来说负载因子就是占用率,当负载因子>=0.7的时候我们才进行扩容。

扩容思路:

我们可以直接开一个新的hash表,将新表的大小设为旧表的2倍,再将旧表的元素一个个插入到新表,最后用swap函数交换新旧表。

这样写的好处:不必销毁新表,因为新表是局部对象,函数结束后自动销毁了。

查找:

         通过key查找某个节点:

先通过key用哈希函数算出对应哈希地址,再从哈希地址开始往后线性探测,找到后返回节点:

插入:

        分析一下插入,当插入一个数时该如何做呢?

1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。

2.用负载因子判断是否需要扩容,需要就进行扩容。

3.通过key和哈希函数,算出哈希地址。

4.哈希地址上有值就往后线性探测。

 删除:

        因为之前,我们在每个节点上都设置了三种状态:EMPTY,EXIST,DELETE,所以现在删除一个数就非常简单了:

只需先通过哈希函数和线性探测找到该节点,再将该节点的状态改为DELETE即可。

开散列:

        开散列也就是C++STL库哈希表实现方法,说明它相比闭散列还是有一定的优越性的,开散列应对哈希冲突的方法就是在冲突数据下面用链表进行连接。

扩容:

        开散列的扩容条件就是_n == 数组大小的时候: 

相比闭散列的扩容方法,开散列只要扩容条件不同,其他差不多,只有旧表中每个桶的数据要依次头插到新表对应的哈希地址。

查找:

  如果对应的哈希地址里面有数据,就沿着该地址的链表遍历,找到即可。 

插入:

1.先用查找函数判断能否找到,若找到了,代表原哈希表里有,直接返回false。

2.判断是否需要扩容,需要就进行扩容。

3.通过key和哈希函数,算出哈希地址。

4.在该哈希地址的链表处进行头插。

删除:

1.先通过查找函数找到key对应的哈希地址

2.遍历该哈希地址的链表。

3.找到后连接该节点的上一个和下一个节点。

4.注意如果是头删要特殊处理,因为上一个节点为空。 

迭代器:

        迭代器功能都比较简单,这里我只讲++的思路,其他功能可以到文章最后看全部代码。

++:

1.到一个哈希地址时要先判断存不存在冲突数据,也就是链表。

2.若有冲突数据,直接走向链表的next即可。

3.链表走到尾部,就需要从这个链表的哈希地址开始往后线性探测,直到找到下一个有有效数据的哈希地址或者哈希表走完。

 全部代码:

  my_ordered_set.h

#include"HashTable.h"template<class K,class Hash = HashFunc<K>>
class my_unordered_map
{struct SetKeyOfT{const K& operator()(const K& key){return key;}};typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;public:iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}const_iterator begin()const{return _ht.begin();}const_iterator end()const{return _ht.end();}pair<iterator,bool> insert(const K& key){return _ht.Insert(key);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K,const K,SetKeyOfT,Hash> _ht;
};

my_unordered_map.h

#include"HashTable.h"
template<class K, class T,class Hash = HashFunc<K>>
class my_unordered_set
{struct MapKeyOfT{const K& operator()(const pair<K,T>& kv){return kv.first;}};typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::iterator iterator;typedef typename hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash>::const_iterator const_iterator;public:iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}const_iterator begin()const{return _ht.begin();}const_iterator end()const{return _ht.end();}pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}T& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, T()));return ret->iterator->second;}
private:hash_bucket::HashTable<K, pair<K,T>, MapKeyOfT, Hash> _ht;
};

 HashTable.h

#pragma once
#include<vector>
#include<string>
using namespace std;
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 open_address
{enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;};template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//_n / _table.size() >= 0.7//不采用这种判断方法是因为左右类型不同,所以将左右都*10if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);// 旧表重新计算负载到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);//交换新旧表}Hash hs;size_t hashi = hs(kv.first) % _tables.size();//hs为仿函数,将key转为size_t,因为kv.first 可能为string // 线性探测while (_tables[hashi]._state == EXIST)//_tables为顺序表{++hashi;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();// 线性探测while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST &&_tables[hashi]._kv.first == key){return &_tables[hashi];}++hashi;hashi %= _tables.size();//防止越界}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;--_n;return true;}}private:vector<HashData<K, V>> _tables;size_t _n = 0;  // 有效数据个数};
}namespace hash_bucket
{template<class T>struct HashNode{T _data;HashNode<T>* next;HashNode(const T& data):_data(data),next(nullptr){}};template<class K,class T,class KeyOfT,class Hash = HashFunc<T>>class HashTable{typedef HashNode<T> Node;public:template<class Ptr,class Ref>struct _HTIterator//迭代器,设置为内部类{typedef _HTIterator Self;typedef HashNode<T> Node;Node* _node;const HashTable* _pht;_HTIterator(Node* node,const HashTable* pht):_node(node),_pht(pht){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}Self& operator++(){if (_node->next)//当前桶没走完,直接找下一个节点{return _node->next;}else{Hash ha;KeyOfT kot;size_t i = ha(kot(_node->_data)) % _pht->_table.size();i++;for (;i < _pht->_table.size();i++){if (_pht->_table[i])break;}//循环结束后要么找到一个有数据的桶,要么整个表走完了if (i == _table.size())//表走完了{_node = nullptr;}else//找到了有数据的桶{_node = _pht->_table[i];}}return this;}};typedef _HTIterator<T*, T&> iterator;typedef _HTIterator<const T*, const T&> const_iterator;iterator end(){return iterator(nullptr, this);}iterator begin(){size_t i = 0;for (i = 0;i < _table.size();i++){Node* cur = _table[i];if (cur){return iterator(cur, this);}}}const_iterator end() const{return const_terator(nullptr, this);}const_iterator begin() const{size_t i = 0;for (i = 0;i < _table.size();i++){Node* cur = _table[i];if (cur){return const_iterator(cur, this);}}}pair<iterator,bool> Insert(const T& data){HashFunc hs;KeyOfT kot;iterator it = Find(kot(data));if (it!=end())//如果表中原来有{return make_pair(it, false);}if (_n == _table.size())//扩容{vector<Node*> newtable(2 * _table.size(), nullptr);for (int i = 0;i < _table.size();i++){Node* cur = _table[i];while (cur){Node* next = cur->next;size_t hashi = hs(kot(data)) % newtable._table.size();//头插cur->next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}//新旧表交换_table.swap(newtable._table);}size_t hashi = hs(kot(data)) % _table.size();Node* newnode = new HashNode(data);//头插newnode->next = _table[hashi];_table[hashi] = newnode;_n++;return make_pair(interator(newnode,this),true);}iterator Find(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kot(cur->_data) == key){return iterator(cur,this);}cur = cur->next;}return end();}bool Erase(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (kot(cur->data) == key){if (prev == nullptr)//如果删除的是第一个{_table[hashi] = cur->next;}else//如果删除的是中间的{prev->next = cur->next;}delete cur;return true;}else{prev = cur;cur = cur->next;}}return false;}private:vector<Node*> _table;size_t _n;};
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/842553.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法

PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法 吃鸡热潮依旧绝地求生PUBG可是咱们玩家的心头好啊&#xff01;不过有时候可能会遇到点小麻烦&#xff0c;比如PUBG绝地求生卡在初始界面 登不上去 打不开游戏的解决办法。小编这就给大家分享几个超实用的解决方法&…

LDRA Testbed(TBrun)软件单元测试_操作指南

系列文章目录 LDRA Testbed软件静态分析_操作指南 LDRA Testbed软件静态分析_自动提取静态分析数据生成文档 LDRA Testbed软件静态分析_Jenkins持续集成_(1)自动进行静态分析的环境搭建 LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果 LDRA Testb…

YOLOv10来了

B站&#xff1a;啥都会一点的研究生公众号&#xff1a;啥都会一点的研究生 前言 YOLOv10 由清华大学研究人员在 Ultralytics版基础上进行进一步开发&#xff0c;引入了一种新的实时目标检测方法&#xff0c;解决了以前版本 YOLO 在后处理和模型架构方面的不足。通过消除非最大…

[8] CUDA之向量点乘和矩阵乘法

CUDA之向量点乘和矩阵乘法 计算类似矩阵乘法的数学运算 1. 向量点乘 两个向量点乘运算定义如下&#xff1a; #真正的向量可能很长&#xff0c;两个向量里边可能有多个元素 (X1,Y1,Z1) * (Y1,Y2,Y3) X1Y1 X2Y2 X3Y3这种原始输入是两个数组而输出却缩减为一个(单一值)的运…

linux 查看csv文件,按指定列聚合 排序

在Linux中&#xff0c;你可以使用awk工具来查看CSV文件的内容&#xff0c;并按照指定的列进行聚合。awk是一种强大的文本处理工具&#xff0c;它可以处理文本文件中的数据&#xff0c;并根据条件执行相应的操作。 以下是一个示例&#xff0c;假设你有一个名为data.csv的CSV文件…

单点登录(JWT实现)

单点登陆的英文名是&#xff1a;Single Sign On&#xff08;简称SSO&#xff09;&#xff0c;只需要登陆一次&#xff0c;就可以访问所有信任的应用系统。 在单体项目中&#xff0c;我们登陆之后可以把验证用户信息的值放入session中&#xff0c;单个tomcat中的session是可以共…

C++ 数据结构算法 学习笔记(33) -查找算法及企业级应用

C 数据结构算法 学习笔记(33) -查找算法及企业级应用 数组和索引 日常生活中&#xff0c;我们经常会在电话号码簿中查阅“某人”的电话号码&#xff0c;按姓查询或者按字母排 序查询&#xff1b;在字典中查阅“某个词”的读音和含义等等。在这里&#xff0c;“电话号码簿”和…

【FPGA】Verilog:2-bit 二进制比较器的实现(2-bit binary comparator)

解释 2-bit 二进制比较器仿真结果及过程说明(包括真值表和卡诺图) 真值表和卡洛图如下: 2-bit Binary Comparator A1 A2 B1

写好的文章怎样联系媒体投稿?

作为单位信息宣传的桥梁,我肩负着将单位的每一次活动、每一项成就转化为社会认可与赞美的重任。初涉此职,我满腔热血,以为凭借扎实的文字功底与不懈的努力,便能在各大媒体平台上为单位赢得一席之地。然而,现实很快就给了我一记响亮的耳光。 我最初采取的是最直接的方式——邮箱…

QT 使用QLsitView 实现多个子项选中取消效果

文章目录 效果图概述部分代码总结 效果图 概述 整个界面的布局介绍请看这篇博客想要的到这种自由选择中的Item效果&#xff0c;需要使用到Model-view的思想&#xff0c;每个item中都要存放一个标志位&#xff0c;用在Paint函数去判断是否绘制为按下的状态。每次item被点击时&a…

记录下所遇到远程桌面连接方法winSCP跟mstsc

之前公司使用过连接远程桌面&#xff0c;今天又遇到要使用远程桌面问题&#xff0c;来记录下。 之前公司使用的是winR 然后回车弹出 后面按照用户名密码就能登陆了 今天后台给了我一张图片准备接着用这个方法&#xff0c;后台就说这个东西要下载winSCP 后台发给我图片 然后去…

[笔试强训day09]

文章目录 BC146 添加逗号DP2 跳台阶JZ61 扑克牌顺子解法一&#xff1a;排序模拟解法二&#xff1a;规律哈希 BC146 添加逗号 BC146 添加逗号 #include<iostream> #include<string>using namespace std;int main() {string s;cin>>s;string ans;for(int i0;i…

LLaMa系列模型详解(原理介绍、代码解读):LLaMA 2

LLaMA 2 大型语言模型&#xff08;LLMs&#xff09;作为高度能力的人工智能助手&#xff0c;在需要跨多个领域专家知识的复杂推理任务中表现出巨大潜力&#xff0c;包括编程和创意写作等专业领域。它们通过直观的聊天界面与人类互动&#xff0c;这导致了快速和广泛的公众采用。…

军工单位如何做到安全跨网文件交换与导出的

在现代信息化战争中&#xff0c;军工单位在信息安全方面的需求尤为突出。跨网文件交换与导出作为军工单位日常运营的重要环节&#xff0c;面临着网络带宽限制、数据安全风险、合规性要求和传输稳定性等挑战。下面&#xff0c;我们将从以下几个方面探讨军工单位如何实现安全、高…

【C++】开源:RabbitMQ安装与配置使用(SimpleAmqpClient)

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&#x1…

用队列实现栈 用栈实现队列 设计循环队列

用队列实现栈 思路 栈的特点&#xff1a;后进先出 队列的特点&#xff1a;先进先出 使用两个队列实现栈&#xff1a; 我们可以使用两个队列&#xff0c;一个队列为&#xff1a;空队列&#xff0c;一个队列为&#xff1a;非空队列 当我们要出队列时&#xff1a; 将 size - …

多线程JUC 第2季 BlockingQueue 阻塞队列

一 阻塞队列 1.1 阻塞队列介绍 阻塞队列&#xff08;BlockingQueue&#xff09;是一个在队列基础上又支持了两个附加操作的队列&#xff1a; put方法&#xff1a;当队列装满时&#xff0c;添加的线程则被阻塞&#xff0c;直到队列不满&#xff0c;则可用。 take方法&#x…

力扣2028. 找出缺失的观测数据

题目&#xff1a; 现有一份 n m 次投掷单个 六面 骰子的观测数据&#xff0c;骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份&#xff0c;你手上只拿到剩余 m 次投掷的数据。幸好你有之前计算过的这 n m 次投掷数据的 平均值 。 给你一个长度为 m 的整数数组 rolls &a…

【Linux 网络】网络基础(三)(数据链路层协议:以太网协议、ARP 协议)

一、以太网 两个不同局域网的主机传递数据并不是直接传递的&#xff0c;而是通过路由器 “一跳一跳” 的传递过去。 跨网络传输的本质&#xff1a;由无数个局域网&#xff08;子网&#xff09;转发的结果。 所以&#xff0c;要理解数据跨网络转发原理就要先理解一个局域网中数…

安全厂商第一站!OASA 走进绿盟科技圆满结束

近日&#xff0c;龙蜥社区安全联盟&#xff08;OASA&#xff09;走进联盟成员单位绿盟科技集团股份有限公司&#xff08;以下简称“绿盟科技”&#xff09;&#xff0c;就未来合作方向&#xff0c;双方进行了一次深入的合作交流。该会议共有 11 位人员出席&#xff0c;有来自绿…