对比之前博客讨论的二叉排序树 二叉平衡树 红黑树,它们的查找都是先从根节点进行查找,从节点取出数据或索引与查找值进行比较。那么,有没有一种函数H,根据这个函数和查找关键字key,可以直接确定查找值所在位置,而不需要一个个比较。这样就预先知道key所在的位置,直接找到数据,提升效率

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表

常见的哈希函数

一个哈希函数的好不好,取决于以下三点

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

除留余数法(最常用)
函数:Hash(key)=key MOD p (p<=m m为表长),求出来的hash(key)就为存储该key的下标

例如有一下数据{2, 4, 6, 8, 9}
表长为10,也就是数组容量为10

直接定制法(常用)
取关键字的某个线性函数为散列地址(A、B为常数):Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
适用场景:适合查找较小数据范围且连续的情况

平方取中法(少)
如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址。
使用举例
比如key=1234 1234^2=1522756 取227作hash地址
比如key=4321 4321^2=18671041 取671作hash地址
适用场景:事先不知道数据并且数据长度较小的情况

哈希冲突

即不同的key通过同一哈希函数产生了相同的哈希位置,H(key1)=H(key2),例如我们在除留余数法中的例子,如果此时插入一个12,其hash(12)为2,此时下标为2的位置已经有元素,此时就会产生哈希冲突

处理哈希冲突

解决哈希冲突主要有两个方案:闭散列开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那 么可以把key存放到冲突位置中的“下一个” 空位置中去

闭散列中主要处理方法有 线性探测二次探测

线性探测

思想:从计算的哈希位置开始,往后找到第一个空闲的位置存放数据

插入:插入就是计算哈希地址,将数据存放在计算出来的哈希位置上,如果该位置有数据则往后查找第一个空闲位置插入。但是当我们的元素越多时,我们产生的哈希冲突的次数就会越多,

删除:当我们要删除一个元素时,不能物理上直接删除,例如我们把15删除了,此时下标为8的位置为空,当我们要查找25这个元素时,也是会从下标为5这个位置开始查找,当5这个位置不是25时,说明产生了哈希冲突,且该插入是使用的是线性探测,也就是第一个空位置插入。我们往后查找时,如果该数据存在,则在空位置之前一定存在该数。但是此时我们物理上把15删除了。查找会查找到下标为8的位置就结束查找,此时也就不会找到25这个数据了。

所以使用线性探测方法,删除并不是实际意义上的删除,而是一种伪删除,我们可以定义三种状态,分别是:EMPTYEXISTDELETE。EMPTY表示该位置从来没存放过数据,是一个空位置;EXIST表示该位置存在数据;DELETE表示该位置之前存放过数据,只是已经删除了而已

此时我们想要删除8这个位置上的数据时,就将该位置的状态置为DELETE,我们再次查找25这个数字时,遇到8位置就不会停止搜索,会继续往后搜索,直至遇到状态为EMPTY的位置为止。但是次方法会造成一个问题,就是有可能数据满了,如果此时还一直搜索,就不会找到空的位置,会一直搜索下去。而且如果数据比较极端且数据越来越多,产生的哈希冲突会越来越多。这就不符合我们的哈希要求的高效率的插入与查找。解决办法就是进行扩容

扩容:扩容并不是一定要等到数据满了才扩容。我们知道当数据越来越多,产生哈希冲突的次数就越多,所以我们要设定一个阈值,也就是当数据达到一定的数量时,就有必要进行扩容。而这决定这个阈值的高低的是一个叫负载因子。负载因子 = 实际存放元素 / 数组容量,范围在0~1之间,我们通常将负载因子置为[0.6, 0.8]之间。例如我们数组大小有10个,负载因为为0.7,则当插入第8个元素的时候就需要进行扩容,因为8/10=0.8>.07,也就是大于我们的负载因子就需要进行扩容。扩容的时候要注意,我们需要将原来的数据移动到新的表中,但是如果是单纯的赋值获取,那哈希冲突并没有解决,而此时我们应该将旧表中的数据重新以插入的方式插入到新的表中,从而减少哈希冲突的次数

查找:哈希的优势之一也就是在于查找,查找的效率是非常高,只要通过key来计算哈希地址,如果计算的哈希位置的值与要查找的key相同,则表示已经找到,如果遇到空之后还没找到表示不存在这个要查找的数。并且要注意的是,如果查找到末尾就需要将查找的索引置为0,从头开始查找

二次探测

二次探测和线性探测都属于闭散列,其原理都一样,两者的主要区别就是探测的方式不同,线性探测是如果要插入的位置已有元素,会一个一个往后查找到新的空位置。而二次探测是通过该位置的哈希冲突次数的平方来向后查找新的位置

将产生哈希冲突的数据分散,使不堆积在一起。但是这两种方法都有很大的缺陷,就是空间利用率低。在这个基础上,引进一种新的技术来解决哈希冲突----开散列

开散列

开散列方法又叫链地址法,哈希表中存储的是链表的头结点。具有相同的哈希地址会存放在同一链表中,每个链表中的元素都具有相同的哈希地址。

该哈希表示由指针数组来组成的,每个数组中的元素都是一个链表的头指针。从该表中我们可以看出,产生哈希冲突的元素并不会占用其他元素的位置,每个链表中的元素都是哈希冲突的元素

插入:当我们插入时,计算出哈希地址,就可以直接定位到该key对应的链表的头结点,但是由于不能存放相同的key,我们需要遍历该链表中是否存在相同元素,如果不存在才进行插入。插入时我们可以进行头插或者尾插,这里头插会更简单些,创建key的一个新的结点cur,让该结点指向该链表的头结点,并将该链表的头结点更新为新创建的新结点cur

增容:开散列的增容并不像闭散列一样要求那么严格,虽然也是有个负载因子,但是这个负载因子可以为1,也就是当有10个元素,负载因子为1时。此时10个元素都没有产生哈希冲突,这效率才是最高的,所以我们没必要限制它的上限。也就是当元素比哈希表的元素大时才需要进行扩容,来减少产生哈希冲突。哈希表中每个元素都是一个链表的头结点,我们可以创建新的一个指针数组,遍历旧的哈希表,只要遍历的链表头结点不为空,就定义一个cur,用来遍历这个单链表,遍历单链表的也需要记录该当前节点的下一个结点,否则会下一个结点会被丢弃。我们在当前节点cur中计算它的哈希地址,然后在新的指针数组中的指定位置进行头插。每遍历完一个单链表,都需要将旧的链表的头结点置为空,因为后面我们需要交换这两个指针数组,然后释放掉旧的指针数组。

查找:查找一个元素,我们可以先计算出要查找元素的哈希地址,直接定位到指定的链表的头结点,然后遍历该条链表,如果当前节点的值和要查找的元素的值相同,则表示查找,返回所找到的结点,如果没有找到则返回空
删除:这里的删除是实际上的删除,我们可以先通过查找,查看要删除的值是否存在哈希表中,如果不存直接返回false,存在则需要先计算当前删除的结点的所在链表的哈希地址,找到后遍历该链表,并用一个prev结点记录要删除的前一个结点,当遍历找到我们删除的结点时,要先判断该节点是否为链表的头结点,如果为头结点,则将要删除的结点的下一个结点置为头结点,如果不是头结点则将记录的前结点prev的下一个结点的next置为要删除结点的下一个结点。最后将有效元素-1并删除要删除的结点

代码

代码:闭散列----线性探测

enum STATE
{EXIST,DELETE,EMPTY
};
//哈希表:线性探测解决哈希冲突
template<class K, class V>
struct HashNode
{pair<K, V> _kv;//数据STATE _state = EMPTY;//状态
};//顺序表实现哈希
template<class K, class V>
class HashTable
{public:typedef HashNode<K, V> Node;HashTable(size_t n = 10):_hTable(n),_size(0){}bool insert(const pair<K, V>& kv){//0.检查容量checkCapacity();//1.当前元素的哈希位置int idx = kv.first % _hTable.size();//2.判断key是否存在while (_hTable[idx]._state != EMPTY)//当前位置已有数据或者为删除位置,都不能存放{//当前位置存在数据且key相同,则不能插入if (_hTable[idx]._state == EXIST&& _hTable[idx]._kv.first == kv.first){return false;}//继续往后搜索空位置++idx;//走到末尾,需要从头开始找if (idx == _hTable.size())idx = 0;}_hTable[idx]._kv = kv;_hTable[idx]._state = EXIST;++_size;return true;}void checkCapacity(){//负载因子[0, 1],这里定负载因子为0.7if (_hTable.size() == 0 || _size * 10 / _hTable.size() >= 7){//创建新表int newC = _hTable.size() == 0 ? 10 : 2 * _hTable.size();HashTable<K, V> newHt(newC);for (size_t i = 0; i < _hTable.size(); ++i){//将原先的表的数据插入到新的表中,if (_hTable[i]._state == EXIST){newHt.insert(_hTable[i]._kv);}}//交换两个表的内容Swap(newHt);}}void Swap(HashTable<K, V>& Ht){swap(_hTable, Ht._hTable);swap(_size, Ht._size);}Node* find(const K& key){//计算位置int idx = key % _hTable.size();while (_hTable[idx]._state != EMPTY){//找到if (_hTable[idx]._state == EXIST&& key == _hTable[idx]._kv.first){return &_hTable[idx];}++idx;if (idx == _hTable.size())idx = 0;}//如果遇到空格则表示没找到,返回空return nullptr;}bool erase(const K& key){Node* node = find(key);if (node){//伪删除--_size;node->_state = DELETE;return true;}return false;}
private:vector<Node> _hTable;//表size_t _size;//有效元素个数
};

测试:

代码:开散列法

#include <vector>
#include <iostream>
using namespace std;template<class K>
struct HashNode
{typedef HashNode<K> Node;K _val;Node* _next;HashNode(const K& val):_val(val), _next(nullptr){}
};template<class K>
class HTable
{public:typedef HashNode<K> Node;HTable(int n = 10):_ht(n), _size(0){}bool insert(const K& val){//0.检查容量checkCapacity();//1.计算hash位置int idx = val % _ht.size();//2.查找Node* cur = _ht[idx];while (cur){if (cur->_val == val)return false;cur = cur->_next;}//3.插入--头插cur = new Node(val);cur->_next = _ht[idx];_ht[idx] = cur;++_size;return true;}void checkCapacity(){if (_size == _ht.size()){int newC = _size == 0 ? 10 : 2 * _size;//创建新的指针数组vector<Node*> newHt(newC);//遍历旧表for (size_t i = 0; i < _ht.size(); ++i){Node* cur = _ht[i];//遍历单链表while (cur){Node* next = cur->_next;//计算新的位置int idx = cur->_val % newHt.size();//头插cur->_next = newHt[idx];newHt[idx] = cur;cur = next;}//旧表指针置空_ht[i] = nullptr;}//交换新表和旧表swap(_ht, newHt);}}Node* find(const K& val){int idx = val % _ht.size();Node* cur = _ht[idx];while (cur){if (cur->_val == val)return cur;cur = cur->_next;}return nullptr;}bool erase(const K& val){Node* node = find(val);if (node){int idx = val % _ht.size();Node* cur = _ht[idx];Node* prev = nullptr;while (cur != node){prev = cur;cur = cur->_next;}Node* next = cur->_next;if (prev)prev->_next = next;else_ht[idx] = next;--_size;delete node;return true;}return false;}
private://指针数组vector<Node*> _ht;int _size;
};

测试:

数据结构 5分钟带你搞定哈希表(建议收藏)!!!相关推荐

  1. 【转载】一文彻底搞定哈希表!

    一文彻底搞定哈希表!非常风趣的讲解. https://blog.csdn.net/sinat_33921105/article/details/103344078

  2. 并联谐振电路工作原理详解,案例+计算公式,几分钟带你搞定

    昨天给大家分享了关于串联谐振的文章,今天给大家分享关于并联谐振的文章.(私信我的那个朋友,记得准备来看) 错过了串联谐振的朋友,可以直接点击下方标题跳转. 串联谐振是怎么工作的?案例+公式,几分钟,一 ...

  3. 达梦两个表模糊查询_一文彻底搞定哈希表

    哈希表是个啥? 小白: 庆哥,什么是哈希表?这个哈希好熟悉,记得好像有HashMap和HashTable之类的吧,这是一样的嘛? 庆哥: 这个哈希确实经常见,足以说明它是个使用非常频繁的玩意儿,而且像 ...

  4. 来吧!一文彻底搞定哈希表!

    哈希表是个啥? 小白:庆哥,什么是哈希表?这个哈希好熟悉,记得好像有HashMap和HashTable之类的吧,这是一样的嘛????? 庆哥: 这个哈希确实经常见????,足以说明它是个使用非常频繁的 ...

  5. GitHub标星1.3W!五分钟带你搞定Linux Bash脚本使用技巧

    原文:https://mp.weixin.qq.com/s/NnpyTOAghr_MXXK5H9twAg 来自:开源最前线(ID:OpenSourceTop)  综合自:https://leanpub ...

  6. GitHub星数1.3W!五分钟带你搞定Bash脚本使用技巧

    点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长 每天早上8点20分, 第一时间与你相约 每日英文 Always remember that in life. Whatev ...

  7. 这篇文章带你彻底搞定哈希表!

    哈希表是个啥? 小白:什么是哈希表?这个哈希好熟悉,记得好像有HashMap和HashTable之类的吧,这是一样的嘛?

  8. 深度学习基础之matplotlib,一文搞定各个示例,建议收藏以后参考

    目录 1.先来个demo 分析下 2.matplotlib中的概念 3.matplotlib 支持的图形 3.1 线图line 3.2 直方图hist

  9. RC振荡器工作原理分析,案例+公式,几分钟,带你搞定RC振荡器

    在前面我已经给大家介绍了三种振荡器,Colpitts 振荡器.皮尔斯振荡器.哈特利振荡器.如果错过的话,可以点击下方标题直接跳转. colpitts振荡器电路图分析,几分钟,立马搞定colpitts振 ...

最新文章

  1. ASP.NET MVC 导入Excel文件
  2. mongoengine.NotUniqueError
  3. chisel快速入门(二)
  4. php源码怎样安装mysql_安装MySQL
  5. css div滚动_如何使用CSS创建可垂直滚动的div?
  6. linux压缩隐藏文件,Linux下查看压缩文件内容的10种方法(小结)
  7. [BZOJ1572] [Usaco2009 Open]工作安排Job(贪心 + 堆)
  8. Linux下java进程CPU占用率高分析方法
  9. 梅特勒托利多xk3124电子秤说明书_梅特勒托利多电子秤校正标定方法
  10. 驱动开发笔记2—PAGED_CODE(),IRQL,分页内存
  11. 我收藏的thinkphp扩展插件
  12. 苹果手机在升级系统时一直显示无服务器,苹果手机一直显示检测更新,怎么办?...
  13. 广告行业中那些趣事系列21:从理论到实战BERT知识蒸馏
  14. HDU6438(优先队列+思维)
  15. SP许可证延期需要的东西
  16. UOJ#310 【UNR #2】黎明前的巧克力:FWT
  17. 华为运营商级路由器配置示例 | 配置HVPLS组网的VPLS PW Redundancy示例
  18. 用html做垃圾邮件1005无标题,垃圾邮件传播新型FTCode无文件勒索病毒
  19. flutter 记录 问题
  20. Windows下如何查看十几G的日志文件

热门文章

  1. 用doss进入mysql_一键连接给你省了多少事_DOSS 听吧_音频评测-中关村在线
  2. python中capital是什么意思_capital是什么意思_capital的翻译_音标_读音_用法_例句_爱词霸在线词典...
  3. ORACLE 正值表达式
  4. @RunWith@ContextConfiguration进行Spring上下文测试报错
  5. 基于JAVA+Swing+MYSQL的在线订餐管理系统
  6. 基于JAVA+SpringBoot+Vue+Mybatis+MYSQL的微信小程序点餐系统
  7. 字体编辑器_Mac系统常见的Birdfont字体编辑器
  8. 洛谷 P1067 多项式输出
  9. java框架----mybatis的使用(一)
  10. 矩阵分析与应用课后答案——张贤达版本