7. 查找
考纲内容
- 查找的基本概念
- 顺序查找
- 分块查找
折半查找
- 过程、构造判定树、分析平均查找长度
B树及其基本操作、B+树的基本概念
- 掌握B树的插入、删除和查找过程
- 了解B+树的基本概念和性质
散列表
- 散列表的构造、冲突处理方法(各种方法的处理过程)
- 查找成功和查找失败的平均查找长度
- 散列查找的特征和性能分析
- 查找算法的分析及应用
1. 查找的基本概念
- 查找:在数据集合中寻找满足某种条件的数据元素的过程
查找表:用于查找的数据集合,由同一类型的数据元素组成
- 查询某个特定的数据元素是否在查找表中
- 检索满足条件的某个特定的数据元素
- 查询某个特定的数据元素是否在查找表中
- 从查找表中删除某个数据元素
静态查找表:只涉及查询和检索的查找表,无须动态地修改查找表
- 适用方法:顺序查找,折半查找,散列查找
动态查找表:需要动态地插入或删除的查找表
- 适用方法:二叉排序树的查找,散列查找
- 关键字:数据元素中唯一标识该元素的某个数据项的值
- 平均查找长度:所有查找过程中进行关键字的比较次数的平均值(衡量算法效率的最主要指标)
2. 顺序查找和折半查找
1. 顺序查找(主要用于线性表)
1. 一般线性表的顺序查找
基本思想:从线性表的一端开始,逐个检查关键字是否满足给定的条件
- 哨兵:避免很多不必要的判断语句,提高程序效率
性能分析——元素个数为n
- 时间复杂度: O ( n ) O(n) O(n)
- 定位地i个元素时,需要进行n-i+1次关键字的比较
- 查找成功:查找概率相等,即 P i = 1 n P_i=\frac{1}n Pi=n1 A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) = 1 + 2 + ⋅ ⋅ ⋅ + n 2 × 1 n = n + 1 2 ASL_{成功}=\sum_{i=1}^nP_i(n-i+1)=\frac{1+2+···+n}2\times\frac{1}n = \frac{n+1}2 ASL成功=i=1∑nPi(n−i+1)=21+2+⋅⋅⋅+n×n1=2n+1
- 查找失败: A S L 不 成 功 = n + 1 ASL_{不成功}=n+1 ASL不成功=n+1
优缺点
- 优点:对数据元素的存储没有要求
- 缺点:当n较大时,平均查找长度较大,效率低
2. 有序表的顺序查找——线性表可为链表
- 基本思想:若提前知道关键字有序,则查找失败时可以不用比较到表的另一端就能返回查找失败的信息,从而降低顺序查找失败的平均查找长度
性能分析
- 时间复杂度: O ( n ) O(n) O(n)
- 查找成功:同一般线性表的顺序查找
- 查找失败:查找概率相等,即 q j = 1 n + 1 , l j q_j=\frac{1}{n+1},l_j qj=n+11,lj是第j个失败结点的层数 A S L 不 成 功 = ∑ j = 1 n q j ( l j − 1 ) = 1 + 2 + ⋅ ⋅ ⋅ + n + n n + 1 = n 2 + n n + 1 ASL_{不成功}=\sum_{j=1}^nq_j(l_j-1) = \frac{1+2+···+n+n}{n+1}=\frac{n}2+\frac{n}{n+1} ASL不成功=j=1∑nqj(lj−1)=n+11+2+⋅⋅⋅+n+n=2n+n+1n
2. 折半查找(仅适用于有序顺序表)
基本思想
- 给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分,然后在缩小的范围内继续查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素
- 给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分,然后在缩小的范围内继续查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素
算法实现
int Binary_Search(SqList L, ElemType key)
{
int low = 0, high = L.length-1, mid;
while(low <= high)
{
mid = (low + high) / 2; //取中间位置
if(L.elem[mid] == key)
return mid; //查找成功则返回所在位置
else if(L.elem[mid] > key)
high = mid - 1; //从前半部分继续查找
else
low = mid + 1; //从后半部分继续查找
}
return -1; //查找失败,返回-1
}
判定树(平衡二叉树)
- 圆形结点表示一个记录,结点中的值为该记录的关键字值
- 方形叶结点表示查找不成功的情况
- 查找成功的查找长度为从根结点到目的结点的路径上的结点数;查找不成功的查找长度为从根结点到对应失败结点的父结点的路径上的结点数
- 每个结点值均大于其左子结点值,小于其右结点值
- 若有序序列有n个元素,则对应的判定树有n个非叶结点和n+1个叶结点
性能分析(要求线性表必须能随机存取)
- 比较次数最多不会超过树的高度
- 时间复杂度: O ( l o g 2 n ) O(log_2 {n}) O(log2n)
- 查找成功(等概率情况下):h为树高,元素个数为n时树高 h = ⌈ l o g 2 ( n + 1 ) ⌉ h=\lceil log_2(n+1)\rceil h=⌈log2(n+1)⌉(不包含失败结点) A S L 成 功 = 1 n ∑ i = 1 n l i = 1 n ( 1 ∗ 1 + 2 ∗ 2 + ⋅ ⋅ ⋅ + h ∗ 2 h − 1 ) = n + 1 n l o g 2 ( n + 1 ) − 1 ≈ l o g 2 ( n + 1 ) − 1 ASL_{成功}=\frac{1}n\sum\limits_{i=1}^nl_i=\frac{1}n(1*1+2*2+···+h*2^{h-1})=\frac{n+1}nlog_2{(n+1)}-1\approx log_2(n+1)-1 ASL成功=n1i=1∑nli=n1(1∗1+2∗2+⋅⋅⋅+h∗2h−1)=nn+1log2(n+1)−1≈log2(n+1)−1
3. 分块查找
- 基本思想:将查找表分为若干块,块内的元素可以无序,但块间有序。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列
查找步骤
- 在索引表中确定待查记录所在的块,可以顺序查找或折半查找
- 在块内顺序查找
性能分析
- 平均查找长度为索引查找( L I L_I LI)和块内查找( L S L_S LS)的平均长度之和
长度为n的查找表均匀地分成b块,每块有s个记录,等概率下
- 若在块内和索引表中均采用顺序查找,则平均查找长度 A S L = L I + L S = b + 1 2 + s + 1 2 = s 2 + 2 s + n 2 s ASL=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 = n s=\sqrt{n} s=n,则平均查找长度取最小值 n + 1 \sqrt{n}+1 n+1
- 若索引表采用折半查找时,则平均查找长度为 A S L = L I + L = ⌈ l o g 2 ( b + 1 ) ⌉ + s + 1 2 ASL=L_I+L_=\lceil log_2(b+1)\rceil+\frac{s+1}2 ASL=LI+L=⌈log2(b+1)⌉+2s+1
3. B树和B+树
1. B树(多路平衡查找树)
1. 定义
一棵m阶B树或为空树,或为满足如下特性的m叉树
- 树中每个结点至多有m棵子树,即至多含有m-1个关键字
- 若根结点不是终端结点,则至少有两棵子树
- 除根结点外的所有非叶结点至少有 ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉棵子树,即至少有 ⌈ m 2 ⌉ − 1 \lceil\frac{m}2\rceil-1 ⌈2m⌉−1个关键字
- 所有非叶结点的结构如下: K i ( i = 1 , 2 , ⋅ ⋅ ⋅ , n ) K_i(i=1,2,···,n) Ki(i=1,2,⋅⋅⋅,n)为结点的关键字,且满足 K 1 < K 2 < ⋅ ⋅ ⋅ < K n K_1<K_2<···<K_n K1<K2<⋅⋅⋅<Kn; P i ( i = 1 , 2 , ⋅ ⋅ ⋅ , n ) P_i(i=1,2,···,n) Pi(i=1,2,⋅⋅⋅,n)为指向子树根结点的指针,且指针 P i − 1 P_{i-1} Pi−1所指子树中所有结点的关键字均小于 K i K_i Ki, P i P_i Pi所指子树中所有结点的关键字均大于 K i K_i Ki, n ( ⌈ m 2 ⌉ − 1 ≤ n ≤ m − 1 ) n(\lceil\frac{m}2\rceil-1\le n\le m-1) n(⌈2m⌉−1≤n≤m−1)为结点中关键字的个数
- 所有叶结点都出现在同一层次上,并且不带信息(实际不存在)
2. B树的高度(磁盘存取次数)
- B树中的大部分操作所需的磁盘存取次数与B树的高度成正比
- B树的高度不包括最后的不带任何信息的叶结点所处的那一层
- 若 n ≥ 1 n\ge1 n≥1,则对任意一棵包含n个关键字、高度为h、阶数为m的B树
n ≤ ( m − 1 ) ( 1 + m + m 2 + ⋅ ⋅ ⋅ + m h − 1 ) = m h − 1 ⇒ h ≥ l o g m ( n + 1 ) n\le(m-1)(1+m+m^2+···+m^{h-1})=m^h-1\Rightarrow h\ge log_m(n+1) n≤(m−1)(1+m+m2+⋅⋅⋅+mh−1)=mh−1⇒h≥logm(n+1)
n + 1 ≥ 2 ( ⌈ m 2 ⌉ ) h − 1 ⇒ h ≤ l o g ⌈ m 2 ⌉ n + 1 2 + 1 n+1\ge2(\lceil\frac{m}2\rceil)^{h-1}\Rightarrow h\le log_{\lceil\frac{m}2\rceil}\frac{n+1}2+1 n+1≥2(⌈2m⌉)h−1⇒h≤log⌈2m⌉2n+1+1
3. B树的查找
两个基本操作
- 在B树中查找结点(磁盘上进行)
- 在结点内找关键字(内存中进行)
B树常存储在磁盘上,找到目标结点后,先将结点信息读入内存,然后在结点内采用顺序查找或折半查找
查找步骤
- 先在有序表中进行查找,若找到则成功,否则按照对应的指针信息到所指的子树中查找
- 查到叶结点时(对应指针为空),则说明树中没有对应的关键字,查找失败
4. B树的插入
插入过程
- 定位。利用B树查找算法,找出插入该关键字的最低层中的某个非叶结点(插入位置一定是最低层中的某个非叶结点)
- 插入。每个非失败结点的关键字个数都在 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil\frac{m}2\rceil-1, m-1] [⌈2m⌉−1,m−1]内,若插入后的结点关键字个数小于m,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的关键字个数大于m-1时,必须对结点进行分裂
- 分裂:取一个新结点,在插入key后的原结点,从中间位置( ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置( ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度+1
5. B树的删除
- 当被删关键字k不在终端结点时,可以用k的前驱(或后继)k‘来代替k,然后在相应的结点中删除k’,关键字k‘必定落在某个终端结点中,则转换成了被删关键字在终端结点中的情形
当被删关键字在终端结点中时,有下列三种情况
- 直接删除关键字。若被删除关键字所在结点的关键字个数 ≥ ⌈ m 2 ⌉ \ge\lceil\frac{m}2\rceil ≥⌈2m⌉,表明删除该关键字后仍满足B树的定义,则直接删除该关键字
- 兄弟够借。若被删除关键字所在结点删除前的关键字个数= ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉-1,且与此结点相邻的右(或左)兄弟结点的关键字个数 ≥ ⌈ m 2 ⌉ \ge\lceil\frac{m}2\rceil ≥⌈2m⌉,则需调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡
兄弟不够借。若被删除关键字所在结点删除前的关键字个数= ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉-1,且与此结点相邻的左、右兄弟结点的关键字个数均= ⌈ m 2 ⌉ − 1 \lceil\frac{m}2\rceil-1 ⌈2m⌉−1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并
合并:双亲结点的关键字个数会-1
- 若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根
- 若双亲结点不是根结点,且关键字个数减少到 ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉-2,则又要与它自己的兄弟结点进行调整或合并操作
2. B+树
一棵m阶的B+树需满足以下条件
- 每个分支结点最多有m棵子树(孩子结点)
- 非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ m 2 ⌉ \lceil\frac{m}2\rceil ⌈2m⌉棵子树(所有子树高度要相同)
- 结点的子树个数与关键字个数相等
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻结点按大小顺序相互链接起来
- 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针
- 主要差异
4. 散列表
1. 基本概念
- 散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key) = Addr
- 冲突:散列函数把两个或两个以上的不同关键字映射到同一地址
- 散列表:根据关键字直接进行访问的数据结构(建立了关键字和存储地址之间的一种直接映射关系)
2. 散列函数的构造方法
1. 注意事项
- 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
- 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生
- 散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址
2. 常用函数
直接定址法
- 定义:直接取关键字的某个线性函数值为散列地址,取H(key) = key 或 H(key) = a*key + b
- 优点:最简单且不会发生冲突
- 缺点:若关键字分布不连续,空位较多,则会造成存储空间的浪费
- 适用情况:关键字的分布基本连续
除留余数法
- 定义:取一个不大于表长m但最接近或等于m的质数p,取H(key) = key % p
- 优点:最简单
- 关键:选好质数p,使得分布更均匀,冲突更少
数字分析法
- 定义:设关键字是r进制数,选取数码分布较为均匀的若干位作为散列地址
- 适用情况:已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数
平方取中法
- 定义:取关键字的平方值的中间几位作为散列地址
- 优点:散列地址与关键字的每位都有关系,因此使得散列地址分布较均匀
- 适用情况:关键字的每位取值都不够均匀或小于散列地址所需的位数
3. 处理冲突的方法
任何设计出来的散列函数都不可能绝对地避免冲突。因此,必须在发生冲突时为产生冲突的关键字寻找下一个“空”的Hash地址。 H i H_i Hi表示处理冲突中第i次探测得到的散列地址
1. 开放地址法
定义
- 可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义表项开放
递推式
H i = ( H ( k e y ) + d i ) % m H_i = (H(key) + d_i) \% m Hi=(H(key)+di)%m- H(key)为散列函数,i=0, 1, 2, … , k(k ≤ \le ≤m-1)
- m表示散列表表长
- d i d_i di为增量序列
注意
- 不能随便物理删除表中的元素,因为会截断其他具有相同散列地址的元素的查找地址
- 删除一个元素时,可给它做一个删除标记,进行逻辑删除,副作用:执行多次删除后,表面上看起来散列表很满,实际上许多位置未利用,需要定期维护散列表
- 空位置的判断也算作一次比较
确定增量序列的方法
线性探测法
- 定义: d i = 0 , 1 , 2 , … , m − 1 d_i = 0, 1, 2, … , m-1 di=0,1,2,…,m−1
- 特点:冲突发生时,顺序查看表中下一个单元(循环),直到找出一个空闲单元或查遍全表
- 缺点:造成大量元素在相邻的散列地址上“堆积”起来,大大降低了查找效率
平方探测法
- 定义: d i = 0 2 , 1 2 , − 1 2 , … , k 2 , − k 2 ( k ≤ m 2 ) d_i=0^2, 1^2, -1^2, … , k^2, -k^2(k\le\frac{m}2) di=02,12,−12,…,k2,−k2(k≤2m)
- 特点:表长m必须是一个可以表示为4k+3的素数,否则不能探测所有表格
- 优点:解决了堆积问题
- 缺点:不能够探测到散列表上的所有单元
再散列法
- 定义:多准备几个散列函数,发生冲突时,用下一个散列函数计算一个新地址,直到不冲突为止
伪随机序列法
- 定义: d i = d_i= di=伪随机序列
2. 拉链法(链接法)
- 定义:把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识
- 适用情况:适用于经常进行插入和删除的情况
- 特点:空指针的判断不算作一次比较
4. 散列查找及性能分析
执行过程
- 初始化:Addr = Hash(key);
- 检测查找表中地址为Addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与key的值,若相等,则返回查找成功标志,否则执行步骤3
- 用给定的处理冲突方法计算下一个散列地址,并把Addr置为此地址。转入步骤2
性能分析
- 散列表的查找过程仍然是一个给定值和关键字进行比较的过程
- 查找效率取决于:散列函数、处理冲突的方法、装填因子
- 装填因子:记为 α \alpha α,表示一个表的装满程度,即 α = 表 中 记 录 数 n 散 列 表 长 度 m \alpha = \frac{表中记录数n}{散列表长度m} α=散列表长度m表中记录数n
- 散列表的平均查找长度依赖于装填因子,而不直接依赖于n或m
还没有评论,来说两句吧...