本文来源于《数据结构考研复习指导》,仅做笔记记录用。
文章目录
基本概念
查找
在数据集合中寻找满足某种条件的数据元素的过程称为查找。查找的结果一般分为两种:一是查找成功;二是查找失败。
查找表(查找结构)
用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成,可以是一个数组或链表等数据类型。对于查找表的操作一般有4种:
- 查询某个特定的数据元素是否在查找表中。
- 检查满足条件的某个特定的数据元素的各种该属性。
- 在查找表中插入一个数据元素。
- 从查找表中删除某个数据元素。
静态查找表
若一个查找表的操作只涉及上述操作的1和2,则无需动态地查找表,此类查找表称为静态查找表。与此相对,需要动态地插入或删除的查找表称为动态查找表。适合静态查找表的查找方式有顺序查找、折半查找、散列查找等;适合动态查找表的查找方法有二叉排序树的查找、散列查找等。
关键字
数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
平均查找长度
在查找过程中,一次查找的长度时指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为
A
S
L
=
∑
i
=
1
n
P
i
C
i
ASL=\sum_{i=1}^nP_iC_i
ASL=i=1∑nPiCi
式中,n是查找表的长度;Pi是查找第i个数据元素的概率,一般认为每个数据元素的查找概率相等,即Pi=1/n;Ci是找到第i个数据元素所需进行的比较次数。
顺序查找
顺序查找又称线性查找,主要用于在线性表中查找。
一般线性表的顺序查找
基本思想
从线性表的一端开始,逐个关键字检查是否满足给定的条件。
算法实现
typedef struct{ // 查找表的数据结构
ElemType *elem; // 元素存储空间基址,建表时按实际长度分配,0号单元留空
int TableLen; // 表的长度
} SSTable;
int Search_Seq(SSTable ST, ElemType key) {
ST.elem[0] = key; // “哨兵”
for (i = ST.TableLen; ST.elem[i] != key; --i);
// 从后往前找
return i;
// 若表中不存在关键字为key的元素,将查找到的i为0时推出for循环
}
平均查找长度
对于有n个元素的表,给定值key与表中第i个元素相等时,即定位第i个元素时,需进行n - i + 1次比较,即Ci = n - i + 1。
- 查找成功时,顺序查找的平均长度为
A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) = n + 1 2 ASL_{成功}=\sum_{i=1}^nP_i(n-i+1)=\frac{n+1}{2} ASL成功=i=1∑nPi(n−i+1)=2n+1 - 查找不成功时,与表中各关键字的比较次数显然是n + 1次,从而顺序查找不成功的平均查找长度为
A S L 不 成 功 = n + 1 ASL_{不成功}=n+1 ASL不成功=n+1
有序表的顺序查找
若在查找之前就已经知道表是关键字有序的,则查找失败时可以不用再继续比较到表的另一端就能返回失败的信息,从而降低顺序查找失败的平均查找长度。
操作
可以用判定树来描述有序顺序表的查找过程。树中的圆形结点表示有序顺序表存在的元素;树中的矩形结点称为失败结点(若有n个结点,则相应地会有n+1个查找失败结点),若查找到失败结点,则说明查找不成功。
平均查找长度
- 查找成功:平均长度为
A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) = n + 1 2 ASL_{成功}=\sum_{i=1}^nP_i(n-i+1)=\frac{n+1}{2} ASL成功=i=1∑nPi(n−i+1)=2n+1 - 查找失败:平均查找长度在相等查找概率的情形下为
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
式中,qj是到达第j个失败结点的概率,在相等查找概率下,它为1/(n + 1)。
折半查找
折半查找又称二分查找,它仅适合与有序的顺序表。
基本思想
首先将给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分。然后在缩小范围内继续进行同样查找,如此重复,直到找到位置或确定表中没有所需要的元素,则查找不成功。
算法实现
int Binary_Search(SeqList L, ElemType key) {
int low = 0, high = L.TableLen - 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个方形的叶结点。
平均查找长度
- 查找成功时:平均查找长度为
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_{i=1}^nl_i\\ =\frac{1}{n}(1\times 1+2\times 2+...+h\times 2^{h-1}\\ =\frac{n+1}{n}log_2(n+1)-1\\ ≈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
式中,h是树的高度,并且元素个数为n时树高h=┌log2(n+1)┐。所以折半查找时间复杂度为O(log2n)。
分块查找
分块查找又称索引顺序查找。
基本思想
将查找表分为若干子块。块内元素可以无序,但块之间必须有序。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块第一个元素的地址,索引表按关键字有序排列。
过程
- 在索引表中确定待查记录所在的块。
- 在块内查找。
平均查找长度
设索引查找和块内查找的长度分别为LI,LS。将长度为n的查找表均匀地分成b块,每块有s个记录,在等概率的情况下,若在块内和索引表中均用顺序查找,则平均查找长度为
A
S
L
=
L
I
+
L
S
=
b
+
1
2
+
s
+
1
2
=
s
2
+
2
s
+
1
2
s
ASL=L_I+L_S=\frac{b+1}{2}+\frac{s+1}{2}=\frac{s^2+2s+1}{2s}
ASL=LI+LS=2b+1+2s+1=2ss2+2s+1
此时,若s=√n,则平均查找长度最小值为√n+1;若对索引表采用折半查找时,则平均查找长度为
A
S
L
=
L
I
+
L
S
=
┌
l
o
g
2
(
b
+
1
)
┐
+
s
+
1
2
ASL=L_I+L_S=┌log~2~(b+1)┐+\frac{s+1}{2}
ASL=LI+LS=┌log 2 (b+1)┐+2s+1
B树
B树的定义
B树又称多路平衡查找树,B树中所有孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特征的m叉树:
- 树中每个结点至多有m棵子树,即至多含有m-1个关键字。
- 若根结点不是终端结点,则至少有两棵子树。
- 除根结点外的所有非叶结点至少有┌m / 2┐棵子树,即至少含有┌m / 2┐-1个关键字。
- 所有非叶结点的结构如下:
其中,Ki(i=1,2,…,m)为结点的关键字,且满足K1<K2<…<Kn;Pi(i=0,1,…,n)为指向子树结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki,n(┌m / 2┐ - 1 ≤ n ≤ m - 1)为结点中关键字的个数。
- 所有的叶结点都出现在同一层次上,并且不带信息。
B树的高度(磁盘存取次数)
B树的高度不包括最后的不带任何信息的叶结点所处的那一层
若n ≥ 1,则对任意一棵包含n个关键字、高度为h、阶数为m的B树:
-
因为B树中每个结点最多有m棵子树,m - 1个关键字,所以在一棵高度为h的m阶B树中关键字的个数应满足
n ≤ ( m − 1 ) ( 1 + m + m 2 + . . . + m h − 1 ) = m h − 1 h ≥ l o g m ( n + 1 ) n\leq (m-1)(1+m+m^2+...+m^{h-1})=m^h-1\\ h\geq log_m(n+1) n≤(m−1)(1+m+m2+...+mh−1)=mh−1h≥logm(n+1) -
若让每个结点中的关键字个数最少,则容纳同样多关键字的B树的高度达到最大。由B树的定义:第一层至少有1个结点;第二层至少有2个结点;除根结点外每个非终端结点至少有┌m / 2┐棵子树,则第三层至少有2┌m / 2┐个结点…第h+1层至少有2(┌m / 2┐)h-1个结点。(第h+1层是不包含任何信息的叶结点)对于关键字个数为n的B树,叶结点有n+1个,因此有
n + 1 ≥ 2 ( ┌ m / 2 ┐ ) h − 1 h ≤ l o g ┌ m / 2 ┐ ( ( n + 1 ) / 2 ) + 1 n+1\geq 2(┌m / 2┐)^{h-1}\\ h \leq log_{┌m / 2┐}((n+1)/2)+1 n+1≥2(┌m/2┐)h−1h≤log┌m/2┐((n+1)/2)+1
B树的查找
基本操作
- 在B树上找结点。
- 在结点内找关键字。由于B树常存储在磁盘上,因此前一个查找操作是在磁盘上进行的,而后一个查找操作是在内存中进行的。即在找到目标结点后,先将结点读入内存,然后在结点内采用顺序查找法或折半查找法。
B树的插入
过程
- 定位。利用B树查找算法,找出插入该关键字的最底层的某个非叶结点。
- 插入。在B树中,每个非失败结点的关键字个数都在区间 [┌m / 2┐ - 1, m - 1] 内。当插入后的结点关键字个数小于m,则可以直接插入;插入后检查被插入结点内关键字个数,当插入后的结点关键字个数大于m-1时,必须对结点进行分裂。
分裂的方法
取一个新结点,在插入key后的原结点,从中间位置(┌m / 2┐)将其中的关键字分为两部分,左部分包含的关键字放在源点中,右部分包含的关键字放在新结点中,中间位置(┌m / 2┐)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行分裂,直到这个过程传到根结点,使整个B树高度增加1。
B树的删除
B树的删除要注意删除后的结点的关键字个数≥┌m / 2┐-1。
关键字k为非终端结点
此时可以用k的前驱(或后继)k’来替代k,然后在相应的结点中删除k’。关键字k’必定落在某个终端结点中,则转换成了被删关键字在终端结点的情形。
关键字K为终端结点
当被删关键字在终端结点(最底层非叶结点)中时,有下列三种情况:
- 直接删除关键字。若被删除关键字所在结点关键字个数≥┌m / 2┐,表明删除该关键字后仍能满足B树的定义,则直接删去关键字。
- 兄弟够借。若被删除关键字索塞的结点删除前关键字个数=┌m / 2┐ - 1,且与此结点相邻的右(或左)兄弟结点的关键字个数≥┌m / 2┐,则需要调整台节点、右(或左)兄弟结点及其双亲结点(父子换位)以达到新的平衡。
- 兄弟不够借。若被删除关键字所在结点删除前的关键字个数=┌m / 2┐-1,且此时与该结点相邻的左、右兄弟结点的关键字个数均=┌m / 2┐-1,则将关键字删除后与左(或右)兄弟及双亲结点中的关键字合并。
B+树
基本概念
一棵m阶的B+树需要满足下列条件:
- 每个分支结点最多有m棵子树(孩子结点)。
- 非叶根结点至少有两棵子树,其他每个分支结点至少有┌m / 2┐棵子树。
- 结点的子树个数和关键字个数相等。
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接。
- 所有分支结点(可视作索引的索引)中仅包含它的各个子结点(即下一块索引块)中关键字的最大值及指向其子节点的指针。
B+树查找时,无论成功与否,每次查找都是一条从根结点到叶结点的路径。
M阶B+树与M阶B树的差异
- B+树中,具有n个管家你在的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有n+1棵子树。
- B+树中,每个结点(非根内部结点)的关键字个数n的范围是┌m / 2┐ ≤ n ≤ m(根结点则是1 ≤ n ≤ m);在B树中,每个结点(非根内部结点)的关键字个数n的范围是┌m / 2┐-1 ≤ n ≤ m-1(根结点:1 ≤ n ≤ m-1)。
- 在B+树中,叶结点包含信息,所有非叶结点仅起到缩阴作用,每个结点的每个索引项只含对应子树的最大关键字和指向该子树的指针,不含有该管家你在对应的记录的存储地址。
- 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
散列表
基本概念
散列函数
一个把查找表中关键字映射成该关键字对应地址的函数,记为Hash(key)=Addr(这里的地址可以是数组下标、索引或内存地址等)。
冲突
散列函数可能把两个或以上的不同关键字映射到用以地址,称这种情况叫冲突,这些发生碰撞的不同关键字称为同义词。
散列表
根据关键字而直接进行访问的结构。散列表建立了关键字和存储地址的一种直接映射关系。
理想情况下,对散列表的查找时间复杂度为O(1),即与表中元素个数无关。
常见散列函数
直接定址法
直接取关键字的某个线性函数值为散列地址,散列函数为
H
(
k
e
y
)
=
k
e
y
或
H
(
k
e
y
)
=
a
×
k
e
y
+
b
H(key)=key或H(key)=a\times key+b
H(key)=key或H(key)=a×key+b
式中,a和b是常数。这种适合关键字分布基本连续的情况。否则空位会多,造成存储空间浪费。
除留余数法
假设散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换为散列地址。公式为
H
(
k
e
y
)
=
k
e
y
%
p
H(key)=key\%p
H(key)=key%p
数字分析法
设关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀,每种数码出现机会均等;而某些位上分布不均匀。此时应选取数码分布均匀的若干位作为散列地址。这种方法适合已知的关键字集合,若更换了关键字,则需重新构造散列函数。
平方取中法
这种方法取关键字的平方值的中间几位作为散列地址。这种方法适合关键字的每位取值都不够均匀或均小于散列地址所需的位数。
处理冲突的方法
开放定址法
所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,有向它的非同义词表项开放。数学递推公式为
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≤m-1);m表示散列表表长;di为增量序列。增量序列通常有4中取法:
线性探测法
当di=0,1,2,…,m-1时,称为线性探测法。特点是:冲突发生时,顺序查看表中下一个元素,直到找出一个空闲单元或查遍全表。
平方探测法
当di=02,12,-12,22,-22,…,k2,-k2,称为平方探测法。其中k≤m/2,散列表长度m必须是可以表示成4k+3的素数,又称二次探测法。
它的缺点是不能探测到散列表的所有单元。
再散列法
当di=Hash2(key)时,称为再散列法,又称双散列法。通过第一个散列函数H(key)得到的地址冲突时,再利用第二个散列函数Hash2(key)计算该关键字的地址增量。它的具体散列函数形式如下。
H
i
=
(
H
(
k
e
y
)
+
i
×
H
a
s
h
2
(
k
e
y
)
)
%
m
H_i=(H(key)+i\times Hash_2(key))\% m
Hi=(H(key)+i×Hash2(key))%m
初始探测位置H0=H(key)%m。i是冲突的位置,初始为0。在再散列法中,最多经过m-1次探测就会遍历表中所有位置,回到H0。
伪随机序列法
当di=伪随机序列时,称为伪随机序列法。
拉链法
为了避免非同义词冲突,可以把鄋同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。拉链法适合用于经常进行插入和删除的情况。
性能分析
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法、填装因子。
填装因子
散列表的填装因子一般记为α,定义为一个表的装满程度,即
α
=
表
中
记
录
数
n
散
列
表
长
度
m
\alpha=\frac{表中记录数n}{散列表长度m}
α=散列表长度m表中记录数n
散列表的平均查找长度依赖于散列表填装因子α。
错题整理
- 由n个数据元素组成的两个表:一个递增有序,一个无序。采用顺序查找算法,对有序表从头开始查找,发现当前元素已不小于待查元素,停止查找,确定查找不成功,已知查找任一元素的概率是相同的,则在两种表中成功查找的平均时间两者相同。
对于顺序查找,不管线性表是有序的还是无序的,成功查找第一个元素的比较次数为1,成功查找第二个元素的比较次数为2,以此类推,每个元素查找成功的次数只与其位置有关(与是否有序无关)。
- 对表长为n的有序表进行折半查找,其判定树的高度为┌log2(n+1)┐。
对n个结点的判定树,设结点总数n=2h-1,则h=┌log2(n+1)┐。
- 对2500个记录的索引顺序表(分块表)进行查找,最理想的块长是50。
设块长为b,索引表包含n/b项,索引表的ASL=(n / b+1)/2,总ASL=索引表的ASL+块内的ASL=(b + n / b + 2)/ 2。其中对于b + n / b,由均值不等式得知b = n / b时有最小值,此时b=√n。则最理想的块长为√2500=50。
- 下列二叉树中,可能成为折半查找的判定树的是(A)
B选项4、5相加除以2向上取整,7、8相加除以2向下取整,矛盾;
C选项,3、4相加除以2向上取整,6、7相加除以2向下取整,矛盾;
D选项,1、10相加除以2向下取整,6、7相加除以2向上取整,矛盾。
- B+树不同于B树的特点之一是由于B+树的所有叶结点包含全部关键字信息,且叶结点本身依关键字从小到大顺序连接,因此可以进行顺序查找,而B树不支持顺序查找。
- 只能在顺序存储结构上进行的方法是折半查找法。
折半查找只能顺序存储且要求关键字有序。
- 用哈希(散列)方法处理冲突(碰撞)时可能出现堆积(聚集)现象,平均查找长度会直接受堆积影响。