查找
1、查找的基本概念
-
查找
-
查找表(查找结构)
typedef struct{ //查找表的数据结构 ElemType *elem; //元素存储空间基址,建表时按照实际长度分配,0 号单元留空 int TableLen; //表的长度 }SSTable;
-
静态查找表
适合静态查找表的查找方法有:顺序查找、折半查找、散列查找等;
适合动态查找表的查找方法有:二叉排序树的查找(二叉平衡树和 B 树都是二叉排序树的改进)、散列查找等。
-
关键字
-
平均查找长度
在查找的过程中,一次查找的长度是指需要比较的关键字的次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值。数学定义为:
A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^{n}P_iC_i ASL=i=1∑nPiCi
式中,n 是查找表的长度;Pi 是查找第 i 个数据元素的概率;Ci 是找到第 i 个数据元素所需要进行比较的次数。平均查找长度是衡量查找算法效率的最主要指标。
2、顺序查找和折半查找
2.1、顺序查找
基本思想:从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定的条件,则查找成功,返回该元素在线性表中的位置;若已查找到表的另一端,还没有找到符合给定条件的元素,则返回查找失败的信息。
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 循环
}
引入了 “哨兵”,将 ST.elem[0] 称为 “哨兵”。目的是防止循环越界。
查找成功时,顺序查找的平均查找长度为:
A
S
L
成
功
=
∑
i
=
1
n
P
i
(
n
−
i
+
1
)
ASL_{成功}=\sum_{i=1}^{n}P_i(n-i+1)
ASL成功=i=1∑nPi(n−i+1)
当每个元素的查找概率相等时,即 Pi = 1/n,则有:
A
S
L
成
功
=
∑
i
=
1
n
P
i
(
n
−
i
+
1
)
=
n
+
1
2
ASL_{成功}=\sum_{i=1}^{n}P_i(n-i+1)=\frac{n+1}{2}
ASL成功=i=1∑nPi(n−i+1)=2n+1
查找不成功时,顺序查找不成功的平均查找长度为 ASL不成功 = n + 1。
2.2、折半查找(二分查找)
前提条件:有序的顺序表。
基本思路:首先将给定值 key 与表中中间位置的元素的关键字进行比较,若相等,则查找成功,返回该元素的存储位置;若不相等,则所需查找的元素只能在中间元素以外的前半部分或后半部分中。然后在缩小的范围内继续进行同样的查找,如此重复直到找到为止,或者确定表中没有所需要的元素,则查找不成功,返回失败信息。
int Binary_Search(SeqList L,ElemType key){
//在有序表 L 中查找关键字为 key 的元素,若存在则返回其位置,不存在返回 -1
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;
}
折半查找的过程可用二叉树来表示,称为判定树。树中每个圆形结点表示一个记录,结点中的值为该记录的关键字值;树中最下面的叶结点都是方形的,它表示查找不成功的情况。从判定树可以看出,查找成功时的查找长度为从根结点到目的结点的路径上的结点数,而查找不成功时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数;每个结点值均大于其左子树结点值,且均小于其右子树结点值。若有序序列有 n 个元素,则对应的判定树有 n 个圆形的非叶子结点和 n+1 个方形的叶结点。
有序序列为:[7,10,13,16,19,29,32,33,37,41,43]。判定树如下:
折半查找的时间复杂度为 O(log2n)。
在上图中,等概率的情况下,查找成功的 ASL = (1×1+2×2+3×4+4×4)/11 = 3,查找失败的 ASL = (3×4+4×8)/12 = 11/3。
3、B 树和 B+ 树
3.1、B 树
B 树,又称为多路平衡查找树,B 树中所有结点的孩子结点数最大值称为 B 数的阶,通常用 m 表示。一棵 m 阶 B 树或为空树,或为满足如下特性的 m 叉树:
-
树中每个结点至多有 m 棵子树(即至少含有 m-1 个关键字);
-
若根结点不是终端结点,则至少有两棵子树;
-
除根结点外的所有非叶结点至少有 m/2 棵子树(即至少含有 m/2 -1 个关键字)。
-
所有非叶结点的结构如下:
其中,Ki (i=1,2,…,n) 为结点的关键字,且满足 K1 < K2 < … < Kn;Pi (i = 0,1,…,n) 为指向子树根结点的指针,且指针 Pi-1 所指子树中所有结点的关键字均小于 Ki,Pi 所指子树中所有结点的关键字均大于 Ki,n (m/2 -1 <= n <= m-1)为结点中关键字的个数。
- 所有叶结点都出现在同一层上,并且不携带信息;
B 树是所有结点的平衡因子均等于 0 的多路查找树。如图,是一棵 3 阶 B 树,其中底层方形结点表示叶结点,在这些结点中没有存储任何信息。
3.1.1、B 树的查找
B 树的查找分为两个基本操作:1、在 B 树中找结点;2、在结点中找关键字。
3.1.2、B 树的插入
将 key 插入到 B 树中的过程如下:
-
定位:利用前面的 B 树查找算法,找出插入该关键字的最底层中某个非叶结点(B 树的插入一定是插入到最底层中的某个非叶结点内);
-
插入:在 B 树中,每个非叶结点的关键字个数都在 [m/2 - 1,m-1]之间。当插入后的结点关键字个数小于 m,则可以直接插入;插入后检查被插入结点内关键字的个数,当插入的结点关键字个数大于 m-1 时,则必须对结点进行分裂。
分裂的方法是:取一个新结点,将插入 key 后的原结点从中间将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放在新结点中,中间位置的结点插入到原结点的父结点中。若父结点也超过了上限,继续分裂。
3.1.3、B 树的删除
当所删除的关键字 k 不在终端结点(最底层非叶结点)中时,有以下几种情况:
-
如果小于 k 的子树中关键字个数 > m/2 -1,则找出 k 的前驱 k’,并利用 k’ 来取代 k,再递归地删除 k’ 即可;
-
如果大于 k 的子树中关键字个数 > m/2 -1,则找出 k 的后继 k’,并利用 k’ 来取代 k,再递归地删除 k’ 即可;
-
如果前后两个子树中关键字个数均为 m/2 -1,则直接将两个子结点合并,直接删除 k 即可。
当删除的关键字在终端结点(最底层的非叶结点)中时,有以下几种情况:
-
直接删除关键字:若被删除关键字所在的关键字个数 > m/2 -1,表名删除该关键字后仍满足 B 树的定义,则直接删除;
-
兄弟够借
- 兄弟不够借
3.2、B+ 树
B+ 树是应数据库所需而出现的一种 B 树的变形树。
一棵 m 阶的 B+ 树需满足以下条件:
- 每个分支结点最多有 m 棵子树(子结点);
- 非叶结点至少有两棵子树,其他每个分支结点至少有 m/2 棵子树;
- 结点的子树个数与关键字个数相同;
- 所有叶结点包含全部关键字及指向相应记录的指针,而且叶结点中将关键字按大小顺序排列,并且相邻结点按大小顺序相互链接起来;
- 所有分支结点(可以看成索引的索引)中仅包含于它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针。
m 阶 B 树和 m 阶 B+ 树的主要差异在于:
- 在 B+ 树中,具有 n 个关键字的结点只含有 n 棵子树,即每个结点对应一棵子树;而在 B 树中,具有 n 个关键字的结点含有 (n+1) 课子树;
- 在 B+ 树中,每个结点关键字个数为 m/2 <= n <= m(根结点:1<= n <= m),在 B 树中,每个结点关键字个数为 m/2 - 1 <= n <= m-1(根结点:1 <= n <= m-1);
- 在 B+ 树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不包含该关键字对应记录的存储地址;
- 在 B+ 树中,叶结点包含了全部的关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在 B 树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
4、散列(Hash)表
4.1、散列表的基本概念
-
散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为 Hash(key) = Addr。(这里的地址可以是数组下标、索引、或内存地址等)
散列函数可能会把两个或两个以上的不同的关键字映射到同一个地址,称这种情况为 “冲突”,这些发生碰撞的不同关键字称为同义词。
-
散列表:是根据关键字而直接进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
理想情况下,对散列表进行查找的时间复杂度为 O(1),即与表中元素个数无关。
4.2、散列函数的构造方法
-
直接地址法
H ( k e y ) = a × k e y + b H(key)=a\times key+b H(key)=a×key+b
不会产生冲突。 -
除留余数法
H ( k e y ) = k e y % p H(key)=key \% p H(key)=key%p -
数字分析法
-
平方取中法
-
折叠法
4.3、处理冲突的方法
4.3.1、开放地址法
所谓的开放地址法,指的是可存放新表项的空闲地址既向它的同义词开放,又向它的非同义词开放。其数学公式为:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
%
m
H_i=(H(key)+d_i)\% m
Hi=(H(key)+di)%m
式中,i = 0,1,…,k;m 表示散列表表长;di 为增量序列。
-
线性探测法
当 di = 0,1,…,m-1,称为线性探测法。
-
平方探测法
di = 02,12,-12,22,-22,…,k2,-k2,又称为二次探测。
-
再散列法
di = Hash2(Key),又称为双散列法。
-
伪随机序列法
di = 伪随机序列。
4.3.2、拉链法
把同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。
例如,关键字序列为 {19,14,23,01,68,20,84,27,55,11,10,79},散列函数 H(Key) = key % 13,用拉链法处理冲突:
4.4、散列查找及性能分析
散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。
装填因子:散列表的装填因子一般记为 α,定义为一个表的装满程度,即:
α
=
表
中
的
记
录
数
n
散
列
表
长
度
m
\alpha=\frac{表中的记录数n}{散列表长度m}
α=散列表长度m表中的记录数n
散列表的平均查找长度依赖于散列表的装填因子 α,而不直接依赖于 n 或 m。