本篇主要介绍查找概念及各类查找方法。
看完本篇,你将了解到:
1.查找问题概述(查找表可进行的操作、时间开销、一些计算方法)
2.顺序表的查找(存储方式、算法时间性能)
3.折半查找(可递归可迭代)
4.分块查找
5.二叉排序树(查找、插入、创建、删除)
6.平衡二叉排序树(平衡化方法:四种类型)
7.哈希查找(哈希表、哈希函数、解决冲突的方法)
(1)定义:
①“查找”是基于数据逻辑结构(D,R)定义的一种十分常见的运算。
②D:数据元素的一个集合
R:数据元素关系的偶对的集合,(可以是相对比较复杂的结构关系,如树的结构,图的结构等)
(D,R):是定义在D中的二元关系集
③如果R限定的关系为D中的数据元素构成一个集合的话,此时称为查找表(Search Table),
即:查找表,定义为由同一类型的数据元素(或记录)构成的集合。
④数学上,“查找”是指问题:“x ∈D?”,
即查找是确定某个数据元素是否在数据元素集上的问题.
⑤通常,D={a1 ,a2,…,ai,…,an }的每个数据元素ai具有唯一标识的分量,
被称为关键字,记为: ai .key。
⑥次关键字:用以识别若干数据元素(或记录)的数据项
(2)查找
x∈D等价于x.key ∈{a1.key,a2.key,…,ai.key, …,an .key}
key ∈ {key1 ,key2 ,…,keyi , …,keyn}
如果D中存在给定关键字的数据元素,则称为“查找成功”,否则称为“查找失败”。
(3)定义形式及返回值
①查找运算的一般定义形式为Search(T,x.key)。
②Search(T,x.key)的返回值,通常采用下列3种形式之一。
1.返回逻辑值
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210620213925424.png#pic_center)
若查找成功,返回值为数据元素的序号值i,否则返回0
3.返回地址值
若查找成功,返回值为数据元素的地址p,否则返回NULL
(4)对查找表可进行的操作
①查询某个特定的数据元素是否在查找表中;
②检索某个特定的数据元素的各种属性;
③在查找表中插入一个数据元素;
④从查找表中删除某个数据元素。
注:如果限定对于某个查找表,只允许执行第(1)和第(2)种操作,称为静态查找表,即静态查找表的内容不允许发生改变。
否则,如果查找表同时允许执行第(1)、第(2)、第(3)和第(4)种操作,则称其为动态查找表。
(5)时间开销
T(n):在查找表中查找某一数据元素的时间开销计为T(n),其通常与问题规模n相关,此外,还与数据的存储方式等因素相关
n定义为查找表中数据元素的个数。
Tbest (n):指最好情况下的时间开销;
Tworst(n):指最坏情况下的时间开销;
注:①对于基于“比较”操作实现的查找算法,时间开销T(n)主要来源于关键字的“比较”。
我们可以采用“平均查找长度”来衡量实现的查找算法中“比较”操作的平均执行次数,
即平均时间开销Taverage (n)。
②在查找表中查找是否有某个数据元素的关键字值等于给定的一个关键字值K,
往往需要跟查找表中的多个数据元素的关键字值进行比较,
平均查找长度(Average Search Length):这种关键字值比较次数的数学期望值,记为ASL。
假设一个查找表中有n个数据元素,如上所述,分别记做a1,a2,…,ai,…,an。
显然,一共有n个查找成功的不同情形,分别对应于成功查找这n个数据元素。
对于任意一个数据元素ai (1<=i<=n),假设其查找概率为Pi,查找ai成功时关键字比较次数记为Ci。
公式:
见下
(1)假设一个一维数组a|0…m]来存放数据元素,数据元素存放在a[1]、a[2]、… a[n]这n个单元中,这里要求n<=m。
(2)假定待查找的元素为x,其对应关键字为x.key,可从a1到an正序方向查找,也可反过来逆序查找
(3)但无论哪一种,都要以防在比较关键字之前,越过顺序表的边界,这就增加了时间开销
(1)首先将待查找的那个数据元素存放在顺表表的边界,即执行赋值语句a[o]=x;
(2)从a[n]开始进行逆序方向查找。
(3)以保证游标i的值不会越界,以此消除了判断是否越界的(比较i与n)的开销
注:在每种查找成功情况概率值相等时,即每个数据元素的查找概率均为1/n时。
平均查找长度为:ASL=(1+2+…+(n-i+1)+…+(n-1)+n)* 1/n=(n+1)/2
即:Taverage(n)= O(n)。
说明:①在实际应用中,在查找成功情况下,不一定每个数据元素查找概率相同,需要根据实际情况计算ASL,分析算法的平均时间性能;
②在各数据元素查找概率(P1,P2,…,Pn)呈现递增情况下,算法的平均时间性能到达最佳。
③在“查投失败”的情况下,需要与所有关键字比较一次,共计n次。
(2)查找失败情况
后续说明
在顺序查找表中,由于数据元素的关键字是无序的,无法确定待查找数据元素在顺序表中的大致位置,故只能依次查找顺序表中的数据元素。
有序顺序表:在有些情况下,顺序表中数据元素关健字是有序的,可递增可递减
下面我们只讨论递增情形
(1)算法思想
①将待查找元素x的关键字x.key与查找表中间位置数据元素的关键字(a mid.key)比较
②如果等于给定值,则查找成功,返回成功值;
若大于给定值,在表的左部继续折半查找
若小于给定值,在表的右部继续折半查找
③仅当左部或右部为空,查找失败,返回失败值
(2)初始化
low=1;
high=表长
公式:mid=(low+high)/2(向下取整)
(3)方法
①顺序查找表的左半部分查找,low不变,high为mid- 1;
②顺序查找表的右半部分查找,high不变,low为mid+1;
注:low<=high时,表示顺序查找子表非空,如果没查到数据元素,可以继续查找。
这里详细解释迭代方法
(1)迭代函数
//迭代实现
typedef struct
{
Elemtype *elem;
int length;
}SSTable;
int Search(SSTable T,KeyType k)
{
low =1;
high = T.length;
while (low<=high)
{
mid = (low+high)/2;
if(T.elem[mid]==k)
return mid;//成功
else if(T.elem[mid]>k)
high = mid-1;//左部
else
low = mid+1;//右部
}
return 0;//失败
}
①算法分析
规模为表长n,统计关键字之间的比较次数。
描述折半查找过程的二叉树,称为“判定树”
该二叉树中结点值:数据元素的序号
虚线框结点:空子树
每个结点的层次:在该顺序查找表中查找该结点对应的数据元素时,需执行的关键字比较的操作次数
(2)举例
①假设查找x.key值为a[6].key的情况
此时发现,mid的关键字与查找值相等,返回。故该过程只需一次关键字比较
结点6成为该判定树的根结点
②假设查找x.key值为a[3].key的情况
第一次比较后发现需在左半部分继续查找
③在这样一棵判定树中,对于某一个节点N,从根节点到N的路径上包含的所有节点,
实际上对应于在顺序查表中,查找节点N所对应数据元素时,查找过的所有数据元素。
(3)查找不成功时的情形
查找x.key值在区间内(a[6].key~a[7].key)的情况
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210620215156882.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzUxMzA4Mzcz,size_16,color_FFFFFF,t_70#pic_center)
(4)总结分析
①在“查找成功”的情况下,位置i数据元素查找成功时进行的关键字比较次数等于判定树中i结点的层数。
②最少的比较次数是1次,最多比较次数是树的高度。
即:Tbest:(n)=O(1),Tworst(n)= O(log n)。
③平均查找长度:“i位置查找成功的比较次数”ד发生的概率pi”之和,
即“i结点在判定树中的层数”ד发生的概率pi”之和。
④在每种查找成功情况的概率值相等时,平均查找长度为“i结点在判定树中的层数”之和,除以n。
(1)顺序查找法:
适应于顺序表中数据元素关键字无序的情形,其查找算法的时间复杂度为O(n);
(2)折半查找法:
适应于顺序表中数据元素关键字完全有序的情形,该查找算法的时间复杂度为O(log2 n)。
(1)定义
利用关键字序列的分段(块)有序性,建立分段(块)索引表。借助分段索引表,实现快速查找。
(2)分块方法
①将查找表分成若干数据元素块,对于任意两个相邻的数据块
第一块数据元素中的最大关键字小于第二块数据元素中的最小关键字
即:第一块数据元素中的所有关键字都比第二块小
则称该查找表是分块有序的
②而在每一块中,数据元素的大小是无序的
(3)算法思想
①在分段索引表中“顺序”或“折半”查找给定值所在的块;
②在①确定的块中,顺序查找给定值。
(4)举例
在其中查找表被划分为3块,且分块有序
分块索引表包含:每个分块的起始位置,本块数据元素关键字的最大值
(5)算法分析:
假设在索引表上的平均查找长度为ASLb,在查找表上的平均查找长度为ASLw,则
ASL= ASLb+ASLw
注:一般情况下,不妨令整个查找表长为n,均匀划分为b块,每块含数据元素个数s=n/b;
假定在索引表查找到每一块和块内查找到每个关键字都是等概率的,即概率分别为1/b和1/s 。
①若采用顺序查找法确定块
显然,当s=根号n时,ASL达到最小值。
②若采用折半查找法确定块
(1)有序性
分块查找法需要划分块,建立分块索引表。
其中,分块要求查找表呈现分块有序性,分块有序性通常属于“自然天成”的。(例如:高校历届毕业生的归档资料)。
(2)推广
分块查找法可以推广,建立多级分块索引表。
(1)定义:一棵空树,或是满足下列性质的一棵非空二叉树T:
①若T的左子树非空,则左子树中所有结点值小于T的根结点的值
②若T的右子树非空,则右子树的所有结点值大于T大根结点值
③T的左右子树均为二叉排序树
不是二叉排序树
(2)据(1)中定义及中序遍历的顺序要求,对任何一颗二叉排序树进行中序遍历
得到结点的值是递增有序的
假设一个查找表中的数据元素已经组织成一颗如图所示的二叉排序树
树节点的值表示数据元素的关键字值
①查找数据元素X(K=24)
顺序:先查找根结点45,24比45小,故一定在左子树中,故继续查找左子树的根结点12
由于24大于12,故在12的右子树中继续找,以此类推,直至找到
②查找数据元素X(K=30)
查找至24,发现其右子树为空,查找失败
(2)附码
//二叉排序树查找
BiTree *Locate(BiTree *T,KeyType K)//参数:指向二叉排序树根结点的指针,待查找数据元素关键字的值
{
if (!T)//T为空指针
return NULL;
if (T->data==K)//查找成功
return T;
if (T->data.key<K)
{
T=T->lchild;
}else
{
T=T->rchild;
}
return Locate(T,K);
}
如何把一个查找表组织成一个二叉排序树的形式呢?
(1)Insert(&T,K)
T表示一棵已经存在的二叉排序树的根结点,K为待插入的数据元素的关键字
根据给定的关键字K,zT为根结点的二叉排序树中查找,并在失败出插入该数据元素
根据插入方法,就能将一个查找表构造成二叉排序树
基本思想:
先初始化一棵二叉排序树的根结点为空。然后不断地往这棵二叉排序树中,
根据查找表中数据元素插入数据元素
假设被删除结点为p,其双亲结点为f,且不失一般性,假设p是f的左孩子
可分为4种情况分别处理
(1)待删除结点的左右子树均为空
此时直接修改双亲结点的指针,实现删除结点即可
不会破坏二叉排序树的特征要求
(2)待删除结点的左子树为空,右子树pr非空
此时只需让pr成为*f的左子树即可
此时只需让pl成为*f的左子树即可
(4)待删除结点的左右子树均为非空子树
比较复杂,举例说明
结点S是*p的左孩子结点的右子树中的,沿着右孩子指针路径下去的,
直到无右孩子的结点
此时有两种方法:
①令p的左子树为f的右子树,p的右子树为s的右子树
②结点s替换结点p(即删除p),结点s的左子树作为s的父节点*q的右子树
假设二叉排序树有n个结点,最少的比较次数是1次,最多比较次数是树的高度h。
树的高度h取决于n个关键字序列的初始序列,初始序列共计n!种排列情况,其树的最小高度为[(log n)]+1 ,最大高度为n。
即最多比较次数是在[[(log2 n)]+1~n]区间。有Tbest(n)=O(1),Tworst(n)=O(n)
引:
基于平衡二叉排序树的查找算法的最坏时间复查度Tworst(n)=O(n),其原因是树的高度h取决于n个关键字的初始序列。
初始序列共计有n!种可能的排列情况,其中有些初始序列导致二叉排序树高度h=n。
如果对于任意的初始序列,均能够确保构造出高度h<=Clog n的二叉排序树,那么其查找的最坏时间复杂度Tworst(n)=O(log n)。
如何构造这样的二叉排序树?
(1)定义:是一颗空树;或是满足下列性质的一颗非空二叉树T
(1)T的左子树和右子树深度之差的绝对值不超过1
(2)T的左子树和右子树均为平衡二叉树
(2)结点的平衡因子:该结点左子树高度-右子树高度。
由定义知,平衡因子只可能为0,-1,1
(1)算法思想:根据关键字序列,对每一个关键字K,逐个在平衡二叉排序树T上查找。
在失败处插入关键字K,如果插入时失去平衡,对最小不平衡子树进行平衡化处理
(2)核心问题:
①什么是最小不平衡子树
②平衡化处理的方法
(3)最小不平衡子树:从插入结点K到树根的路径上,距结点K最近的、
平衡因子绝对值大于1的结点为根的子树
插入24时失去平衡,此时12为根的树是最小不平衡二叉树
对于最小不平衡二叉树,可以根据导致失去平衡的插入位置,
分为LL型、RR型、LR型和RL型4种情况,并分别进行平衡化处理。
(1)LL型:插入点是最小不平衡子树的左子树的左子树,“单向右旋”
以结点B为中心,顺时针旋转,将B的右子树作为A的左子树
(2)RR型:插入点是最小不平衡子树的右子树的右子树,“单向左旋”
以结点B为中心,逆时针旋转,将B的左子树作为A的右子树
(3)LR型:插入点是最小不平衡子树的左子树的右子树,“左旋右旋”
以结点C为中心,先单向左旋,后单向右旋
(4)RL型:插入点是最小不平衡子树的右子树的左子树,“右旋左旋”
以结点C为中心,先单向右旋,后单向左旋
(5)举例
例:根据关键字序列: (13,24,37,53,90),试构造平衡的二叉排序树的过程。
解:(1)平衡二叉排序树构造如下:
如图8,9
(2)计算ASL(平均查找长度)如下:
(6)分析该算法时间性能
假设Nh表示深度为h的平衡二叉树至少含有的结点个数。
No=0,N=1,N2=2,…,N= Nh-1+Nh-2+1, …
由此可以得出,平衡二叉树的最大深度≤Clogn,即:Tworst(n)=O(logn).
引:①在前面结构中,数据元素的位置是相对随机的,与数据元素的关键字之间不存在
确定关系, 查找时需进行一系列关键字的比较。
②顺序查找时,比较的结果为等于和不等于;折半、二叉排序树查找是,比较结果为大于小于等于
③查找效率取决于查找过程中比较次数
④我们期望不经过任何比较,一次存取可得所查元素。则必须在数据元素的存储位置和其关键字之间建立一个
确定的对应关系H,使得每个关键字和结构中的一个唯一存储位置相对应
⑤查找时,只需根据这个对应关系找到给定值K的映象H(k),若存在关键字和k相等的数据元素,则必定在H(k)的
存储位置上
⑥由此,不需比较可直接获得所查数据元素
称对应关系H为哈希函数
(1)一个哈希函数H(K)的作用:计算关键字K对应的数据元素的存储地址
(2)思想:
存储过程:将关键字ki通过哈希函数H(Ki)计算在哈希表的存储地址h
将关键字ki对应的数据元素存放在第h个单元中
查找过程:查找key的数据元素,通过哈希函数H(K)计算在哈希表的存储地址h、
若h处有数据,则查找成功
(3)这种不依赖于“比较”运算,而是依靠算术“计算”的查找方法,
在理想状况下,最坏时间复杂性可以达到常量级,即:Tworst(n)=O(1)。
(4)需要考虑的两个问题:
①如何考虑关键字的分布特点构造一个合适的哈希函数
②如何解决冲突情况(根据哈希函数,不同关键字具有相同哈希地址,其中的不同关键字成为同义词)
即:定义域大于值域
(1)其定义域要考虑到理论上可能出现的所有关键字
(2)哈希表要考虑存储空间的有效利用和可行性,值域取决于实际上
出现的关键字数量,即实际取值范围
(1)基本原则:
①“均匀的哈希函数”,避免冲突情况的过度集中,预期哈希查找的平均时间效率能达到最佳
②如果对于关键字集合的每个关键字key ,经哈希函数H(key)映射到哈希地址集合中的任何一个地址之概率均是相等的,
则称哈希函数H(key)为均匀的(Uniform)哈希函数。
即每类同义词的数目大致相等
(2)构造的常见方法
①直接定址法
②数字分析法
③平方取中法
④折叠法
⑤余数法
(3)直接定址法:取关键字的某个线性函数值作为哈希地址
H(key) = a*key+b (a≠0)
关键字集合和地址集合大小完全相同,无冲突,即:Tworst(n)=O(1)
(4)数字分析法:对于可能出现的关键字集,事先分析这些关键字的每一位,选择其中
“若干”“随机”位构成哈希地址
说明:关于关键字的每一位随机性,可通过程序进行评价
取4、5两位作哈希地址 ,如H(k3)=87
(5)平方取中法:取关键字平方后的中间若干位为哈希地址(实际上对关键字进行某种叠加)
其中间位随机性高于两端
(6)折叠法:将关键字分割成位数相同的若干个段,然后各段叠加求和作为哈希地址
p’表示p的逆序
(7)余数法:取关键字不大于哈希表长m的某个数p,除后的余数作为哈希地址
H(key) = key MOD p (p≤m)(关键在于p的取值方法,选取不当容易产生同义词)
一般情况下,p为一个质数或为不含小于20质因子的一个合数
(1)常见方法:
①开放地址法
②再哈希法
③链地址法
④公共溢出区法
(2)开放地址法:当关键字key在哈希函数H(key)出现冲突时,在H(key)为起点,取一个增量di,作为下一个探测的位置
即:Hi(key)= (H(key)+di) MOD m ( H(key)为哈希函数,m为哈希表长,di为增量)
对于增量di,可以3种取法:
①线性探测di = i (i=1,2,3,… , m-1)
②二次探测di= 12,-12,22,-22,,3^2,.…- ±k^2, (k≤m/2)
③伪随机探测再散列
(3)再哈希法:当关键字key在哈希函数H(key)出现“冲突情况”时,依次采用其它哈希函数RHi计算关键字key的下一个地址,
直到冲突不再发生为止。
即:Hi(key)= RHi(key) (i=1,2,3,…,k) (RHi(key)为不同的哈希函数)
此方法不易产生聚集,但花费时间
(4)链地址法:将关键字的同义词存储在各种的单链表中
(5)公共溢出区法:当增加关键字到哈希表中遇到冲突时,将所有同义词统一存储到“公共溢出区”中。
(1)在给定关键字key值对应计算的哈希地址H(key)处查找
(2)若(1)查找不成功,根据处理冲突方法确定“下一地址处”查找
(3)重复(2),直到查找成功,或遇到结束标志为止
说明:
①“下一个地址”和“结束标志”是由处理冲突方法决定的
②查找算法中既使用了算术运算又使用了比较运算
③插入算法是在查找失败处增加关键字
④创建算法可以循环调用插入运算实现
⑤删除运算的实现是在查找成功处删除某个数据元素,对于某些方法,删除实际上是填入删除标志
算法复杂度:
α称为哈希表的装填因子,即:哈希表中填入的关键字个数与哈希表长之比。
问: 设关键字序列(19,14,23,01,68,20,84,27,55,11,10,90),哈希函数H(key)=key MOD 13,哈希表长16,
并采用线性探索法处理冲突,试构造哈希表,并计算其平均查找长度ASL。
解:(1)构造哈希表如下:
对19的哈希地址为6,因为6号单元中没有产生冲突,故将19存放在6号单元
以此类推。直到对1的哈希地址为1,因为1号单元中有冲突,所以向右找到第一个没有冲突的单元,将1存放进去
以此类推。最后得哈希表
(2)计算ASL如下:
ASL成功=(1+1+1+2+1+1+3+4+3+1+3+2)/12 =23/12
ASL失败=(0+8+7+6+5+4+3+2+1+0+4+3+2)/13
查找方法多种,其中哈希查找在算法性能方面较优。结合之前所学的树,平衡二叉树也为查找提供了更多新的思路。本篇所附代码较少,更多的是文字的内容赘述及贴图。哈希查找重在讨论解决冲突问题及哈希函数和哈希表的相关问题处理。
下一篇将介绍内部排序。