0 总结
0.1 时间复杂度
查找方式 | 时间复杂度 | 存储结构约束 |
---|---|---|
顺序查找(线性查找) | O ( n ) O(n) O(n) | 顺序 / 链式 |
折半查找(二分查找) | O ( l o g 2 n O({log}_2n O(log2n) | 有序、顺序 |
分块查找(索引顺序查找) | 顺序+折半 | 顺序+折半 |
散列查找 | O ( 1 ) O(1) O(1) | 顺序、顺序+链式 |
0.2 ASL
设一个查找集合中已有 n n n个数据元素
p i p_i pi:每个元素的查找概率
c i c_i ci:查找成功的数据比较次数
不在此集合中的数据元素分布在由这 n n n个元素的间隔构成的 n + 1 n+1 n+1个子集合内
q j q_j qj:每个子集合元素的查找概率
c j c_j cj:查找不成功的数据比较次数
-
A
S
L
成
功
=
∑
i
=
1
n
p
i
c
i
{ASL}_{成功}=\sum_{i=1}^n{p_ic_i}
ASL成功=i=1∑npici
A S L 不 成 功 = ∑ j = 0 n q j c j {ASL}_{不成功}=\sum_{j=0}^n{q_jc_j} ASL不成功=j=0∑nqjcj - 若综合考虑
A
S
L
成
功
和
A
S
L
不
成
功
{ASL}_{成功}和{ASL}_{不成功}
ASL成功和ASL不成功:
∑
i
=
1
n
p
i
+
∑
j
=
0
n
q
j
=
1
\sum_{i=1}^n{p_i}+\sum_{j=0}^n{q_j}=1
i=1∑npi+j=0∑nqj=1
若所有元素查找概率相等: p i = q j = 1 2 n + 1 p_i=q_j=\frac{1}{2n+1} pi=qj=2n+11 - 若分开考虑
A
S
L
成
功
和
A
S
L
不
成
功
{ASL}_{成功}和{ASL}_{不成功}
ASL成功和ASL不成功:
∑
i
=
1
n
p
i
=
1
,
∑
j
=
0
n
q
j
=
1
\sum_{i=1}^n{p_i}=1,\sum_{j=0}^n{q_j}=1
i=1∑npi=1,j=0∑nqj=1
若所有元素查找概率相等: p i = 1 n , q j = 1 n + 1 p_i=\frac{1}{n},q_j=\frac{1}{n+1} pi=n1,qj=n+11
1 查找的基本概念
- 查找 :在数据集合中寻找满足某种条件的数据元素的过程
(1)查找成功
(2)查找失败 - 查找表(查找结构):用于查找的数据集合,由同一类型的数据元素(或记录)组成,可以是一个数组或链表等数据类型
- 对查找表的操作
(1)查询某个特定的数据元素是否在查找表中
(2)检索满足条件的某个特定的数据元素的各种属性
(3)在查找表中插入一个数据元素
(4)从查找表中删除某个数据元素 - 静态查找表 :只涉及查询、检索而不涉及插入、操作的查找表
(1)顺序查找
(2)折半查找
(3)散列查找
动态查找表 :涉及查询、检索、插入、操作的查找表
(1)二叉排序树的查找
① 二叉平衡树的查找
② B树的查找
(2)散列查找 - 关键字 :数据元素中唯一标识该元素的某个数据项的值
使用关键字查找,查找结果唯一 - 一次查找长度 :在查找过程中,需要比较的关键字次数
平均查找长度ASL :所有查找过程中进行关键字的比较次数的平均值。 A S L = ∑ i = 1 查 找 表 的 长 度 n 查 找 第 i 个 元 素 的 概 率 P i × 找 到 第 i 个 元 素 所 需 进 行 的 比 较 次 数 C i 一 般 认 为 , P i = 1 / n ASL=\sum_{i=1}^{查找表的长度n}{查找第i个元素的概率P_i×找到第i个元素所需进行的比较次数C_i}\\一般认为,P_i=1/n ASL=i=1∑查找表的长度n查找第i个元素的概率Pi×找到第i个元素所需进行的比较次数Ci一般认为,Pi=1/n
2 顺序查找和折半查找
2.1 顺序查找(线性查找)
- 主要用于在线性表中进行查找
2.1.1 对一般的无序线性表的顺序查找
- “哨兵”:
S
T
.
e
l
e
m
[
0
]
ST.elem[0]
ST.elem[0]
引入哨兵可以避免许多不必要的判断语句,从而提高程序效率
(1)算法实现
/*查找表的数据结构*/
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循环
(2)ASL
- A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) P i = 1 / n 时 , 有 A S L 成 功 = n + 1 2 {ASL}_{成功}=\sum_{i=1}^n{P_i(n-i+1)}\\ P_i=1/n时,有{ASL}_{成功}=\frac{n+1}{2} ASL成功=i=1∑nPi(n−i+1)Pi=1/n时,有ASL成功=2n+1
- A S L 不 成 功 = n + 1 {ASL}_{不成功}=n+1 ASL不成功=n+1
2.1.2 对关键字有序的顺序表的顺序查找
(1)有序顺序表上的顺序查找判定树
(2)ASL
- A S L 成 功 = ∑ i = 1 n P i ( n − i + 1 ) P i = 1 / n 时 ( 相 等 查 找 概 率 ) , 有 A S L 成 功 = n + 1 2 {ASL}_{成功}=\sum_{i=1}^n{P_i(n-i+1)}\\ P_i=1/n时(相等查找概率),有{ASL}_{成功}=\frac{n+1}{2} ASL成功=i=1∑nPi(n−i+1)Pi=1/n时(相等查找概率),有ASL成功=2n+1
- A S L 不 成 功 = ∑ i = 1 n 到 达 第 j 个 失 败 结 点 的 概 率 q j ( 第 j 个 失 败 结 点 所 在 的 层 数 l j − 1 ) q j = 1 n + 1 时 ( 相 等 查 找 概 率 ) , 有 A S L 不 成 功 = n 2 + n n + 1 {ASL}_{不成功}=\sum_{i=1}^n{到达第j个失败结点的概率q_j(第j个失败结点所在的层数l_j-1)}\\q_j=\frac{1}{n+1}时(相等查找概率),有{ASL}_{不成功}=\frac{n}{2}+\frac{n}{n+1} ASL不成功=i=1∑n到达第j个失败结点的概率qj(第j个失败结点所在的层数lj−1)qj=n+11时(相等查找概率),有ASL不成功=2n+n+1n
2.1.3 顺序查找的优缺点
- 优点
(1)对数据元素的存储结构无要求,且对线性的链表只能顺序查找
(2)对表中记录的有序性无要求 - 缺点:当 n n n较大时,平均查找长度较大,效率低
2.2 折半查找(二分查找)
- 仅适用于有序的顺序表
2.2.1 算法实现
int Binary_Search(SeqList L, ElemType key) {
int low = 0;
int mid;
int high = L.TableLen - 1;
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
}
2.2.2 判定树
-
判定树 —— 平衡二叉排序树,但并不意味着任意的平衡二叉树都符合 ⌊ ⌋ \lfloor \rfloor ⌊⌋
-
每个双亲结点在判定树中的位置对应其在有序表中的位置,都是由统一往下取整或往上取整得到
-
折半查找的判定树和平衡二叉树的区别:
判定树 —— 左右子树的结点数最多差1
平衡二叉树 —— 左右子树的高度最多差1 -
相同结点树的折半查找判定树与完全二叉树的高度相同( ⌈ l o g 2 ( n + 1 ) ⌉ \lceil{log}_2(n+1)\rceil ⌈log2(n+1)⌉)
2.2.3 ASL
- 等概率情况下, A S L 成 功 = 1 n ∑ i = 1 h i × 2 i − 1 ( h = ⌈ l o g 2 ( n + 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}^h{i×2^{i-1}}(h=\lceil{log}_2(n+1)\rceil)\\=\frac{n+1}{n}{log}_2(n+1)-1≈{log}_2(n+1)-1 ASL成功=n1i=1∑hi×2i−1(h=⌈log2(n+1)⌉)=nn+1log2(n+1)−1≈log2(n+1)−1
2.2.4 折半查找的优缺点
- 优点:快速查找,平均情况下比顺序查找的效率高
- 缺点:无动态结构,仅适用于有序的顺序存储结构
2.2.5 注意
- 对长度为 n n n的有序表,采用折半查找时,查找成功和查找失败的最多比较次数相同,均为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil{log}_2(n+1)\rceil ⌈log2(n+1)⌉,最少比较次数也相同。
- 但若是求查找成功或查找失败的ASL,需要画出判定树求解
(1) A S L 成 功 {ASL}_{成功} ASL成功,看圆形结点
(2) A S L 不 成 功 {ASL}_{不成功} ASL不成功,看每个方形结点所对应的上面的圆形结点
查找失败结点的ASL不是判定树中的方形结点,而是方形结点上一层的圆形结点
2.3 分块查找(索引顺序查找)【顺序+折半】
2.3.1 算法思想
- 1° 将查找表分为若干子块,块内的元素可无序,块之间有序,每块内最大(或最小)的数据组成索引块
- 2° 在索引表中确定待查记录所在的块,可以顺序或折半查找索引表
- 3° 在块内顺序查找
2.3.2 ASL
-
分块查找示意图
-
A S L 成 功 = 索 引 查 找 的 平 均 查 找 长 度 L I + 块 内 查 找 的 平 均 查 找 长 度 L S {ASL}_{成功}=索引查找的平均查找长度L_I+块内查找的平均查找长度L_S ASL成功=索引查找的平均查找长度LI+块内查找的平均查找长度LS
将 长 度 为 将长度为 将长度为n 的 查 找 表 均 匀 地 分 为 的查找表均匀地分为 的查找表均匀地分为b 块 , 每 块 有 块,每块有 块,每块有s 个 记 录 个记录 个记录。在等概率情况下:
(1)最好情况下的分块: b = s = n b=s=\sqrt{n} b=s=n
(2)👇
- 索引顺序+块内顺序:
A
S
L
成
功
=
【
索
引
表
的
A
S
L
】
b
+
1
2
+
【
块
内
的
A
S
L
】
s
+
1
2
{ASL}_{成功}=【索引表的ASL】\frac{b+1}{2}+【块内的ASL】\frac{s+1}{2}
ASL成功=【索引表的ASL】2b+1+【块内的ASL】2s+1
若 s = n , 则 A S L 成 功 最 小 为 n + 1 若s=\sqrt{n},则{ASL}_{成功}最小为\sqrt{n}+1 若s=n,则ASL成功最小为n+1
s = n / b , A S L = ( b + 1 ) / 2 + ( n / b + 1 ) / 2 s=n/b,ASL=(b+1)/2+(n/b+1)/2 s=n/b,ASL=(b+1)/2+(n/b+1)/2,均值不等式b=n/b时,ASL最小
- 索引折半+块内顺序: A S L 成 功 = 【 索 引 表 的 A S L 】 ⌈ l o g 2 ( b + 1 ) ⌉ + 【 块 内 的 A S L 】 s + 1 2 {ASL}_{成功}=【索引表的ASL】\lceil{log}_2(b+1)\rceil+【块内的ASL】\frac{s+1}{2} ASL成功=【索引表的ASL】⌈log2(b+1)⌉+【块内的ASL】2s+1
- 索引折半+块内折半: A S L 成 功 = 【 索 引 表 的 A S L 】 ⌈ l o g 2 ( b + 1 ) ⌉ + 【 块 内 的 A S L 】 ⌈ l o g 2 ( s + 1 ) ⌉ {ASL}_{成功}=【索引表的ASL】\lceil{log}_2(b+1)\rceil+【块内的ASL】\lceil{log}_2(s+1)\rceil ASL成功=【索引表的ASL】⌈log2(b+1)⌉+【块内的ASL】⌈log2(s+1)⌉
2.3.3 分块查找的优点
顺序的优点+折半的优点
- 动态结构
- 快速查找
3 B树和B+树
这一块几乎都忘了
3.1 B树(多路平衡查找树)
- 常存储在磁盘上
3.1.1 B树概念
- B树 :所有结点的平衡因子均等于0的多路平衡查找树
- B树的高度:有的书定义不包括叶结点那一层,有的书定义包括
- B树的阶 m m m:B树中所有结点的孩子个数的最大值
-
m
m
m阶B树或为空,或为如下特性的
m
m
m叉树:
(1)树中每个结点至多有 m m m棵子树,即至多含有 m − 1 m-1 m−1个关键字
∴结点的孩子个数 = 该结点中关键字个数 + 1
(2)若根结点不是终端结点,则至少有两棵子树
∵子树个数 = 关键字个数 + 1
(3)除根结点外的所有非叶结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1个关键字
(4)所有的叶结点代表查找失败的位置,都出现在同一层次上,并且不带信息
∵叶结点对应查找失败的情况,∴叶结点数 = 关键字数 + 1
实际上这些结点不存在,指向这些结点的指针为空
(5)叶结点结构👇
n n n | P 0 P_0 P0 | K 1 K_1 K1 | P 1 P_1 P1 | K 2 K_2 K2 | P 2 P_2 P2 | ⋅ ⋅ ⋅ ··· ⋅⋅⋅ | K n K_n Kn | P n P_n Pn |
---|
其中,
n n n( ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil m/2 \rceil-1≤n≤m-1 ⌈m/2⌉−1≤n≤m−1)为结点中关键字的个数
K i K_i Ki( i = 1 , 2 , ⋅ ⋅ ⋅ , n i=1,2,···,n i=1,2,⋅⋅⋅,n)为结点的关键字,且 K 1 < K 2 < ⋅ ⋅ ⋅ < K n K_1<K_2<···<K_n K1<K2<⋅⋅⋅<Kn
P i P_i Pi( i = 1 , 2 , ⋅ ⋅ ⋅ , n i=1,2,···,n 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
- B树示例
3.1.2 B树基操
(1)B树的高度(磁盘存取次数)
若 n ≥ 1 n≥1 n≥1,则对任意一棵包含 n n n个关键字、高度为 h h h、阶数为 m m m的B树👇
l
o
g
m
(
n
+
1
)
≤
h
≤
l
o
g
⌈
m
/
2
⌉
(
n
+
1
2
)
+
1
{log}_m(n+1)≤h≤{log}_{\lceil m/2 \rceil}(\frac{n+1}{2})+1
logm(n+1)≤h≤log⌈m/2⌉(2n+1)+1
∵每个结点至多有
m
m
m棵子树,
m
−
1
m-1
m−1个关键字,
∴
n
≤
(
m
−
1
)
(
1
+
m
+
m
2
+
⋅
⋅
⋅
+
m
h
−
1
)
=
m
h
−
1
∴n≤(m-1)(1+m+m^2+···+m^{h-1})=m^h-1
∴n≤(m−1)(1+m+m2+⋅⋅⋅+mh−1)=mh−1
∵第
h
+
1
h+1
h+1层(不包含任何信息的叶结点层)至少有
2
(
⌈
m
/
2
⌉
)
h
−
1
个
结
点
2{(\lceil m/2 \rceil)}^{h-1}个结点
2(⌈m/2⌉)h−1个结点,∴查找不成功的结点数
n
+
1
≥
2
(
⌈
m
/
2
⌉
)
h
−
1
n+1≥2{(\lceil m/2 \rceil)}^{h-1}
n+1≥2(⌈m/2⌉)h−1
(2)B树的查找
- 与二叉查找树类似,只是每个结点都是多个关键字的有序表,在每个结点上都是根据该结点的子树所做的多路分支决定
- 2个基本操作:
(1)在B树中找结点【在磁盘上进行】
(2)在结点内找关键字【在内存中进行】 - 找到目标结点后,先将结点信息读入内存,再在结点内顺序或折半查找关键字。
(3)B树的插入
- 将关键字key插入B树的过程👇:
1° 定位。利用B树查找算法,找出插入该关键字的最低层中的某个非叶结点
2° 插入。在B树中,每个非失败结点的关键字都在区间 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil-1,m-1] [⌈m/2⌉−1,m−1]内。
(1)插入后的结点关键字个数小于 m m m,可以直接插入;
(2)插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于 m − 1 m-1 m−1时,必须对结点进行分裂。 - 结点分裂方法:
1° 取一个新结点,在插入key后的原结点,从中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉将其中的关键字分为两部分:
(1)左部分包含的关键字放在原结点中,
(2)右部分包含的关键字放在新结点中,
(3)中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉的结点插入原结点的父结点。
2° 若此时导致其父结点的关键字个数也超过了上限,则继续这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度 + 1。
(4)B树的删除
删除后的结点中的关键字个数须 ≥ ⌈ m / 2 ⌉ − 1 ≥\lceil m/2 \rceil-1 ≥⌈m/2⌉−1
- 将关键字
k
k
k删除的过程👇:
1° 当被删关键字 k k k在非终端结点中时,转换成被删关键字在终端结点中的情形:可以用 k k k的前驱(或后继) k ′ k' k′代替 k k k,然后在相应的结点中删除 k ′ k' k′
k k k的前驱: k k k所在结点的左子树中最右侧的值,即比 k k k小的关键字中最大的那一个
k k k的后继: k k k所在结点的右子树中最左侧的值,即比 k k k大的关键字中最小的那一个
(1) 若 < k <k <k的子树中关键字个数 > ⌈ m / 2 ⌉ >\lceil m/2 \rceil >⌈m/2⌉,则找出前驱代替
(2) 若 > k >k >k的子树中关键字个数 > ⌈ m / 2 ⌉ >\lceil m/2 \rceil >⌈m/2⌉,则找出后继代替
2° 当被删关键字 k k k在终端结点中时,三种情况:
(1)直接删除关键字 —— 若被删关键字所在结点的关键字个数 > ⌈ m / 2 ⌉ − 1 >\lceil m/2 \rceil-1 >⌈m/2⌉−1,则直接删除该关键字。
此时仍满足B树定义
(2)兄弟够借 —— 若被删关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil -1 =⌈m/2⌉−1,且与此结点相邻的右(或左)兄弟结点的关键字个数 > ⌈ m / 2 ⌉ − 1 >\lceil m/2 \rceil-1 >⌈m/2⌉−1,则需要调整该结点、右(或左)兄弟结点及其双亲结点【父子换位法】,以达到新的平衡:
① 用双亲结点中的处于右(或左)兄弟和被删关键字值之间的关键字代替被删关键字;
② 用右(或左)兄弟结点中的最小(或最大,与右左对应)关键字代替双亲结点中的那个关键字。
(3)兄弟不够借 —— 若被删关键字所在结点删除前的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil -1 =⌈m/2⌉−1,且与此结点相邻的右(或左)兄弟结点的关键字个数均 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil-1 =⌈m/2⌉−1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
① 将被删关键字结点与其左(或右)兄弟进行合并
② 将①中两个结点关键字范围夹着的双亲结点的关键字与①合并后的结点进行关键字合并
③ 若双亲结点在合并后不满足关键字个数要求,则重复①②操作,直至符合B树要求为止 - 合并过程:双亲结点中的关键字个数
−
1
- 1
−1
(1)若其双亲结点是根结点,且关键字个数减少至 0 0 0,则直接将根结点删除,合并后的新结点成为根;
(2)若其双亲结点不是根结点,且关键字个数减少到 ⌈ m / 2 ⌉ − 2 \lceil m/2 \rceil-2 ⌈m/2⌉−2,则又要与它自己的兄弟结点进行调整或合并操作;
(3)重复(1)(2),直至符合B树的要求为止。
3.1.3 注意点
- 针对 m m m阶B树而言,每个结点的关键字个数 x x x满足 ⌈ m / 2 ⌉ − 1 ≤ x ≤ m − 1 \lceil m/2 \rceil-1≤x≤m-1 ⌈m/2⌉−1≤x≤m−1即可,即每个结点的关键字个数都为 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1也是满足要求的。除非至少有一个结点,它所包含的关键字个数大于 m − 1 m-1 m−1,那么这就不是 m m m阶的B树
3.2 B+树
3.2.1 m m m阶B+树条件
- 每个分支结点最多有 m m m棵子树(孩子结点)
- 非叶根结点至少有两棵子树,其他每个分支结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉棵子树
- 结点的子树个数 = 关键字个数
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来
- 所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针
3.2.2 m m m阶B树与 m m m阶B+树的区别
- 具有
n
n
n个关键字的结点含有几棵子树:
(1)B+树中 —— n n n
(2)B 树中 —— n + 1 n+1 n+1 - 每个结点(非根内部结点)的关键字个数
n
n
n的范围:
(1)B+树中 —— ⌈ m / 2 ⌉ ≤ n ≤ m \lceil m/2 \rceil≤n≤m ⌈m/2⌉≤n≤m
(2)B 树中 —— ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil m/2 \rceil-1≤n≤m-1 ⌈m/2⌉−1≤n≤m−1 - 根结点的关键字个数
n
n
n的范围:
(1)B+树中 —— 1 ≤ n ≤ m 1≤n≤m 1≤n≤m
(2)B 树中 —— 1 ≤ n ≤ m − 1 1≤n≤m-1 1≤n≤m−1 - 关于叶结点和信息:
(1)B+树中 —— 叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
(2)B 树中 —— 叶结点不包含信息 - 关于关键字:
(1)B+树中 —— 叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中
(2)B 树中 —— 叶结点包含的关键字和其他结点包含的关键字是不重复的 - 关于查找:
(1)B+树中 —— 支持顺序查找和随机查找
(2)B 树中 —— 仅支持随机查找
3.2.3 B+树基操
- 增、删、查,与B树基本类似
- 在B+树中查找时,无论查找成功与否,每次查找都是一条从根结点到叶结点的路径
在查找过程中,非叶结点上的关键字值 = 给定值时,并不终止,而是继续往下查找,直到叶结点上的该关键字为止
- 可实现两种查找运算:
(1)从最小关键字开始的顺序查找
(2)从根结点开始的多路查找
3.2.4 B+树的优点
- B+树是应文件系统所需而产生的B树变形
- B+树比B树更适合用于OS的文件索引和数据库索引
B+树的磁盘读写代价更低,查询效率更加稳定
4 散列查找
4.1 散列表的基本概念
- 散列函数 :一个把查找表中的关键字映射成该关键字对应的地址(可以是数组下标、索引或内存地址)的函数 H a s h ( k e y ) = A d d r Hash(key)=Addr Hash(key)=Addr
- 冲突 :散列函数可能会把两个或两个以上的不同关键字映射到同一地址
冲突总是不可避免的
- 处理冲突 :为产生冲突的关键字寻找下一个“空”的Hash地址
- 同义词 :冲突时发生碰撞的不同关键字
- 散列表 :根据关键字而直接进行访问的数据结构。散列表建立了关键字和存储地址之间的一种直接映射关系
- 堆积(聚集):进行一次查找,查找长度有点长,就叫堆积。由同义词冲突的探查序列和非同义词之间不同的探查序列交织在一起引起
4.2 散列函数的构造方法
- 目标:为了尽量降低产生冲突的可能性
4.2.0 注意点
- 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围
- 散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生
- 散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址
4.2.1 直接定址法
- 直接取关键字的某个线性函数值为散列地址
- 散列函数: 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
- 优点:计算最简单,且不会产生冲突
- 缺点:若关键字分布不连续,空位较多,则会造成存储空间的浪费
- 适用范围:关键字的分布基本连续的情况
4.2.2 除留余数法
- 散列函数:假定散列表表长为 m m m,取一个 ≤ m ≤m ≤m但最接近或等于 m m m的质数 p p p H ( k e y ) = k e y % p H(key)=key\%p H(key)=key%p
- 优点:最简单、最常用
- 关键:选好 p p p
4.2.3 数字分析法
- 设关键字是
r
r
r进制数,选取数码分布较为均匀的若干位作为散列地址
∵ r r r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现
- 适用范围:已知的关键字集合
若更换了关键字,则需要重新构造新的散列函数
4.2.4 平方取中法
- 取关键字的平方值的中间几位作为散列地址。具体取多少位要视实际情况而定
- 优点:使得散列地址分布比较均匀
- 适用范围:关键字的每位取值都不够均匀或均小于散列地址所需的位数
4.3 处理冲突的方法
H i H_i Hi:表示处理冲突中第 i i i次探测得到的散列地址
假设得到的另一个散列地址 H 1 H_1 H1仍然发生冲突,只得继续求下一个地址 H 2 H_2 H2,以此类推,直到 H k H_k Hk不发生冲突为止,则 K k K_k Kk为关键字在表中的地址
4.3.1 开放定址法
- 指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放 H i = ( H ( k e y ) + d i ) % m H_i=(H(key)+d_i)\%m Hi=(H(key)+di)%m
H ( k e y ) H(key) H(key):散列函数
i = 0 , 1 , 2 , ⋅ ⋅ ⋅ , k ( k ≤ m − 1 ) i=0,1,2,···,k(k≤m-1) i=0,1,2,⋅⋅⋅,k(k≤m−1)
m m m:散列表表长
d i d_i di:增量序列
- 问题:不能随便物理删除表中的已有元素
∵若删除元素,则会截断其他具有相同散列地址的元素的查找地址
解决:要删除一个元素时,可给它做一个删除标记,进行逻辑删除
副作用:需要定期维护散列表,要把删除标记的元素物理删除
执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用
(1)线性探测法
- 当 d i = 0 , 1 , 2 , ⋅ ⋅ ⋅ , m − 1 时 d_i=0,1,2,···,m-1时 di=0,1,2,⋅⋅⋅,m−1时
- 特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m − 1 m-1 m−1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表
- 缺点:造成大量元素在相邻的散列地址“聚集”(或堆积)起来,大大降低了查找效率
(2)平方探测法(二次探测法)
- 当 d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , ⋅ ⋅ ⋅ , k 2 , − k 2 时 , 其 中 k ≤ m / 2 d_i=0^2,1^2,{-1}^2,2^2,{-2}^2,···,k^2,{-k}^2时,其中k≤m/2 di=02,12,−12,22,−22,⋅⋅⋅,k2,−k2时,其中k≤m/2
- 散列表长度 m m m必须是一个可以表示成 4 k + 3 4k+3 4k+3的素数
- 优点:避免出现“堆积”问题
- 缺点:不能探测到散列表上的所有单元,但至少能探测到一半单元
(3)再散列法(双散列法)
- 当 d i = H a s h 2 ( k e y ) 时 当d_i={Hash}_2(key)时 当di=Hash2(key)时
- 当通过第一个散列函数 H ( k e y ) H(key) H(key)得到的地址发生冲突时,则利用第二个散列函数 H a s h 2 ( k e y ) {Hash}_2(key) Hash2(key)计算该关键字的地址增量 H i = ( H ( k e y ) + i × H a s h 2 ( k e y ) ) % m , ( i 是 冲 突 的 次 数 , 初 始 未 0 ) 初 始 探 测 位 置 H 0 = H ( k e y ) % m H_i=(H(key)+i×{Hash}_2(key))\%m,(i是冲突的次数,初始未0)\\初始探测位置H_0=H(key)\%m Hi=(H(key)+i×Hash2(key))%m,(i是冲突的次数,初始未0)初始探测位置H0=H(key)%m
- 最多经过 m − 1 m-1 m−1次探测就会遍历表中所有位置,回到 H 0 H_0 H0位置
(4)伪随机序列法
- 当 d i = 伪 随 机 数 序 列 时 当d_i=伪随机数序列时 当di=伪随机数序列时
4.3.2 拉链法(链接法,chaining)
- 把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识
4.4 散列查找步骤
- 1° 初始化: A d d r = H a s h ( k e y ) Addr=Hash(key) Addr=Hash(key)
- 2° 检测查找表中地址为
A
d
d
r
Addr
Addr的位置上是否有记录:
(1)若无记录,返回查找失败;
(2)若有记录,比较它与 k e y key key的值:
①若相等,则返回查找成功标志;
②若不等,执行3° - 3° 用给定的处理冲突方法计算“下一个散列地址”,并把 A d d r Addr Addr置为此地址,转入2°
4.5 散列查找性能分析
- 仍需要以平均查找长度作为衡量散列表的查找效率的度量
∵存在“冲突”
- 散列表的查找效率取决于三个因素:
(1)散列函数
(2)处理冲突的方法
(3)装填因子 α \alpha α:一个表的装满程度 α = 表 中 记 录 数 n 散 列 表 长 度 m \alpha=\frac{表中记录数n}{散列表长度m} α=散列表长度m表中记录数n - 平均查找长度 A S L ASL ASL与装填因子 α \alpha α直接相关
参考
王道