Fork me on GitHub

数据结构与算法之字典与集合

字典的解释

字典:就好像我们所知的新华字典、中英文字典等等,字典这个词的含义就是为字词提供解释,在数据结构中的字典给我们提供了什么的解释呢?我们去百度去搜索一下:

可以发现,我们在百度输入框中输入一段搜索文字,就能得到对应的结果。我们可以称百度是一个巨大的“字典”,它能给我们带来一段关键字的解释。在整个过程中,是(关键字->解释)这样的一个映射关系,这就是字典的基础结构,字典也称为(查找表、映射或关联表)常见的用法是(key->value)。在Python语言中dict就是一个典型的字典结构,Java语言中Map结构也是一个字典结构,可以通过Python中的dict或Java中的Map来学习和使用字典结构。

为什么需要字典?

在我们日常生活中,字典关系处处可见,比如上学时给每个同学的学号,工作时给每位同事的工号等,如果没有这样的一个关系映射,那么在查找时将会消耗多余时间,而这是难接受的一种现象。在计算机中,数据的存储与检索操作很频繁,迭代方式效率低,所以想到能不能在检索数据结构时,所提供的关键字(或关键码)作为下标,然后所需要得到的信息作为存储内容,(关键码->存储内容)这样的下标映射如果存在,那么我们通过关键码去拿信息将会是常量时间。

字典分类

静态字典:只用建立一次,建立之后,字典内容和结构都不再变化,主要操作是检索。
动态字典:在初始创建之后,字典的内容和结构将一直处于变动之中。除了检索操作之外,重要的还有数据项的插入和删除。

字典怎么实现?

从结构上来,字典就是key-value结构的汇集,可以使用Python中的一个tuple(二元组)来定义字典中元素的结构,或者定义一个结点类,里面有key或value两个属性来定义字典中元素的结构。字典可以通过其他一些数据结构作为基础来实现。比如顺序表。

顺序表实现字典

实现代码:略
实现总结:实现比较简单,就是一个数组,数组里的每个元素是一个二元组,[(key1, value1),(key2, value2)...],类似此种结构,检索时,只能通过给定的关键字来遍历数组然后对比key来查找元素,删除元素时也需要先遍历再删除并移动元素。检索、删除操作效率不高,但实现简单。

顺序表实现字典2(有序顺序表和二分查找)

以上通过顺序表来实现字典时,检索、删除不高的原因在于字典中的二元组存储是乱的,如果二元组按顺序存储,那么当我们需要定位某个元素(关键码)时,就可以通过二分查找的算法来快速定位。
实现代码:略
实现总结:通过有序的顺序表来实现字典,在检索时速度快,为O(logn)的时间复杂度。对于插入、删除元素时,检索到要插入位置与删除元素的位置时是O(logn)时间,但是插入元素后或删除元素后都要维护字典原有的结构,都需要去移动元素,所以时间还是O(n)。
顺序表实现字典适应场景:字典规模比较小,且不常进行动态变化的一些字典。

散列表实现字典

散列:将关键字key转换为存储数据的地址(下标)的方式叫做散列。此种转换函数称为散列函数或哈希(hash)函数。
前面提过,最快能访问到关键字对应的信息的方法就是将关键字作为地址下标,那么我们在获取数据的时候就是O(1)时间。而关键字作为地址下标的操作称之为散列。
散列技术的实现:我们将哈希函数记做h(),对于任何一个key都有对应的下标index=h(key),而对应的下标是我们已经选定的一块存储区域地址,比如0-19,对于大数据量的key,我们都需要将h(key)映射到(0-19)这个范围内。这是一种大范围到小范围的映射,肯定会产生不同的key映射到同一个下标的情况,这种情况称之为散列冲突:当一个散列表中负载因子越大,发生散列冲突的几率也越大,而且冲突具有不知何时发生的不确定性与随时都可能发生的确定性。所以必须要有处理散列冲突的方法。
散列函数的设计:散列函数的选择好坏决定于散列冲突发生的几率,散列函数选择需要遵循以下几点:
1、函数能将关键码映射到存储地址区间(值域index)中尽可能大的部分,也就是说散列函数最好能将存储地址区间index里的每个下标都能一一映射到,避免有的下标映射不上,浪费。
2、存储地址区间中的散列值应该均匀分布。
3、函数简单
对于整数的关键码若干散列方法:数字分析法(就是选取整数关键码的某些位置的数字作为下标),折叠法(将数字分为几段,并将它们进行运算,运算的结果去掉进位等)、中平方法(求出关键码的平方,然后取出中间的几位作为散列值)
常用散列函数:
1、除余法:将整数值除于存储地址区间index的范围值,得到的余数作为散列地址,比如一段整数值为5,7,11,44,将它们除以index范围值为10(index=[0-9]),得到的散列地址为5/10=5,7/10=7,11/10=1,44/10=4 => 5,7,1,4
2、基数转换法:将关键码看做r进制的数,然后将关键码转换为十进制或二进制
3、非整数的转换:先转换成整数,然后使用基数转换法或除余法进行得到散列地址
冲突消解机制
冲突的内消解:开地址技术:
内消解:在存储区的内部解决冲突问题。
开地址法:基本思想是:当遇到冲突时,为所冲突的元素再查找一个合适的位置,查找的过程称之为探查方式。
探查方式分为两种:
1、线性探查:当遇到冲突时,为冲突元素查找合适位置时,是一步步向前后向后探测,每次探测距离是线性的。
2、双散列探查:当遇到冲突时,为冲突元素查找合适位置时,再进行一次散列来确定新的位置。
冲突的外消解:链地址技术
外消解的方法有两种:
1、溢出区方法:另外设置一个溢出区,当发生冲突时将元素存储在溢出区,在溢出区里面的元素顺序存储,当在散列表中找不到元素时,就到溢出区中查找,但是一旦溢出区扩大,字典的性能将趋向于线性。
2、桶散列(链地址法、拉链法):就是在散列表中不直接存储关键码对应的数据,而是存储了另一个链表结构,链表结构是另一个表,里面存的是冲突关键码的所有结点。桶散列可用于大型字典,用于组织大量的数据,包括外存文件等。

集合的解释

集合:个体的汇集。在数学中集合是一个很重要的概念,它有三种特性:
1、确定性:集合中的元素是明确的
2、互异性:集合中任何两个元素都是不相同的。
3、无序性
集合有三种重要的运算操作应用很广:
求并集:比如两个集合S与T,S与T的并集中的元素是S的元素或者是T的元素。
求交集:比如两个集合S与T,S与T的交集中的元素既是S的元素,也是T的元素。
求差集:比如两个集合S与T,S与T的差集中的元素表示仅属于S不属于T的元素,T与S的差集表示仅属于T不属于S的元素。

集合的实现

集合是一种汇集的数据结构,可以使用一些汇集型的数据结构比如顺序表、链表或字典。

简单顺序表实现集合

总结:一些基本的操作,比如判断元素是否在集合中,往集合插入元素(保证唯一性),删除某个元素都是O(n)时间,重要操作,求并交差集为O(m*n)时间。性能上较差。

排序顺序表实现集合

总结:相较于简单顺序表,检索方面的性能有所提升,为O(logn)时间,重要操作,求并交差集为O(m+n)时间。性能上有较大提升

散列表实现集合

性能:在散列表还没有较大冲突的情况下,各种操作都很高效。求并差交集也有O(m+n)的高效时间。
为什么用散列表实现集合比较好?
相比最高效,将集合中的元素作为散列表中的关键码来使用,并通过散列函数进行转换为地址下标,就能通过常量时间来访问集合元素了。

位向量实现集合

有一个集合的总集U,比如U={a,b,c,d,e,f,g,h},我们所使用的任何一个集合Si都是属于U的子集,比如S1={a,b,c},那么S1的位向量表示则为S=11100000,S2={b,c,d},则S2=01110000,等等情况,1和0表示的含义在于,1代表此集合S元素在U中,0代表不在U中。
位向量使用集合时,操作代价基于集合U的大小度量,因为无论是空集,它的表示长度还是8位,空集=00000000。在需要处理的是U的一些子集时,且U的规模不是很大的情况下这种实现方式比较适用。

扩展:
平均检索长度:在一次完整检索中比较关键码的的平均次数,通常称为平均检索长度(ASL)。
索引:字典是两种功能的统一,1、作为一种数据存储结构,支持在字典里存储一批数据项。2、提供支持数据检索功能,设法维护从关键码找到相关数据的联系信息。第二点也称之为索引,其存在的目的就是为检索服务。索引所做的事就是要实现从关键码到数据存储位置的映射。
负载因子:散列表中当时的实际数据项树/散列表的基本存储区能容纳的元素个数,比如散列表所存储的元素为7个,散列表存储区能存下10个元素,那么负载因子就是7/10=0.7。

总结
1、字典是一种常用的数据结构,字典的基础结构就是key-value的映射,因此字典也被称为映射。
2、用散列技术来实现字典,可以将关键码转换为地址下标,将访问时间达到常量级别。
3、用散列实现字典时,散列是大范围到小范围的映射,所以会产生散列冲突,散列冲突的方式有内消解与外消解方法,内消解是开地址法,在发生冲突时,为所冲突元素再找到一个插入位置;外消解法包括溢出表与桶散列法,在发生冲突时,将元素不直接存储在已冲突的散列表中。
4、集合就是一批元素的汇集,使用散列技术来实现集合是最高效的。在特定场景下,使用位向量方式使用集合也是一种高效的手段。
5、基于散列实现的字典虽然效率高,但是没有确定性的效率保证,因为一旦数据量增大,散列字典的效率逐步下降。所以对于实现字典还有更多可探究的方式,树形结构来实现字典是其中一种,可以参考这篇博文。二叉排序树、平衡二叉树、多分支排序树、B树、B+树

posted @   三脚半猫  阅读(227)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示