文章目录
七、查找
7.1 查找的基本概念
- 查找。数据集合中寻找满足某种条件的数据元素的过程。结果一般分为两种:查找成功和查找失败。
- 查找表,查找结构。用于查找的数据集合,由同一类型的数据元素(记录)组成,可以是一个数组或链表等数据类型。对查找表的操作一般有4种:
- 查询某个特定的数据元素是否在查找表中。
- 检索满足条件的某个特定的数据元素的各种属性。
- 在查找表中插入一个数据元素。
- 从查找表中删除某个元素。
- 静态查找表。无须动态修改查找表的查找表,与之对应的是动态查找表。适合静态查找表的查找方法有顺序查找、折半查找、散列查找等。适合动态查找表的查找方法有二叉排序树的查找、散列查找等。
- 关键字。数据元素中唯一表示该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
- 平均查找长度。在查找过程中,一次查找的长度是指需要比较的关键字次数。平均查找长度是所有查找过程中进行关键字的比较次数的平均值。数学定义为 A S L = ∑ i = 1 n P i C i ASL = \sum_{i=1}^nP_iC_i ASL=∑i=1nPiCi。式中, P i P_i Pi 是查找第 i i i 个数据元素的概率,一般人为每个数据元素的查找概率相等,即 P i = 1 n P_i = \frac{1}{n} Pi=n1; C i C_i Ci 是找到第 i i i 个数据元素所需要进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。
7.2 顺序查找
引入“哨兵”。
typedef struct{
ElemType *elem;
int TableLen;
} SSTable;
int Search_Seq(SSTable ST, ElemType key){
ST.elem[0] = key; // 0号位置为哨兵
int i;
for(i = ST.TableLen;ST.elem[i] != key;--i);
return 0;
}
A S L 成 功 = n + 1 2 ; A S L 不 成 功 = n + 1 ASL_{成功} = \frac{n+1}{2};\ \ ASL_{不成功} = n+1 ASL成功=2n+1; ASL不成功=n+1。
顺序查找判定树:
7.3 折半查找
又称二分查找,仅适用于有序的顺序表。
int BinarySearch(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;
if(L.elem[mid] > key) high = mid - 1;
else low = mid + 1;
}
return -1; // 未找到
}
折半查找判定树:
判定树是一棵平衡二叉树。
折半查找查找成功平均查找长度为: A S L = l o g 2 ( n + 1 ) − 1 ASL = log_2(n+1)-1 ASL=log2(n+1)−1。
复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。
7.4 分块查找
又称索引顺序查找。吸取了顺序查找和折半查找的各自的优点,既有动态结构,又适于快速查找。
基本思想:
- 将查找表分为若干块。
- 块内可以无序,块间必需有序。每一个块中的最大关键字必需小于下一个块中的所有记录的关键字。
- 建立索引表,索引表按关键字有序排列。
查找分两步,第一步在索引表中确定待查记录所在的块,可以顺序查找或折半查找。第二步是在块内顺序查找。
7.5 B树和B+树
7.5.1 B树及其基本操作
B树,又称多路平衡查找树。B树中所有结点的孩子个数的最大值称为B树的阶,通常用 m 表示。
一棵 m 阶B树或为空树,或为 m 叉树,特性:
- 树中每个结点至多有 m 棵子树,即至多含有 m - 1 个关键字。
- 若根结点不是终端结点,则至少含有2棵子树。
- 除根结点外的所有非叶结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil - 1 ⌈m/2⌉−1 个关键字。
- 所有非叶结点的结构如下: n ∣ P 0 ∣ K 1 ∣ P 1 ∣ K 2 ∣ P 2 ∣ . . . . ∣ K n ∣ P n n|P_0|K_1|P_1|K_2|P_2|....|K_n|P_n n∣P0∣K1∣P1∣K2∣P2∣....∣Kn∣Pn。 K i K_i Ki 为关键字,递增。 P i P_i Pi 为指向子树根结点的指针,指向子树中的所有结点的关键字均大于 K i K_i Ki 小于 K i + 1 K_{i+1} Ki+1。
- 所有叶结点都出现在同一层次上,且不带任何信息。
- 所有结点的平衡因子均为 0。
7.5.1.1 B树的高度
每个结点最多有 m 棵子树,所以数中关键字的个数应该满足 n ≤ ( m − 1 ) ( 1 + m + m 2 + . . . + m h ) n\leq (m-1)(1+m+m^2+...+m^h) n≤(m−1)(1+m+m2+...+mh),可以解得 h ≥ l o g m ( n − 1 ) h\geq log_m(n-1) h≥logm(n−1)。
若让每个结点中的关键字最少,则每层至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 个关键字。同理,可得到 h ≤ l o g ⌈ m / 2 ⌉ ( ( n + 1 ) / 2 ) + 1 h \leq log_{\lceil m/2 \rceil} ((n+1)/2)+1 h≤log⌈m/2⌉((n+1)/2)+1
7.5.1.2 B树的查找
类似二叉查找树,但每个结点上所做的是多路分支。
在结点内可以采用顺序查找法或折半查找法。
7.5.1.3 B树的插入
- 定位,查找到最底层非叶结点的查找位置。
- 插入。
- 插入后的结点关键字个数小于 m,直接插入。
- 分裂结点。从中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 处将其中的关键字分成两部分。左部分包含的关键字放到原结点中,右部分放到新结点中,中间位置的结点插入到原结点的父结点中。向上传导。
7.5.1.4 B树的删除
被删关键字 k 不在终端结点中时,可以用 k 的前驱或后继来代替 k,然后在终端结点中删除相关结点。
因此,只需讨论在终端结点中删除的情形。
- 直接删除关键字。关键字个数 ≥ ⌈ m / 2 ⌉ \geq \lceil m/2 \rceil ≥⌈m/2⌉ 时,直接删除。删除后仍满足B 树的性质。
- 兄弟够借。与结点相邻的左或右结点的关键字个数 ≥ ⌈ m / 2 ⌉ \ge \lceil m/2 \rceil ≥⌈m/2⌉,删除后通过父子换位法来达到新平衡。
- 兄弟不够借。此时被删除的结点的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil - 1 =⌈m/2⌉−1,左或右兄弟结点的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil - 1 =⌈m/2⌉−1,则将关键字删除,左右结点 + 父结点中的关键字合并,此时父结点中的关键字个数 - 1。向上传导。
7.5.2 B+树的基本概念
数据库所需的一种B树变形树。
m 阶B+树条件:
- 每个分支结点最多有 m 棵子树。
- 非叶根结点至少有两棵子树,其他每个分支结点至少有 $\lceil m/2 \rceil $ 棵子树。
- 结点的子树个数与关键字个数相等。
- 所有叶结点包含全部关键字及其指向相应记录的指针。叶结点中将关键字排序,并且相邻的叶结点按大小顺序相互连接起来。
- 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
B+树与B 树的区别:
- 在B+树中,具有 n 个关键字的结点只含有 n 棵子树。而B树中有 n+1 棵。
- B+树中,每个结点(非根内部结点)的关键字个数的范围是 ⌈ m / 2 ⌉ ≤ n ≤ m \lceil m/2\rceil \le n \le m ⌈m/2⌉≤n≤m。(根结点可为最小1)而B 树中 ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil m/2\rceil - 1 \le n \le m - 1 ⌈m/2⌉−1≤n≤m−1。
- B+树中,叶结点包含信息,所有非叶结点仅起到检索作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- B+树中,叶结点包含了全部关键字。关键字可同时出现于叶结点和非叶结点中。B树中是不会重复的。
- B+树中查找在非叶结点上找到时仍要向下查找。
- B+树也可顺序查找。
m阶B树 | m阶B+树 | |
---|---|---|
类比 | 二叉查找树的进化 -> m 叉查找树 | 分块查找的进化-> 多级分块查找 |
关键字与分叉 | n 个关键字对应 n+1 个分叉 | n 个关键字对应 n 个分叉 |
结点包含的信息 | 所有结点中都包含记录的信息 | 只有最下层的叶子结点才包含记录的信息(树更矮) |
查找方式 | 不支持顺序查找。查找成功时,可能停在任何一层结点,查找速度不稳定。 | 支持顺序查找。查找成功或失败都会达到最下一层结点,查找速度稳定 |
相同点:除根结点外,最少 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉ 个分叉,确保结点不要太空;任何一个结点的子树都要一样高,确保绝对平衡。
7.6 散列表
7.6.1 散列表的基本概念
线性表和树表的查找中,记录在表中的位置与记录的关键字之间不存在确定关系。因此,在这些表中查找记录时需要进行一系列的关键字比较。这类查找方法建立在“比较”的基础上,查找的效率取决于比较的次数。
散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为 H a s h ( k e y ) = A d d r Hash(key) = Addr Hash(key)=Addr。
散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。一方面,设计得好的散列函数应尽量减少这样的冲突。另一方面,由于这样的冲突是不可避免的,所以还要设计好处理冲突的方法。
散列表:根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
理想情况下,对散列表进行查找的时间复杂度为 O ( 1 ) O(1) O(1),即表中元素的个数无关。
7.6.2 散列函数的构造方法
构造散列函数时,必需注意:
- 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
- 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。
- 散列函数应尽量简单,能够在较短时间内计算出任一关键字对应的散列地址。
常用的散列函数:
- 直接定址法。直接取关键字的某个线性函数值为散列地址,散列函数为: H ( k e y ) = k e y ; H ( k e y ) = a ∗ k e y + b H(key) = key\ ;\ H(key) = a*key+b H(key)=key ; H(key)=a∗key+b。
- 除留余数法。最简单,最常用的方法。假定散列表表长为 m,取一个不大于 m 但最接近或等于 m 的质数 p,利用 H ( k e y ) = k e y H(key) = key%p H(key)=key,来转换成散列地址。关键是选好 p,使得每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性。
- 数字分析法。设关键字是 r 进制数,而 r 个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等,而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布比较均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了数字,则需要重新构造新的散列函数。
- 平方取中法。取关键字的平方值中间的几位作为散列地址。具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每一位都有关系,因此比较均匀。
不同情况下,不同的散列函数具有不同的性能,因此不能笼统地说哪种散列函数最好。目标是为了尽量讲的产生冲突的可能性。
7.6.3 处理冲突的方法
- 开放定址法。指可存放新表项的空闲地址既向它的同义词开放,又向它的非同义词表项开放。
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i=(H(key)+d_i)\%m
Hi=(H(key)+di)%m。
d
i
d_i
di 为增量序列。通常有4种取法。
- 线性探查法。冲突发生时,顺序查看表中下一个单元(循环),直到找出一个空闲单元或查遍全表。可能会造成大量元素在相邻的散列地址上“聚集”,大大降低查找效率。
- 平方探测法。 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 d_i=0^2,1^2,-1^2,2^2,-2^2,...,k_2,-k^2 di=02,12,−12,22,−22,...,k2,−k2时,称为平方探测法。散列表长度 m 必须是一个可以表示成 4k+3 的素数,又称二次探测法。缺点是不能探测到散列表上的所有单元,但至少能够探测到一半单元。
- 再散列法。多准备几个散列函数,散列发生冲突时,用下一个散列函数计算一个新地址,直到不冲突为止。 H i = R H i ( k e y ) i = 1 , 2 , 3 , . . . , k H_i = RH_i(key)\ \ i = 1,2,3,...,k Hi=RHi(key) i=1,2,3,...,k。
- 伪随机数法。增量为伪随机数。
- 拉链法。连接法。chaining。对于不同的关键字可能会通过散列函数映射到同一地址。为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。假设散列地址为 i i i 的同义词链表的头指针存放在散列表的第 i i i 个单元中,因而查找、插入和删除操作主要在同义词链中进行。拉链法适用于经常插入和删除的情况。
7.6.4 散列查找及性能分析
虽然散列表在关键字与记录的存储位置之间建立了直接映像。但由于冲突的产生,是的散列表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量。
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。
装填因子。一般记为 α \alpha α,定义为一个表的装满程度,即 α = n m \alpha = \frac{n}{m} α=mn,n 为表中记录数,m 为散列表长度。
散列表的平均查找长度依赖于散列表的装填因子,而不直接依赖于 n 和 m。 α \alpha α 越大,装填记录越慢,发生冲突的可能性大。