王道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,一经查实,立即删除!

相关文章

L1-004.计算摄氏温度

给定一个华氏温度F&#xff0c;本题要求编写程序&#xff0c;计算对应的摄氏温度C。计算公式&#xff1a;C 5*(F-32)/9。题目保证输入与输出均在整型范围内。 输入格式&#xff1a; 输入在一行中给出一个华氏温度。 输出格式&#xff1a; 在一行中按照格式“Celsius C”输出对…

C和指针之函数之可变参数

1、可变参数如何实现 可变参数列表是通过宏来实现的,这些宏定义在stdarg.h头文件中,这个头文件声明了一个类型va_list和三个宏,va_start、va_arg、va_end,一般我们声明一个类型为va_list的变量,和这个几个宏配合使用,访问参数的值。 2、实现一个函数多个参数求均值 比…

hadoop:could only be replicated to 0 nodes, instead of 1

在Hadoop的环境搭建过程中&#xff0c;常常会遇到类似这样的错误信息提示&#xff1a;“could only be replicated to 0 nodes, instead of 1 ”&#xff0c;产生这样的错误原因有多种&#xff0c;这里列举出以下四种常用的解决方法以供参考&#xff1a; 确保master&#xff08…

System.Type类

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Reflection; namespace ConsoleApplication1 {class Program{static void Main(string[] args){//System.Type类//获取指向任何给定的类型…

.NET6之MiniAPI(二十三):Refit

本篇是与上篇HttpClient有关联的&#xff0c;有前篇中&#xff0c;我们是直接使用HttpClient来发出请求的&#xff0c;所有的请求信息都是我们根据需要自己来填充的。那Refit是什么呢&#xff1f;它是一个让我们调用API就像调用一个对象的方法一样简单&#xff0c;是通过把http…

设置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.…

C和指针之函数之递归实现厄密多项式

1、厄密多项式介绍 /*Hermite Polynomials(厄密多项式)是这样定义的1 ,n < 0Hn(x) 2x ,n 12xHn-1(x) - 2(n-1)Hn-2(x) ,n > 2*/ 2、代码实现 #include <stdio.h>/*Hermite Polynomials(厄密多项式)是这样定义的…

设置状态栏和导航栏

// 初始化导航栏外观效果 - (void)initialNavigationBarStyle { // 1. 获取导航栏的外观代理对象 UINavigationBar *navBar [UINavigationBar appearance]; // 2. 设置背景图片 [navBar setBackgroundImage:[UIImage imageNamed:"NavBar64"] forBarMetrics:UIBarMet…

PHP 长文章分页函数

<?php function ff_page($content,$page) { global $expert_id; $PageLength 2000; //每页字数 $CLength strlen($content); $PageCount floor(($CLength / $PageLength)) 1; //计算页数 $PageArrayarray(); $Seperator array("n","r","。&q…

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和指针之函数之求最大公约数

1、求最大公约数 求公约数的规则如下/*** M % N 0 : N*gcd(M, N) * M % N R, R > 0 : gcd(N, R)*/ 2、代码实现 #include <stdio.h> /*** M % N 0 : N*gcd(M, N) * M % N R, R > 0 : gcd(N, R)*/int gcd(int …

关于c#调用c编译器

这个已经过了好久了具体的实现代码没得&#xff0c;但是大致思路自己整理了一下&#xff1a; 首先要调用c编译器&#xff0c;process.start&#xff08;&#xff09;&#xff1b; 之后需要自己来进行编译器对代码执行的命令。 转载于:https://www.cnblogs.com/why1115/p/481032…

【自定义控件】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请求 这是最简单…

printk函数出现延迟输出的问题

错误描述 关于测试内核read接口 有如下代码片段&#xff1a; static ssize_t hello_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos){int ret 0;printk("[read task]count%ld", count);memcpy(readbuf, kerneldata, sizeof(kerneldata));…

个人博客(05)

昨天&#xff1a;设计并制作了添加图书信息的界面 今天&#xff1a;准备学习制作修改店铺的界面问题&#xff1a;页面消息的提醒转载于:https://www.cnblogs.com/yibao/p/6849805.html

C和指针之函数之把数字字符串转为整数并且返回这个数字(ascii_to_integer)

1、问题 *字符串必须包含一个或多个数字,函数应该把这些数字字符转换为整数并且 *返回这个值,如果脂肪层参数包含了任何非数字字符,函数就要返回0,不考 *虑溢出情况 2、代码实现 #include <stdio.h>/***字符串必须包含一个或多个数字,函数应该把这些数字字符转…

如何有效的在 LINQ 查询中处理异常?

咨询区 Jader Dias参考下面的代码&#xff1a;myEnumerable.Select(a > ThisMethodMayThrowExceptions(a));如何保证在 Linq 的查询过程中即使抛出了异常&#xff0c;查询不会被提前中断&#xff0c;就好像在每层迭代上都有默认的 try catch 块。回答区 LeBaptiste我写了一个…