408复习笔记——数据结构(七):查找

408笔记系列(七)(PS:本人使用的是王道四本书和王道视频)


前言

至此,我们已经学习了线性表、栈和队列、串、树和图这么多的数据结构了;接下来我们就会开始使用这些数据结构来实现我们最常见也是最常用的查找和排序算法了,我们首先要学习的便是查找算法了;


一、简介

查找算法有很多,根据我们的数据逻辑结构主要划分为线性结构查找算法、树形结构查找算法和散列结构查找算法;当然也是从简单到复杂的一个过程,那么什么是查找呢?顾名思义,查找当然就是从一堆数里面找到你想要的那个数啦,我们称那个数为关键字,而我们的查找算法要做的便是通过不同的数据结构使得我们查找的速度和查找的效率更高;

那么,我们的问题又来了,怎样才算查找的效率高呢?我们给出了平均查找长度ASL的概念,没错,你见过,我们在之前二叉排序树那里就见过,ASL指的就是所有查找过程中进行关键字比较次数的平均值;ASL越小那么我们的查找算法效率越高,这是我们重要的衡量指标;

二、主要内容

1. 顺序查找和折半查找

上面有提到查找算法会根据数据的逻辑结构划分为三类,而这几个算法便是针对第一类的线性逻辑结构的查找算法——顺序查找和折半查找;

1.1 顺序查找

其实这也是最简单的查找算法了,没错就是让你去从一串数里面查找一个数,你首先会想到的那个,我猜你肯定想的肯定是,一个一个找呗!顺序查找算法也是这么想的,大不了一个一个的找呗!哈哈哈,是不是很简单,在写代码期间我们需要注意一个问题就是在顺序查找时我们往往会给数组的第一个位置设置为空,用来存储关键字,让顺序存储的效率更高。好吧!也没高很多,知道有这么个事就行;代码如下:

// 顺序搜索
int Seq_Search(int* SQ, int x)
{
	int i = length - 1;
	// 会在数组第一个位置空出来用于放需要对比的元素,我们一般称之为哨兵
	SQ[0] = x;
	for (i; SQ[i] != x; i--);
	return i;
}

那么算法就是这样,我们来看一下他的查找效率ASL吧!我们假定每个元素被选中为关键字的概率都相同,那么查找成功的ASL为(n+1)/2,查找失败的ASL为(n+1);

但是我们当时学习二叉排序树时候知道我们的一串数他可以是无序的,也可以是有序的呀!那如果他是有序的话,我们的顺序查找算法效率又会是怎样的呢?代码还是刚刚那个,但是我们明显感觉查找效率ASL不对劲,好像不一样,你的直觉很准,他们的查找失败ASL是不一样的,他的查找失败ASL为(n/2+n/(n+1)),比n+1好很多;但是不幸的是他们的查找成功ASL是一样的,都是(n+1)/2;

1.2 折半查找

刚刚说到二叉排序树,是不是想到了二叉排序树后面的平衡二叉树了?没错,折半查找其实就是一个平衡二叉树,但不是让我们创建一棵平衡二叉树,而是通过更改数组指针的方式在有序序列中查找关键字,而整个查找过程会在逻辑上形成一棵折半查找判定树,这棵树便是平衡二叉树;折半查找算法的过程如图所示:
在这里插入图片描述
折半查找算法的代码如下所示(这个代码很简单,但是想清楚折半查找判定树,这个代码必须要自己写一写):

// 折半查找
int Bin_Search(int * SQ, int x)
{
	// 在折半查找中不会有留出数组的第一个空闲位置作为哨兵使用
	int low, mid, height;
	for (low = 0, height = length - 1; low < height || low ==height;)
	{
		mid = (low + height) / 2;
		if (SQ[mid] > x)
		{
			height = mid - 1;
		}
		else if(SQ[mid] < x)
		{
			low = mid + 1;
		}
		else
		{
			return mid+1;
		}
	}
	return 0;
}

这里值得注意的是当我们选取mid值不同时,生成的折半查找判定树也是不同的,当mid取值为(low + height) / 2向上取整时,得到的便是一棵完全二叉树了!通过这一现象,我们发现折半查找的查找效率其实是与判定树树高有关的,而查找成功的ASL为log2(n+1) - 1;还有一点就是折半查找算法仅适合于顺序存储结构,是不适合链式存储结构;

这里我们也可以看一下顺序查找和折半查找的对比:
在这里插入图片描述
你可能会发现折半查找算法很快,但是因为我们要查找的数据是不定的,所以当我们查找的关键字在数组第一位时,其实顺序查找会更快,像这样:
在这里插入图片描述

1.3 分块查找

除了上述两种查找算法外,还有一种常见的索引顺序查找——分块查找;分块查找的思想很现实,也很简单明了,把大量的数据依据大小分为几块,拿出每块中最大的数作为索引,查找时先通过索引表找到关键字所在块,再从块中找到该关键字,无论是查找索引表还是在块中找关键字都可以通过顺序查找和折半查找两种方式实现;但是两种的查找效率差别很大;
在这里插入图片描述
这里我们需要知道一点就是如果分块查找的索引表和每一块中采用的查找算法都是顺序查找的话,那么他的ASL为((b+1)/2 + (s+1)/2)(s为每块中的数据总数,b为分成的块数,n为总数),并且当s为根号n时,ASL取最小值根号n+1

2. B树和B+树

上面我们学习完了线性结构的查找算法,接下来开始学习的便是树性结构的查找算法啦!其实在学习树的时候,我们已经接触过几个可以用于查找的树形结构——二叉排序树和平衡二叉树了;并且我们发现折半查找算法得到的判定树其实就是一棵平衡二叉树了;那么在平衡二叉树的基础上,我们又将学习新的树结构——B树和B+树,B树又叫多路平衡查找树,是的,他们的名字的确很怪,但是学习他们的确心里需要有点B树,要不然很容易就混乱了;

2.1 B树

老规矩,我们先看一下B树长啥样子了:
在这里插入图片描述
没错上图就是一棵三阶B树,看起来平平无奇的样子,但是你肯定发现了他与我们之前见过树的不同之处了,这个每个结点里面的关键字是不是有点多?相信你应该是看出来了,这也是B树的一个特点,决定结点中关键字数量的是这棵B树的阶,阶也就是这棵树中所有结点孩子个数最大值,从图中可以看到这棵树所有结点中孩子数最大为3,所以这是一棵三阶B树,基于此我们可能也会发现其实m阶B树的结点中关键字个数最大便是m-1;而我们在B树中查找关键字其实跟二叉排序树是一样的,像这样:
在这里插入图片描述
这个时候你应该会问这么做的意义何在呢?二叉排序树挺好的呀!不行不是有平衡二叉树嘛!其实这里我们就要说到B树和B+树一般的应用场景了,结合应用场景,我们就会知道为什么要在一个结点放那么多的元素啦!那么B树一般应用在什么地方呢?数据库索引和操作系统的文件索引;

其实大家可以想想B树在每一个结点放不止一个元素,会导致什么样的后果呢?我们的树最显而易见的特点就是变得矮胖起来了,这样直接导致的就是树高变小了;大家可以通过下面这两个图对比一下,其实查找次数并没有变少;那你可能要问了,那既然查找次数不变,那有个卵用嘛!

在这里插入图片描述
在这里插入图片描述
但是你忽略了咱们的应用场景啊,咱们可是要做文件索引呀!那一层可就是一次磁盘的寻址加载呀!同志!!!一次磁盘的寻址加载那得多慢啊!你说我们为什么要有虚拟地址,快表啥的,那还不是想把速度加快嘛,所以,我们的B树就是为此而生的,用B树做文件索引的话,那B树那么矮,我们便可以最小限度的去从一层到下一层找呀!

既然要做的就是让我们的树更矮,那单纯的只是形式增加结点中关键字数量肯定是不行的,他万一不存怎么办呢?于是我们的B树还有以下的条件需要满足:

  1. 除了根结点外的所有非叶子结点至少有m/2向上取整棵子树,及至少含有m/2向上取 - 1个关键字;
  2. 若根结点不是终端结点则至少有两棵子树
  3. 所有叶结点都出现在同一层次,并且不带信息

这样就很好的保证了B树的平衡了!这里就不给出树高、结点总数以及关键字总数的计算公式,可以在考试中直接推导;

B树还有一个很重要的考点,跟平衡二叉树一样——B树的插入和删除,B树的插入和删除比平衡二叉树简单得多;过程如下(这时创建一棵B树的过程):
在这里插入图片描述
插入结点如果发现超出结点关键字范围,就就取出中间位置的关键字把结点分裂开来;删除要稍微复杂一些,过程如图所示:
在这里插入图片描述
第一种情况,自己结点中的关键字够用,直接删除即可
在这里插入图片描述
这张图是删除时兄弟结点够借的情况,删除关键字,并用父结点中关键字替换,再用兄弟结点的关键字给到父结点;
在这里插入图片描述
这张是删除时兄弟结点也不够借的情况,那么不够借,咱们就合并呗!成为一个结点,这里注意在合并过程中也必须要遵循B树的要求;

2.2 B+树

先来看一下B+树长什么样吧!
在这里插入图片描述
相较于B树,B+树结点中有多少关键字便有多少孩子,而且孩子结点中包含了父结点的关键字,如果层数是两层的话,是不是有点像分块查找?B+树其实是一种对B树的优化,因为B树中包含了非叶子结点中关键字对应的存储地址,这样一块磁盘是无法全部用来存储关键字了,而通过这样的方法,我们便不需要存储关键字对应的存储地址,而且叶结点之间通过指针链接,使其能够实现顺序查找;

考试中经常考察的便是B树和B+树的区别了,主要便是B+树在查找过程中,非叶子结点上的关键字值等于给定值时并不终止,而是继续向下查找,知道叶结点为该关键字为止,B+树中所有非叶子结点都实现了索引作用;

3. 散列表

经过线性结构和树形结构后,最后便是散列结构的散列表啦!散列表是平常见到很多的查找算法,因为散列表的时间复杂度可以到达O(1),可能你会更熟悉他的另外一个名字——哈希表;那么哈希表是怎样做到时间复杂度如此之低呢?其实不难想到,既然想要时间复杂度那么低,那能做的便只能是空间换时间啦!

既然是散列结构,那肯定是有索引的,要不然岂不是变成了线性结构的顺序查找了?没错,这里散列表用的便是散列函数的方法来作为每个数字的索引,方便我们快速找到对应元素,具体实现也跟散列函数有着直接的关系;而我们常用的散列函数有直接定址法、除留余数法等,当然上面这两种也是我们考试中经常会出现的。

  1. 直接定址法
    这种方法给出了一个线性函数:H(key)=a*key+b作为散列函数,根据关键字的值我们可以求出散列函数的值,并将散列函数的值作为该关键字在散列表中的下标位置;这样当我们查找关键字时,只需要根据散列函数便可以在数组中快速查找到对应位置的值
    这种方法适合于关键字分布连续的情况,否则,若关键字分布不连续,空位较多,就会造成存储空间的浪费;
  2. 除留余数法
    这种方法是我们在考试中见到最多的方法了,对于一个散列表长度为m的散列函数是这样的:f(key) = key mod p(p<=m),就是模运算,这里p的取值是不大于m但接近或等于m的质数,因为质数取模可以使得分布更加的均匀;我们只需要根据最后的余数便可以查找到该关键字相对应的位置;

相信哈希表的原理是很容易明白的,但是大家应该也从上面的几种用来索引的哈希函数中发现了一些问题,比如在除留余数法中,如果两个不同的关键字得到了相同的余数怎么办呢?这是完全有可能的,5%3 = 2,8%3 = 2,那我们散列表下标为2的位置应该放谁呢?这便是出现了冲突,我们又称之为哈希冲突;那遇到哈希冲突我们该怎么办呢?大佬们给了两条路:一条是继续顺序存储叫做开放定址法,另一条是咱们用链表来实现叫做拉链法(又叫链接法)吧!
在这里插入图片描述

  1. 开放定址法
    开放定址法,依然采用的是顺序存储的方式,过程如图所示:
    在这里插入图片描述
    当发生冲突的时候我们会把新来的关键字像哈希表中其他位置移动,我们称之为探测,探测有没有空闲的位置来存放这个新的元素,但是既然是新的位置,我们肯定需要知道这个位置在哪吧!要不然待会怎么索引呢?所以这里我们给出了两种常见的探测方法:线性探测法、平方探测法;
    线性探测法:发生冲突时顺序查看表中下一个元素,直到找到一个空闲单元,这种方法非常显而易见,如同上图所示,但是我们需要注意的是,这里有一个“聚集”(或堆积)的概念,我们需要清楚,聚集指的是因为采取不当的处理冲突方法导致不同关键字元素对同一个散列地址进行争夺现象,这一现象往往都在开放定址法中出现
    平方探测法:这其实和线性探测法很相似,但是线性探测法很容易导致大量元素在相邻的散列地址上聚集起来,因而会大大降低查找效率;平方探测法为了避免出现聚集现象,会以12,(-1)2,22,(-2)2……这样的跨度来查找空闲位置,但是这样便不能探测散列表中所有单元,但至少能探测一半;
    在使用开放定址法删除时需要注意不能随便物理上删除表中已有的元素,若删除元素,则会截断其他相同散列地址的元素查找地址,因此我们会在删除一个元素后,给他一个删除标记,进行逻辑删除;
  2. 拉链法
    拉链法的过程如图所示:
    在这里插入图片描述
    通过链表的方式在每一个指针结点后添加元素和删除元素,非常容易理解;

最后查找算法讲究的便是查找效率了,哈希表的平均查找长度主要跟表长等并没有关系,能够影响到散列表查找效率的主要取决于:散列函数、处理冲突的方法和装填因子;前面两个我们都已经讨论过了,那么装填因子是什么呢?
装填因子 = 表中记录数 / 散列表长度
装填因子越大说明哈希表越满,发生冲突的可能性越大,反之发生冲突的可能性越小;

至此所有查找算法便已经完结,数据结构也将迎来最后一章——排序,芜湖!!!


三、常见题及易错题总结

图的答案:A、B、B、D、C、C、B
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

答案见下一章

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值