王道408数据结构——第七章 查找

文章目录

  • 一、基本概念
  • 二、顺序查找(线性查找)
    • 一般线性表的顺序查找
    • 有序表的顺序查找
  • 二、折半查找(二分查找)
  • 三、分块查找(索引顺序查找)
  • 四、B树
  • 五、B+树
  • 六、散列表
    • 构造散列函数
      • 1. 直接定址法
      • 2. 除留取余法
      • 3. 数字分析法
      • 4. 平方取中法
    • 冲突处理
      • 1. 开放定址法
      • 2. 拉链法(链地址法)
    • 性能分析

一、基本概念

查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找。
查找表(查找结构):用于查找的数据集。它由同一类型的数据元素(或记录)组成,可以是一个数组或链表等的数据类型。对查找表的常见操作一般有四种:

  1. 查询某个特定元素是否在查找表中;
  2. 检索满足条件的某个特定数据元素的各种属性;
  3. 在查找表中插入一个数据元素;
  4. 从查找表中删除某个数据元素。

静态查找表:若查找表涉及的操作只有上述的1、2,则无序动态地修改查找表。适合静态查找表的查找方法有顺序查找、折半查找、散列查找等。
动态查找表:若查找表涉及上述所有四个操作,则需要动态插入删除查找表。适合动态查找表的查找方式有二叉排序树的查找、二叉平衡树的查找、B树的查找、散列查找等
关键字:数据元素中唯一标识该元素的某个数据项的值。使用基于关键字的查找,其查找结果应是唯一的。
平均查找长度(ASL):在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度是所有查找过程中关键字比较次数的平均值。ASL是衡量查找算法效率的最主要指标。

二、顺序查找(线性查找)

适用于顺序表和链表。对于顺序表,通过数组下标递增来顺序扫描每个元素;对于链表,通过next指针来依次扫描每个元素。

一般线性表的顺序查找

基本思想是从线性表的一端开始,逐个检查关键字是否满足给定条件。若查找到某个元素的关键字满足条件按,则查找成功,返回该元素在线性表中的位置;若已找到表的另一端,但还没找到符合条件的元素,则返回查找失败的信息。
下面给出算法

int searchSeq(SSTable ST, ElemType key){// 引入哨兵元素,在创建查找表时,0号单元留空,预留给带查找元素关键字// 引入哨兵元素的目的是使得循环不必判断数组是否越界,可以避免很多不必要的判断语句,提高效率ST.elem[0] = key;  // 从后往前查找,满足i==0时,循环一定会跳出for(i = ST.length; ST.elem[i] != key; --i);return i;
}

对于有n个元素的表,定位第 i 个元素时,需进行n-i+1次比较。假设每个元素的查找概率相同,即Pi=1/nP_i=1/nPi=1/n时,ASL(成功)=∑i=1nPi(n−i+1)=n+12ASL_{(成功)}=\sum_{i=1}^nP_i(n-i+1)=\frac{n+1}{2}ASL()=i=1nPi(ni+1)=2n+1
查找不成功时,与表中各关键字的比较次数为n+1次,ASL(不成功)=n+1ASL_{(不成功)}=n+1ASL()=n+1

通常,查找表中记录的查找概率并不相等,若能预先得知每个记录的查找概率,则可以先对记录按查找概率进行排序。

缺点:当n较大时,ASL较大,效率低。
优点:对数据元素的储存没有要求,顺序储存或链式存储(只能顺序查找)皆可。对表中记录的有序性也没有要求。

有序表的顺序查找

若在查找前就知道表的关键字是有序的,则可以不用再比较到表的另一端就能确定查找失败,从而降低失败的ASL。
可以用查找判定树来描述查找过程。树中圆形结点标识表中存在的元素;举行结点称为失败结点,描述的是不在表中的数据值的集合,若有n个结点,则相应有n+1个失败结点。
有序顺序表上的顺序查找判定树
查找成功的ASL和一般线性表相同。
查找失败是,查找指针一定走到了某个失败结点。失败结点时虚构的,所以到达失败结点时的查找长度等于其父结点所在的层数。
ASL(不成功)=∑j=1nqj(lj−1)=1+2+⋯+n+nn+1=n2+nn+1ASL_{(不成功)}=\sum_{j=1}^nq_j(l_j-1)=\frac{1+2+\cdots+n+n}{n+1}=\frac{n}{2}+\frac{n}{n+1}ASL()=j=1nqj(lj1)=n+11+2++n+n=2n+n+1n式中,qjq_jqj是到达第 j 个失败结点的概率,在相等查找概率的情形下,它为1/(n+1)1/(n+1)1/(n+1)ljl_jlj是第 j 个失败结点所在层数。

二、折半查找(二分查找)

基本思想:首先将给定值key于表中间位置的元素进行比较,若相等,则查找成功,返回元素位置;若不等,则目标元素只能在中间元素以外的前半部分或后半部分,然后在缩小的范围内继续同样的查找。
算法如下

int binnarySearch(SqList L, ElemType key){int low = 0;int high = L.length - 1;int mid;while(low <= high){mid = (low + high) / 2;if(L.elem[mid] == key)return mid;else if(L.elem[mid] < key)low = mid + 1;elsehigh = mid - 1;} return -1;  // 查找失败
}

折半查找也可用判定树来描述。每个结点值均大于其左子结点,小于其右子结点。若有序序列有n个元素,则判定树中有n个非叶结点和n+1个叶节点。
显然,二分查找的判定树是一棵平衡二叉树
描述折半查找过程的判定树
ASL(成功)=1n∑i=1nli=1n(1×1+2×2+⋯+h×2h−1)=n+1nlog⁡2(n+1)−1≈log⁡2(n+1)−1(n较大时,可近似为)\begin{aligned} ASL_{(成功)}&=\frac{1}{n}\sum_{i=1}^nl_i\\ &=\frac{1}{n}(1\times1+2\times2+\cdots+h\times2^{h-1})\\ &=\frac{n+1}{n}\log_2(n+1)-1\\ &\approx\log_2(n+1)-1(n较大时,可近似为)\\ \end{aligned} ASL()=n1i=1nli=n1(1×1+2×2++h×2h1)=nn+1log2(n+1)1log2(n+1)1(n)
式中,h是树的高度,元素个数为 n 时h=⌈log⁡2(n+1)⌉h=\lceil\log_2(n+1)\rceilh=log2(n+1)(与n个结点的完全二叉树相同)
时间复杂度为O(log⁡2n)O(\log_2n)O(log2n),平均情况下比顺序查找效率高。
计算失败的ASL时,同样已其父节点的高度作为失败结点的高度。

折半查找需要快速定位查找区域,要求线性表必须具有随机存取的特性。该查找法仅适合于顺序存储结构,且要求元素按关键字有序排序。

三、分块查找(索引顺序查找)

基本思想:将查找表分为若干子,块内元素可以无序,但块之间是有序的,即前一块中的最大关键字小于后一块中的最小关键字。再建立一个索引表,索引表中的每个元素含有各块中的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排序。

分块查找吸取了顺序查找和折半查找各自的优点,既有动态结构,又适合于快速查找。查找分为两步,第一步是在索引表中确定待查记录所在的块,可以使用顺序查找或折半查找;第二部是在块内顺序查找。

分块查找的ASL为索引查找和块内查找的平均长度之和。设索引查找和块内查找ASL为LIL_ILI、LS、L_SLS,则分块查找ASL=LI+LSASL=L_I+L_SASL=LI+LS
将长度为 n 的查找表均匀分为 b 块,每块有 s 个记录,在等概率情况下:
若块内和索引表均采用顺序查找,则ASL(成功)=LI+LS=b+12+s+12=s2+2s+n2sASL_{(成功)}=L_I+L_S=\frac{b+1}{2}+\frac{s+1}{2}=\frac{s^2+2s+n}{2s}ASL()=LI+LS=2b+1+2s+1=2ss2+2s+n此时,若s=ns=\sqrt{n}s=n,则ASL有最小值s=n+1s=\sqrt{n}+1s=n+1
若对索引表采用折半查找,则ASL(成功)=LI+LS=⌈log⁡2(b+1)⌉+s+12ASL_{(成功)}=L_I+L_S=\lceil\log_2(b+1)\rceil+\frac{s+1}2ASL()=LI+LS=log2(b+1)+2s+1

四、B树

不考,暂略。

五、B+树

不考,暂略。

六、散列表

散列函数:把查找表中的关键字映射成该关键字对应地址的函数,记为Addr=Hash(key)。这里的地址可以是数组下标、索引或内存地址等。
冲突:散列函数可能把两个不同的关键字映射到相同地址,这种情况称为冲突。发生碰撞的不同关键字称为同义词。设计好的散列函数应尽可能避免冲突,但冲突总是不可避免的。
散列表:根据关键字直接进行访问的数据结构。散列表建立了关键字和储存地址的一种直接映射关系

理想情况下,散列表的查找时间复杂度为O(1)O(1)O(1),即与表中元素的个数无关。

构造散列函数

构造散列函数时,必须注意一下几点:

  1. 散列函数的定义域必须包含全部需要储存的关键字,而值域的范围依赖于散列表的大小。
  2. 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。
  3. 散列函数应尽量简单,以减少计算时间。

1. 直接定址法

直接取关键字的某个线性函数值作为散列地址。散列函数为
H(key)=keyH(key)=keyH(key)=key
H(key)=a×key+bH(key)=a\times key+bH(key)=a×key+b

这种方法计算最简单,且不会产生冲突。
适合关键字分布基本连续的情况,若关键字分布不连续,会导致存储空间的浪费。

2. 除留取余法

假定散列表表长为m,取一个不大于m但最接近m的质数p。散列函数为
H(key)=key%pH(key)=key\%pH(key)=key%p

这是一种最简单、最常用的方法。
关键是选好p,使得每个关键字通过散列函数转换后能等概率地映射到散列空间中,从而进坑了减少冲突的可能性。

3. 数字分析法

设关键字是 r 进制数,而r个数码在各位上的频率不一定相同,可能在某些位上分布均匀一些,此时应选取数码分布较为均匀的若干位作为散列地址。

这种方法适合已知的关键字集合。若更换了关键字,则需要重新构造新的散列函数。

4. 平方取中法

关键字平方值的中间几位作为散列地址。

这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适合关键字的每位取值都不够均匀或均小于散列地址所需的位数。

冲突处理

任何散列函数都不能绝对避免冲突,必须考虑发生冲突时的处理方法,即为冲突的关键字寻找下一个空的hash地址。若得到的另一个散列地址仍然发生冲突,则需要继续求下一个hash地址。

1. 开放定址法

指看存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开发。递推公式为Hi=(H(key)+di)%mH_i=(H(key)+d_i)\%mHi=(H(key)+di)%m式中,HiH_iHi表示处理冲突第 i 次探测得到的散列地址,m表示散列表表长,did_idi为增量序列。

取定某一增量序列did_idi后,对应的处理方法就是确定的。通常有以下4种取法:

  • 线性探测法(线性探测再散列法):di=0,1,2⋯,m−1d_i=0,1,2\cdots ,m-1di=0,1,2,m1
    • 冲突发生时,顺序查看表中下一个单元,直到找出一个空单元或查遍全表(此时表已满)。
    • 线性探查法可能使第 i 个散列地址的同义词存入第 i+1 个散列地址,这样本应存入第 i+1 个散列地址的元素就要争夺 i+2 个散列地址。造成大量元素在相邻的散列地址上聚集(堆积)起来,大大降低了查找效率。
  • 平方探测法(二次探测再散列法):di=02,12,−12,22,−22,⋯,k2,−k2(k≤m2)d_i=0^2,1^2,-1^2,2^2,-2^2,\cdots,k^2,-k^2(k\leq\frac m2)di=02,12,12,22,22,,k2,k2(k2m)
    • 散列表长度m必须是一个可以表示为4k+3的素数。
    • 是一种处理冲突的较好方法,可以避免出现堆积问题。
    • 缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。
  • 再散列法(双散列法):di=Hash2(key)d_i=Hash_2(key)di=Hash2(key)
    • 当通过第一个散列函数得到的地址发生冲突时,利用第二个散列函数计算该关键字的地址增量
    • 具体散列函数为Hi=(H(key)+i×Hash2(key))%mH_i=(H(key)+i\times Hash_2(key))\%mHi=(H(key)+i×Hash2(key))%m
    • 最多经过m-1次探测就会遍历表中所有位置。
  • 伪随机序列法:did_idi为伪随机数序列。

在开放定址法中,不能随便物理删除表中已有的元素,因为若删除元素,会截断其他具有相同散列地址的元素的查找地址。因此,要删除一个元素时,可以给它添加一个删除标记,进行逻辑删除。这种方法需要定期维护散列表,把删除标记的元素物理删除。

2. 拉链法(链地址法)

把所有同义词储存在一个线性链表中。这个线性链表由其散列地址唯一标识。
适合于经常进行插入和删除的情况。

性能分析

散列表的查找过程于构造散列表的过程基本一致。根据散列函数和关键字可以计算出记录的散列地址。若散列地址上:①无记录,说明查找失败;②若有记录且关键字相同,则查找成功;③若有记录但关键字不同,使用给定的处理冲突方法计算下一个散列地址,再次进行比较。
装填因子:定义为一个表的装满程度,即α=表中记录数n散列表长度m\alpha = \frac{表中记录数n}{散列表长度m}α=mn
散列表的查找效率取决于散列函数、处理冲突的方法和装填因子。
散列表的ASL依赖于装填因子,而不直接依赖于n或m。

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

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

相关文章

设置utf8编码问题

注意&#xff1a;乱码和request的具体实现类有关&#xff0c;现在已经查到的是RequestDispatcher.forward调用前使用的是org.apache.catalina.connector.RequestFacade类而RequestDispatcher.forward调用后使用的是org.apache.catalina.core.ApplicationHttpRequest&#xff0c…

王道408数据结构——第八章 排序

文章目录一、排序定义二、插入排序——直接插入排序1. 描述2. 代码和示例3. 空间效率4. 时间效率5. 稳定性6. 适用性三、插入排序——折半插入排序1. 描述2. 时间效率3. 稳定性四、插入排序——希尔排序&#xff08;缩小增量排序&#xff09;1. 描述2. 代码和示例3. 空间效率4.…

Avalonia跨平台入门第二十一篇之玩耍CEF

在前面分享的几篇中咱已经玩耍了Popup、ListBox多选、Grid动态分、RadioButton模板、控件的拖放效果、控件的置顶和置底、控件的锁定、自定义Window样式、动画效果、Expander控件、ListBox折叠列表、聊天窗口、ListBox图片消息、窗口抖动、语音发送、语音播放、语音播放问题;今…

golang实现自定义驱动的Cache

近期在写 ActivedRouter项目的时候需求一个缓存模型&#xff0c;要求缓存模型支持不同驱动,例如:memory、file、redis、mysql&#xff0c;实现思路代码如下: cache.go文件,定义缓存对外接口 //ActivedRouter //Author:usher.yue //Amail:usher.yuegmail.com //TencentQQ:422366…

【自定义控件】c#winform自定义控件实现标签控件

介绍首先我们设计这个控件的时候要明白控件是怎样交互的&#xff0c; 熟悉b站的小伙伴应该知道 &#xff0c;我们上传视频的时候会去选择标签 &#xff0c;我们输入标签文本 按下回车就代表该标签已经添加成功了&#xff0c;效果图如下&#xff01;控件拆分我们首先拆分一下该控…

ASP.NET 使用Ajax(转)

之前在Ajax初步理解中介绍了对Ajax的初步理解&#xff0c;本文将介绍在ASP.NET中如何方便使用Ajax&#xff0c;第一种当然是使用jQuery的ajax&#xff0c;功能强大而且操作简单方便&#xff0c;第二种是使用.NET封装好的ScriptManager。 $.ajax向普通页面发送get请求 这是最简单…

fir.im 持续集成技术实践

互联网时代&#xff0c;人人都在追求产品的快速响应、快速迭代和快速验证。不论是创业团队还是大中型企业&#xff0c;都在探索属于自己的敏捷开发、持续交付之道。fir.im 团队也在全面实施敏捷&#xff0c;并推出新持续集成服务— flow.ci &#xff0c;以帮助企业将开发测试流…

宇宙最強的IDE - Visual Studio 25岁生日快乐

每位开发者从入门开始或多或少都会接触过 Visual Studio &#xff0c; 现今的 Visual Studio 除了支持传统的 C , C# , Visual Basic.NET ,F# 的编程语言外&#xff0c;还可以做 Python , Node.js 的开发。在应用场景上也从单一的桌面应用&#xff0c;延伸到 Web &#xff0c; …

有没有一段代码,让你觉得人类的智慧也可以璀璨无比?【转】

转自&#xff1a;https://www.zhihu.com/question/30262900 作者&#xff1a;烧茄子链接&#xff1a;https://www.zhihu.com/question/30262900/answer/48741026来源&#xff1a;知乎著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。当然是这个…

如何使用 abp 创建 module 并应用单独的数据库迁移

最近在学习使用 abp 来做一些小程序。abp 是一个功能丰富的 .NET 开发框架&#xff0c;完全开源&#xff0c;遵循 DDD&#xff08;领域驱动&#xff09;设计模式&#xff0c;支持微服务开发&#xff0c;集成了 Identity、角色权限、本地化、动态代理、后台任务、分布式消息、审…

MinGW安装和使用基础教程

MinGW全称Minimalist GNU For Windows&#xff0c;是个精简的Windows平台C/C、ADA及Fortran编译器&#xff0c;相比Cygwin而言&#xff0c;体积要小很多&#xff0c;使用较为方便。MinGW提供了一套完整的开源编译工具集&#xff0c;以适合Windows平台应用开发&#xff0c;且不依…

px,em,rem,vw单位在网页和移动端的应用

px&#xff1a; 是网页设计中最常用的单位&#xff0c;然而1px到底是多大长&#xff0c;恐怕没有人能回答上来 它用来表示屏幕设备物理上能显示的最小的一个点&#xff0c;这个点不是固定宽度的&#xff0c;不同设备上点的长度、比例有可能会不同。 假设&#xff1a;你现在用的…

cs-Panination

ylbtech-Unitity: cs-PaninationPager.cs IPagingOption.cs IPagedList.cs PagingOption.cs PagedList.cs PagingExtensions.cs 1.A,效果图返回顶部 1.B,源代码返回顶部1.B.1,Pager.cs using System; using System.Collections.Generic; using System.Linq; using System.Text…

SignalR的使用

什么是 SignalR&#xff1f;ASP.NET Core SignalR 是一个开放源代码库&#xff0c;可用于简化向应用添加实时 Web 功能。实时 Web 功能使服务器端代码能够将内容推送到客户端。适合 SignalR 的候选项&#xff1a;需要从服务器进行高频率更新的应用。示例包括游戏、社交网络、投…

NHibernate之旅(7):初探NHibernate中的并发控制

本节内容 什么是并发控制&#xff1f; 悲观并发控制(Pessimistic Concurrency)乐观并发控制(Optimistic Concurrency)NHibernate支持乐观并发控制实例分析结语什么是并发控制&#xff1f; 当很多人试图同一时候改动数据库中的数据时&#xff0c;必须实现一个控制系统&#xff0…

期望DP

期望DP的一般做法是从末状态開始递推&#xff1a; Problem DescriptionAkemi Homura is a Mahou Shoujo (Puella Magi/Magical Girl).Homura wants to help her friend Madoka save the world. But because of the plot of the Boss Incubator, she is trapped in a labyrinth …

神奇的[Caller*]属性

前言上次&#xff0c;我们《使用 CallerArgumentExpression 检查弃元参数》&#xff0c;它实际是利用编译器编译时将变量名称传入。其实&#xff0c;.NET中提供了多个[Caller*]属性&#xff0c;帮助我们轻松获取调用者信息。CallerFilePathAttribute允许获取包含调用方的源文件…

java dateTime + long

2019独角兽企业重金招聘Python工程师标准>>> public static void main(String[] args) throws Exception{SimpleDateFormat sdfnew SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // long timeStartsdf.parse("2011-09-20 12:30:45").getTime();l…

.NET Core中异常过滤器ExceptionFilter的使用介绍

介绍实现需要继承IExceptionFilter 或 IAsyncExceptionFilter。可用于实现常见的错误处理策略。使用场景首先讲一下我们为什么要使用异常过滤器 &#xff0c;如果业务场景复杂&#xff0c;只使用HttpStatusCode&#xff0c;抛出异常后,后期要加很多字段来描述。那么这种就比较不…

程序一启动检查网络,如果没有网络就退出程序

转载于:https://www.cnblogs.com/songxing10000/p/4823812.html