每日学习录(数据结构—查找)(上)

本文详细介绍了查找算法在顺序表和有序表中的应用,包括顺序查找、折半查找、插值查找、斐波那契查找以及线性索引中的稠密索引、分块索引和倒排索引。还讨论了平衡二叉树,特别是AVL树的实现原理和算法,强调了在动态查找表中维护平衡的重要性以提高查找效率。
摘要由CSDN通过智能技术生成

目录

8.1查找概论

8.2顺序表查找

8.2.1顺序表查找算法

8.2.2顺序表查找优化

8.3有序表查找

8.3.1折半查找

8.3.2 插值查找

8.3.3斐波那契查找

8.3.4线性索引查找

8.4.1稠密索引

8.4.2分块索引

8.4.3倒排索引

8.5二叉排序树

8.5.1二叉排序树查找操作

8.5.2二叉排序树插入操作

8.5.3二叉排序树删除操作

8.6平衡二叉树(AVL树)

8.6.1平衡二叉树实现原理

8.6.2平衡二叉树实现算法


8.1查找概论

查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合,如下图就是一个查找表。

关键字(key)是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项(字段),我们称为关键码,如下图①②。

若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。这就意味着,对不同的记录,其主关键字均不相同。主关键字所在的数据项称为主关键码,如下图③④。

对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key),如下图⑤。次关键字也可以理解为是不以唯一识别一个数据元素(或记录)的关键字,它对应的数据项就是次关键码。

查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

若表中存在这样一个记录,则称查找是成功的,此时查找的结果给出整个记录的信息,或指示该记录在查找表中的位置。

若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。

查找表按照操作方式来分有两大种:静态查找表和动态查找表。

静态查找表(Static Search Table):只做查找操作的查找表。它的主要操作有:

(1)查找某个“特定的”数据元素是否在查找表中。

(2)检索某个“特定的”数据元素和各种属性。

动态查找表(Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。显然动态查找表的操作就是两个:

(1)查找时插入数据元素。

(2)查找时删除数据元素。

为了提高查找的效率,我们需要专门为查找操作设置了数据结构,这种面向查找操作的数据结构称为查找结构。

从逻辑上来说,查找所基于的数据结构是集合,集合中的记录之间没有本质关系。可是要想获得较高的查找性能,我们就不能不改变数据元素之间的关系,在存储时可以将查找结果集合组织成表、树等结构。

8.2顺序表查找

顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

8.2.1顺序表查找算法

顺序查找的算法如下:

8.2.2顺序表查找优化

上面算法并非足够完美,因为每次循环时都需要对i是否越界,即是否小于等于n做判断。事实上,还可以有更好一点的办法,设置一个哨兵,可以不要每次让i与n作比较。改进后的代码如下:

该算法的时间复杂度为O(n),有很大的缺点,当n很大时,查找效率极为低下,不过也有优点,这个算法非常简单,对静态查找表的记录没有任何要求,在一些小型数据的查找时,是可以适用的。

另外,也正由于查找概率的不同,我们完全可以将容易查找到的记录放在前面,不常用的记录放在后面,效率就可以有大幅提高。

8.3有序表查找

下面我们来介绍三种有序表的查找方法。

8.3.1折半查找

折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。折半查找的基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止

假如我们现在有这样一个有序表数组{0,1,16,24,35,47,59,62,73,88,99},除0下标外共10个数字。折半算法如下:

该算法的时间复杂度为O(logn),显然好于顺序查找的O(n)时间复杂度。

不过由于折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法已经比较好了。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

8.3.2 插值查找

举个例子,如果要在取值范围0~10000之间100个元素从小到大均匀分布的数组中查找5,我们自然会考虑从数组下标较小的开始查找。

由此看来,折半查找还有改进的空间。

上面代码的第6句,我们略微变换后得到:

也就是mid等于最低下标low加上最高下标high与low的差的一半。算法科学家们考虑的就是将这个1/2进行改进,改进为下面的计算方案:

此法可以大大提高查找的效率。

换句话说,我们只需要在折半查找算法的代码中更改第8行为:

就得到了另一种有序表查找算法,插值查找法。插值查找(Interpolation Search)是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式。应该说,从时间复杂度来看,它也是O(logn),但对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。反之,数组中如果分布类似{0,1,2,2000,2001,······,99998,999999}这种极端不均匀的数据,用插值查找未必是很适合的选择。

8.3.3斐波那契查找

斐波拉契查找(Fibonaci Search)是利用了黄金分割原理来实现的。

斐波拉契查找算法的核心在于:

1)当key=a[mid]时,查找就成功;

2)当key<a[mid]时,新范围是第low个到第mid-1个,此时范围个数为F[k-1]-1个;

3)当key>a[mid]时,新范围是第mid+1个到第high个,此时范围个数为F[k-2]-1个。

尽管斐波那契查找的时间复杂度也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏情况,比如这里key=1,那么始终都处于左侧长半区在查找,则查找效率要低于折半查找。

总结下来,

1.折半查找是进行加法与除法运算 (mid=(low+high)/2)

2.插值查找进行复杂的四则运算 (mid=low+(high-low)*(key-a[low])/(a[high]-a[low]))

3.斐波那契查找只是最简单加减法运算 (mid=low+F[k-1]-1)

在海量数据的查找过程中,这种细微的差别可能会影响最终的查找效率。

应该说,三种有序表的查找本质上是分隔点的选择不同,各有优劣,实际开发时可根据数据的特点综合考虑再做出选择。

8.3.4线性索引查找

数据结构的最终目的是提高数据处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与他对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术。

索引按照结构可以分为线性索引、树形索引和多级索引。我们这里就只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三种线性索引:稠密索、分块索引和倒排索引。

8.4.1稠密索引

稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,如下图。

稠密索引要对应的可能是成千上万的数据,因此对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。

索引项有序也就意味着,我们要查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高了效率。这显然是稠密索引的优点,但是如果数据集非常大,比如上亿,那就意味着索引也得有同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。

8.4.2分块索引

稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大。为了减少索引项的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。

分块有序,是把数据集的记录分成了若干块,并且这些需要满足两个条件:

块内无序:即每一块内的记录不要求有序。
块间有序:例如,要求第二块所有记录的关键字均要大于第一块所有记录的关键字。
如下图,我们定义的分块索引项结构分为三个数据项:

1.最大关键码,它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最2.小关键字也能比这一块最大的关键字要大;
3.存储了块中的记录个数,以便于循环时使用;                                                                                  4.用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。

在分块索引表中查找,就是分两步进行:

  1. 在分块索引表中查找要查关键字所在的块。由于分块索引表是块间有序的,因此很容易利用折半、插值等算法得到结果。
  2. 根据块首指针找到相应的块,并在块中顺序查找关键字。因为块中可以是无序的,因此只能顺序查找。

总的来说,分块索引在兼顾了对细分块不需要有序的情况下,大大增加了整体查找的速度,所以普遍被用于数据库表查找等技术的应用当中。

8.4.3倒排索引

这里我们介绍最简单的,也是最基础的搜索技术——倒排索引。

我们来看样例,现在有两篇极短的英文文章,编号分别为1和2。

Books and friends should be few but good.
A good book is a good friend.
假如我们忽略掉如“books”、“friends”中的复数“s”,以及大小写差异,可以整理出这样一张单词表,如下图,并将单词做了排序,也就是表格显示了每个不同的单词分别出现在哪篇文章中。

有了这张单词表,我们要搜索文章就非常方便了。如果你在搜索框中填写“book”关键字,系统就会先在这张单词表中有序查找“book”,找到后将它对应的文章编号1和2的文章地址(通常在搜索引擎中就是网页的标题和链接)返回,并告诉你,查找到两条记录,用时0.0001秒。由于单词表是有序的,查找效率很高,返回的又只是文章的编号,所以整体速度都很快。

在里单词表就是索引表,索引项的通用结构是

1.次关键码,例如上面的“英文单词”;
2.记录号表,例如上面的“文章编号”。
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引(inverted index)。倒排索引源于实际应用中需要根据属性(或字段、次关键码)的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。

倒排索引的优点显然就是查找记录非常快,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。但它的缺点是这个记录号不定长,若是对多篇文章所有单词建立倒排索引,那每个单词都将对应相当多的文章编号,维护比较困难,插入和删除操作都需要作相应的处理。

8.5二叉排序树

假设我们的数据集开始只有一个数{62},然后我们要将88插入数据集,我们以二叉树的方式,首先将62定为根结点,88因为比62大,因此做62的右子树,接着要插入58,因为比62小,所以成为左子树。以此类推,最终我们得到下图这样的二叉排序树,并且我们可以对它进行中序遍历,就得到一个有序序列。

二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。

若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总要快于无序的数据集,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。

8.5.1二叉排序树查找操作

首先提供一个二叉树的结构:

二叉排序树的查找实现代码:

8.5.2二叉排序树插入操作

有了二叉排序树的插入代码,我们要实现二叉排序树的构建就非常容易了。下面的代码就可以创建一棵树。

8.5.3二叉排序树删除操作

删除结点的三种情况:

叶子结点
仅有左或右子树的结点
左右子树都有结点
对于第一种情况,叶子结点,直接删除即可,对整棵树并没有什么影响;

对于第二种情况,仅有左或右子树的结点,结点删除后,将它的左子树或右子树整个移动到删除结点的位置即可;

对于第三种情况,左右子树都有结点,比较好的办法是,找到需要删除的结点p的直接前驱(或直接后继)s,用s来替换结点p,然后再删除此结点s。

下面这个算法是递归方式对二叉排序树T查找key,查找到时删除。

上面代码第6行执行了Delete方法,对当前结点进行删除操作。代码如下:

8.6平衡二叉树(AVL树)

平衡二叉树(Self-Balancing Binary Search Tree 或 Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。

有两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis在1962年共同发明一种解决平衡二叉树的算法,所以平衡二叉树又称AVL树

平衡二叉树是一种高度平衡的二叉排序树,也就是说,它要么是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1,0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。

如下图:

图1是平衡二叉树;

图2不是平衡二叉树,因为平衡二叉树的前提是二叉排序树,图2中59比58大却是58的左子树,不是二叉排序树;

图3不是平衡二叉树,因为结点58的左子树高度为2,而右子树为空,差绝对值大于1;

图4是平衡二叉树。

距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称之为最小不平衡子树。如下图,当新插入结点37时,距离它最近的平衡因子绝对值超过1的结点是58(即它的左子树高度2减去右子树高度0),所以从58开始以下的子树为最小不平衡子树。

8.6.1平衡二叉树实现原理

平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,在找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

8.6.2平衡二叉树实现算法

首先需要改进二叉排序树的结点结构,增加一个bf,用来存储平衡因子。

 

然后,对于右旋操作,代码如下:

左旋操作代码如下:

下面我们来看左平衡旋转处理的函数代码:

 

下面就是主函数,代码如下:

对于这段代码来说,我们只需要在需要构建平衡二叉树的时候执行如下代码即可在内存中生成一棵如下图的平衡二叉树。

至此,该算法就讲完了,如果我们需要查找的集合本身没有顺序,在频繁查找的同时也需要经常的插入和删除操作,显然我们需要构建一棵二叉排序树,但是不平衡的二叉排序树,查找效率是非常低的,因此我们需要在构建时,就让这棵二叉排序树是平衡二叉树,此时我们的查找时间复杂度就为O(logn),而插入和删除也为O(logn)。这显然是比较理想的一种动态查找表算法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值