文章目录

  • 一、哈希表
  • 二、哈希函数
    • 2.1. 直接定址法(常用)
    • 2.2. 除留余数法(常用)
    • 2.3. 几种不常用的方法
  • 三、哈希冲突
  • 四、闭散列
    • 4.1. 线性探测
    • 4.2. 负载因子
    • 4.3. 二次探测
    • 4.4. 插入和删除操作
    • 4.5. 扩容操作
    • 4.6. 代码实现
  • 五、开散列(哈希桶)
    • 5.1. 开散列扩容
    • 5.2. 代码实现
  • 六、闭散列和开散列的比较

一、哈希表

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,通过这种方式,可以不经过任何比较,一次直接从表中得到要搜索的元素,即查找的时间复杂度为 O(1)。
这个映射函数叫做哈希(散列)函数,存放记录的数组叫做哈希(散列)表。

最典型的例子是计数排序,将要排序的数组每个元素和新开辟的数组的下标进行映射。

向哈希表中插入和搜索元素的过程如下:
先用哈希函数将被要插入或查找的键值转化为数组的一个索引。

  • 插入元素: 根据索引位置,将元素存放到此位置。
  • 搜索元素: 根据索引,找到存储在该索引位置的元素。

在理想情况下,不同的键都能转化为不同的索引值。当然,这只是理想情况,所以我们需要面对两个或者多个键都会散列到相同的索引值的情况。这就是哈希冲突的问题。

二、哈希函数

2.1. 直接定址法(常用)

取关键字的某个线性函数作为散列地址:hash(key)=A*key + B
优点:简单、均匀
缺点:实现需要知道关键字的分布情况,并且只适合查找比较小,且连续分布的情况
适用场景:查找字符串中,第一次出现的单词:构建一个数组 hash[ch-‘a’] 即为对应的地址
不适用场景:给一批数据, 1 5 8 100000 像这数据跨度大,数据元素不连续,很容易造成空间浪费

2.2. 除留余数法(常用)

设散列表中允许的地址数为m,通常是取一个不大于m,但是最接近或者等于m的质数num,作为除数,按照哈希函数进行计算hash(key)= key%num, 将关键码转换成哈希地址
优点:使用场景广泛,不受限制。
缺点:存在哈希冲突,需要解决哈希冲突,哈希冲突越多,效率下降越厉害。

2.3. 几种不常用的方法

  1. 平方取中法

hash(key)=key*key 然后取函数返回值的中间的几位,作为哈希地址
比如 25^2 = 625 取中间的一位 2 作为哈希地址

比较适合不知道关键字的分布,而位数又不是很大的情况

  1. 折叠法

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

适用于不知道关键字分布,关键字位数比较多的情况

  1. 随机数法

选取一个随机函数,取关键字的随机函数值,作为它的哈希地址,hash(key) = random(key),random为随机函数

通常用于关键字长度不等的情况

  1. 数学分析法

通过实现分析关键字,来获取哈希地址

比如用每个人的手机号码充当关键字,如果采用前三位作为哈希地址,那么冲突的概率是非常大的。如果采用的是中间3位那么冲突的概率要小很多

常用于处理关键字位数比较大的情况,且事前知道关键字的分布和关键字的若干位的分布情况


三、哈希冲突

不同关键字通过相同哈希函数计算出相同的哈希映射地址,这种现象称为哈希冲突或哈希碰撞。

解决哈希冲突通常有两种方法:闭散列(开放地址发)和开散列(链地址法)。


四、闭散列

闭散列也叫做开放地址法,当发生哈希冲突的时候,如果哈希表未被填满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的"下一个"空位置中去,寻找下一个空位置的方法有线性探测法和二次探测法

4.1. 线性探测

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

优点:实现非常简单
缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据"堆积",即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要进行多次比较,导致搜索效率降低。

4.2. 负载因子

随着哈希表中数据的增多,产生哈希冲突的可能性也随着增加。
我们将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。介于此,哈希表当中引入了负载因子(载荷因子):

散列表的载荷因子定义为 α = 填入表中的元素 / 散列表的长度

α是散列表装满程度的标志因子,α越大表明装入表中的元素越多,产生冲突的可能性也就越大,反之填入表中的元素越少,冲突可能性越低,空间利用率也就越低

闭散列:一般将载荷因子控制在 0.7-0.8以下,超过0.8查表时候的缓存不中率会按照指数曲线上升(哈希可能性冲突越大),因此一般hash库中,都将α设置在0.8以下。 闭散列,千万不能为满,否则在插入的时候会陷入死循环

开散列/哈希桶:一般将载荷因子控制在1。超过1,那么链表就挂得越长,效率也就越低

4.3. 二次探测

线性探测的缺陷是产生哈希冲突,容易导致冲突的数据堆积在一起,这是因为线性探测是逐个的找下一个空位置.
二次探测为了缓解这种问题(不是解决),对下一个空位置的查找进行了改进(跳跃式查找):
POS = (H+i2)%m
其中:i=1、2、3…
H是发生哈希冲突的位置
m是哈希表的大小

4.4. 插入和删除操作

插入操作比较简单:通过哈希函数插入元素在哈希表中的位置,如果发生了哈希冲突,则使用线性探测或二次探测寻找下一个空位置插入元素。
但是删除操作比较麻烦,采用闭散列处理哈希冲突时,不能随便删除哈希表中已有的元素,如果直接删除元素,会影响其他元素的搜索(比如原来下标为40的元素因为前面删除了一个元素下标变成了39)。
因此线性探测采用标记的伪删除法来删除下一个元素。

4.5. 扩容操作

为了减少冲突,哈希表的大小最好是素数。为了能够获取每次增容后的大小,将需要用到的素数序列提前用一个数组存储起来,当我们需要增容时就从该数组当中进行获取。

4.6. 代码实现

namespace CloseHash//闭散列
{enum State{EMPTY,EXIT,DELETE};template<class K,class V>struct  HashData{pair<K, V> _kv;State _state=EMPTY;//节点的状态默认为空};template<class K>struct  Hash{size_t operator()(const K& key){return key;}};template<>struct  Hash<string>{size_t operator()(const string& s){size_t value = 0;for (auto ch : s){value += ch;value *= 131;}return value;}};template<class K, class V,class HashFunc=Hash<K>>struct  HashTable{public:size_t GetNextPrime(size_t prime){static const int PRIMECOUNT = 28;//给成静态,不用重复生成static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 429496729ul};size_t i = 0;for (; i < PRIMECOUNT; ++i) {if (primeList[i] > prime)return primeList[i];}return primeList[i];}bool Insert(const pair<K, V>& kv){HashData<K, V>* ret = Find(kv.first);if (ret) //哈希表中已经存在该键值的键值对(不允许数据冗余){return false; //插入失败}//进行扩容检测if (_n == 0 || (_n / _table.size() * 10 > 7))//当前个数为0或者载荷因子超过了,则进行扩容{//size_t newsize = _size == 0 ? 10 : 2 * _tables.size();//初始化给10,后续扩容两倍//选取素数size_t newsize = GetNextPrime(_table.size());//扩容之后,需要重新计算元素的位置HashTable<K, V, HashFunc> newHT;newHT._table.resize(newsize);for (auto& e : _table){if (e._state == EXIT)newHT.Insert(e._kv);}_table.swap(newHT._table);//进行交换}HashFunc hf;size_t start= hf(kv.first) % _table.size();size_t index = start;//探测后面的位置,线性探测或二次探测size_t i = 1;while (_table[index]._state == EXIT){index =start+i;index %= _table.size();++i;}_table[index]._kv = kv;_table[index]._state = EXIT;++_n;return true;}HashData<K,V>* Find(const K& key){if (_table.size() == 0) return nullptr;HashFunc hf;size_t start = hf(key) % _table.size();size_t index = start;size_t i = 1;while (_table[index]._state != EMPTY){if (_table[index]._state== EXIT&&_table[index]._kv.first == key){return &_table[index];}index = start + i;index %= _table.size();++i;}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;return true;}}private:vector<HashData<K,V>> _table;size_t _n = 0;};
}

五、开散列(哈希桶)

开散列又名哈希桶/开链法,首先对关键码集合采用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表串联起来,各个链表的头节点存储在哈希表中

5.1. 开散列扩容

与闭散列不同,开散列需要负载因子达到1的时候才进行扩容。
因为在理想的情况下每个桶下面只有一个节点。哈希桶的载荷因子控制在1,当大于1的时候就进行扩容,这样平均下来,每个桶下面只有一个节点;

与闭散列进行比较: 看起来哈希桶之中存储节点的指针开销比较大,其实不然。闭散列的负载因子需要保证小于0.7,来确保有足够的空间降低哈希冲突的概率,而表项的空间消耗远远高于指针所占的空间效率,因此哈希桶更能节省空间。

5.2. 代码实现

namespace OpenHash//开散列
{template<class K,class V>struct HashNode{HashNode<K, V>*_next;pair<K, V>_kv;HashNode(const pair<K, V>& kv):_next(nullptr),_kv(kv){} };template<class K>struct  Hash{size_t operator()(const K& key){return key;}};template<>struct  Hash<string>{size_t operator()(const string& s){size_t value = 0;for (auto ch : s){value += ch;value *= 131;}return value;}};template<class K, class V, class HashFunc = Hash<K>>struct HashTable{public:typedef HashNode<K, V> Node;Node* Find(const K& key){HashFunc hf;if (_table.size() == 0) return nullptr;size_t index = hf(key) % _table.size();Node* cur = _table[index];//在桶中进行查找while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}return nullptr;}bool Insert(const pair<K, V>& kv){HashFunc hf;if (Find(kv.first)) return false;//列表里已经存在kv//负载因子到1时进行增容if (_n == _table.size()){vector<Node*>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){//记录原来cur后面的节点Node* next = cur->_next;size_t index = hf(cur->_kv.first) % newtable.size();//头插cur->_next = _table[index];_table[index] = cur;cur = next;}_table[i] = nullptr;}}_table.swap(newtable);}size_t index = hf(kv.first) % _table.size();Node* newnode = new Node(kv);newnode->_next = _table[index];_table[index] = newnode;++_n;}bool Erase(const K& key){HashFunc hf;size_t index = hf(key) % _table.size();Node* prev = nullptr;Node* cur = _table[index];//在桶中找到对应的节点进行删除while (cur){if (cur->_kv.first == key){//头结点的情况if (_table[index] == cur){_table[index] = cur->_next;}else{prev->_next = cur->_next;}--_n;delete cur;return true;}prev = cur;cur = cur->_next;}//要删的节点不存在return false;}private:vector<Node*> _table;size_t _n=0;//有效数据的个数};
}

六、闭散列和开散列的比较

  1. 开散列处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 由于开散列中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  3. 闭散列为减少冲突,要求负载因子α较小,故当结点规模较大时会浪费很多空间。而开散列中可取α≥1,且结点较大时,开散列中增加的指针域可忽略不计,因此节省空间;
  4. 在用开散列构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对闭散列构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。实际删除操作待表格重新整理时在进行,这种方法也被称为惰性删除。

资料参考:
哈希表、哈希桶的实现
哈希表底层探索
数据结构之(一)Hash(散列)
哈希之开散列,闭散列

14 哈希表和哈希桶相关推荐

  1. 哈希表及哈希函数研究综述

    哈希表及哈希函数研究综述 摘要 随着信息化水平的不断提高,数据已经取代计算成为了信息计算的中心,对存储的需求不断提高信息量呈现爆炸式增长趋势,存储已经成为急需提高的瓶颈.哈希表作为海量信息存储的有效方 ...

  2. 哈希表及哈希冲突解决办法

    哈希表及哈希冲突解决办法 目录 什么是哈希表? 哈希表的数据结构 哈希冲突 哈希冲突解决办法 1. 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直 ...

  3. ds哈希查找—二次探测再散列_大白话之哈希表和哈希算法

    哈希表概念 哈希表(散列表),是基于关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数(哈希函数 ...

  4. 【数据结构】哈希表、哈希值计算分析

    哈希表.哈希值计算分析 哈希表完整代码 引出哈希表 哈希表(Hash Table) 哈希冲突(Hash Collision) JDK1.8的哈希冲突解决方案 哈希函数 如何生成 key 的哈希值 In ...

  5. 大白话之哈希表和哈希算法

    哈希表概念 哈希表(散列表),是基于关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数(哈希函数 ...

  6. LeetCode哈希表(哈希集合,哈希映射)

    文章目录 哈希表 1.原理 2.复杂度分析 题目&推荐列表 哈希集合的应用 0.常用解题模板 1.lc217 存在重复元素 2.lc136 只出现一次的数字 3.快乐数 哈希映射的应用 0.常 ...

  7. 哈希表(哈希函数和处理哈希冲突)_20230528

    哈希表(哈希函数和处理哈希冲突) 前言 关于哈希表的主题的小记原计划5月23日完成,由于本人新冠阳性,身体发烧乏力,周末感觉身体状况稍加恢复,赶紧打开电脑把本文完成,特别秉承"写是为了更好地 ...

  8. python 哈希表_哈希表哪家强?编程语言找你来帮忙!

    点击关注上方"五分钟学算法", 设为"置顶或星标",第一时间送达干货. 转自编程技术宇宙 哈希表华山论剑 比特宇宙编程语言联合委员会准备举办一次大会,主题为哈希 ...

  9. PAT甲级1145 Hashing - Average Search Time:[C++题解]哈希表、哈希表开放寻址法、二次探测法、求平均查找次数

    文章目录 题目分析 题目链接 题目分析 来源:acwing 本题的分析见另一道PAT的题目:PAT甲级1078 Hashing:[C++题解]哈希表.哈希表开放寻址法.二次探测法链接的题目就是让建立h ...

最新文章

  1. Win7 下安装VirtualBox 没有Ubuntu 64bit 选项问题
  2. 全面降低windows系统的安全隐患 (五)
  3. Algorithms_算法专项_Bitmap原理及应用
  4. MySql查询系统时间,SQLServer查询系统时间,Oracle查询系统时间
  5. JS 防止表单重复提交
  6. java(17) - 增强for循环、装箱拆箱、可变参数
  7. 论文笔记_S2D.11-2018-ECCV_用于语义分割和深度估计的联合任务递归学习
  8. 软件测试中的缺陷分析与管理
  9. 网上银行“安全登录控件”分析
  10. python实现计算标准偏差函数
  11. mysqldump set-gtid-purged=OFF的问题
  12. 最新《择善教育》C/C++黑客编程项目实战教程
  13. 隐式差分方程c语言,科学网—显式与隐式差分 - 张凌的博文
  14. [GUI] 图片背景是黑色的,在unity中怎么变成透明
  15. 数学-向量公式总结和一些公式证明
  16. 2022年上海应届生落户流程及步骤!应届生落户上海材料清单!
  17. 按行遍历和按列遍历哪一个更快一些?
  18. 毕业于清华的90后学霸,即将加入MIT任助理教授
  19. 报错 -bash: bc: command not found
  20. 牛顿法 拟牛顿法DFP BFGS L-BFGS的理解

热门文章

  1. 人的一生应该有三段婚姻
  2. MapboxGL——marker轨迹动画效果,自定义速度。暂停继续轨迹运动
  3. 共轭梯度法matlab代码博客园,共轭梯度法源代码.doc
  4. git如何清空历史commit?
  5. 接口设计之幂等性设计
  6. 网上悬赏帆船捉“熊猫烧香”病毒制造者
  7. RabbitMq第三种模型--fanout
  8. 如何查看OutputDebugString的输出?[英]How to view output of OutputDebugString?
  9. bitblt 与 stretchblt 的使用区别
  10. 五大技巧识别钓鱼网站