c++ map 获取key列表_14.散列表(上)

本文介绍了散列表的概念、散列函数的设计原则以及解决散列冲突的开放寻址法(线性探测、二次探测、双重散列)和链表法。通过实例解析了开放寻址法和链表法的优缺点,并探讨了在实际应用中的场景,如共同好友查询的MapReduce模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开篇问题:

在使用qq或者抖音时候,会出现有多少个共同好友或者共同关注的人(单向好友),通过本篇讲述如何实现共同好友的查询。

散列表

散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性。散列表两个核心问题是散列函数设计散列冲突解决。散列冲突有两种常用的解决方法,开放寻址法和链表法。散列函数设计的好坏决定了散列冲突的概率,也就决定散列表的性能。

散列表的英文叫“Hash Table”,也叫它“哈希表”或者“Hash 表”,散列表用的是数组支持按照下标随机访问数据的特性,是数组的一种扩展。

散列表利用了数组按照下标随机访问的时候时间复杂度是 O(1) 的特性。通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当按照键值查询元素时,用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。

699ba0acab3dce2b53da4ced58da242b.png

散列函数

散列函数,可以定义成hash(key),其中 key 表示元素的键值,hash(key) 的值表示经过散列函数计算得到的散列值。

散列函数,顾名思义,它是一个函数。我们可以把它定义成hash(key),其中 key 表示元素的键值,hash(key) 的值表示经过散列函数计算得到的散列值。

散列函数设计的三点基本要求:

  1. 散列函数计算得到的散列值是一个非负整数;
  2. 如果 key1 = key2,那 hash(key1) == hash(key2);
  3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

对于第一点,因为数组下标是从 0 开始的,所以散列函数生成的散列值也要是非负整数。

对于第二点,相同的 key,经过散列函数得到的散列值也应该是相同的。

对于第三点,几乎无法找到一个完美的无冲突的散列函数,即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。针对散列冲突问题,需要通过其他途径来解决。

解决散列冲突问题的两种方法

常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。

1. 开放寻址法

开放寻址法的核心思想是,如果出现了散列冲突,就重新探测一个空闲位置,将其插入。

探测新的位置的方法有线性探测(Linear Probing)、二次探测(Quadratic probing)和双重散列(Double hashing)

线性探测:

往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

下图黄色的色块表示空闲位置,橙色的色块表示已经存储了数据。

67ff33f2d41a8799e91c68b8bcbbecc6.png

图中散列表的大小为 10,在元素 x 插入散列表之前,已经 6 个元素插入到散列表中。x 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。于是我顺序地往后一个一个找,遍历到尾部都没有找到空闲的位置,于是我们再从表头开始找,直到找到空闲位置 2,于是将其插入到这个位置。

在散列表中查找元素,会先通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。

7c13520375c4086885bc18de27319b96.png

使用线性探测法解决冲突的散列表,对于删除操作,会将被删除的元素特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测。

eba26a93cac8b7bb0edf1f8bc000269f.png

线性探测法存在的问题:

当散列表中插入的数据越来越多时,散列冲突发生的可能性越来越大,空闲位置越来越少,线性探测的时间越来越久。极端情况下,可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。同理,在删除和查找时,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据。

二次探测:

线性探测每次探测的步长是 1,它探测的下标序列是 hash(key)+0,hash(key)+1,hash(key)+2……而二次探测探测的步长就变成了原来的“二次方”,也就是说,它探测的下标序列就是 hash(key)+0,hash(key)+1^2,hash(key)+2^2……

双重散列:

并不只用一个散列函数,而是同时使用一组散列函数 $hash1(key),hash2(key),hash3(key)……$先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

2. 链表法

在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。

e929e8e8f5ec734f0f96388b6eba8cd9.png

插入的时候只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。当查找、删除一个元素时,同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。

查找或删除操作的时间复杂度跟链表的长度 k 成正比,即 O(k)。对于散列比较均匀的散列函数来说,理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。

解决开篇问题:

解释什么是MapReduce:

  • 要想实现该功能必须先了解大数据中的MapReduce模型,MapReduce底层是由散列表所构成,Map(映射),Reduce(归纳)。
  • Map:举个简单例子来解释mapreduce, 就把它想成是一个映射函数,比如考试的成绩表想成是一张map,此时你同学发现你的成绩上多了十分,此时老师就定义一个"减十"的映射函数,来修正这个错误,此时成绩发生了变化,那么就要创建一张新的成绩表。此时能看出,Map操作是可以同时并发进行的,对高性能要求的应用以及并行计算领域有重要的用处
  • Reduce:归纳操作,无疑是把列表中的元素进行合并,还是接着上面的例子,如果说此时老师想看班级的平均分,需要先获取那张成绩表,使用归纳函数进行计算,要想计算平均分还得将这些成绩相加起来(实际上不是这么计算的),实际上是通过odd 和 enev 元素跟自己的相邻的元素相加的方式把列表减半,如此递归运算直到列表只剩下一个元素,然后用这个元素除以人数,就得到了平均分)。

实验验证过程:

1.获取样本数据(虚拟数据):

注:在这已26个大写字母代表一个好友,对共同好友进行虚拟模拟实验环境

数据已散列表存储 本人 : 好友(例如,A:B,C,D,F,E,O)

A:B,C,D,F,E,O

2.实验思路:

  • 要想求出共同好友,样本信息是以散列表形式存储的,在python没有散列表这个说法,因为python属于高级语言,已经有封装好的字典来实现散列表
  • 、使用大数据技术MapReduce模型,将好友设置为键(key),人设置为值(value),最后提交任务给reduce去合并

3.实验方法:

  • 通过两次迭代,找出那些是共同好友
  • 把这些人(用共同好友的人)最为key,其好友作为value输出

4.操作流程图

代码示例:

# coding=utf-8

运行结果

9700dc380a21f2f38e04d38fa851960a.png
A-B C,E
A-C D,F
A-D F,E
A-E B,C,D
A-F B,C,D,E,O
A-G C,D,F,E
A-H C,D,E,O
A-I O
A-J B,O
A-K C,D
A-L D,F,E
A-M F,E
B-C A
B-D E,A
B-E C
B-F C,E,A
B-G C,E,A
B-H C,E,A
B-I A
B-K C,A
B-L E
B-M E
B-O A
C-D F,A
C-E D
C-F D,A
C-G D,F,A
C-H D,A
C-I A
C-K D,A
C-L D,F
C-M F
C-O A,I
D-E L
D-F E,A

思考题

1.假设我们有 10 万条 URL 访问日志,如何按照访问次数给 URL 排序?

解决思路:

遍历 10 万条数据,以 URL 为 key,访问次数为 value,存入散列表,同时记录下访问次数的最大值 K,时间复杂度 O(N)。

如果 K 不是很大,可以使用桶排序,时间复杂度 O(N)。如果 K 非常大(比如大于 10 万),就使用一般的排序方法,复杂度 O(NlogN)。

模拟实现代码:

url_count_dict 

2.有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?

思路:

将两个字符串数组分别存入散列表中,遍历其中一个散列表,每个遍历出来的字符串都去另一个散列表中查找,若找到则说明该字符串为公共字符串。

python简易实现:

arr1 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值