一、二分查找
当序列分布较为均匀时效率高
int bin_search(vector<int> &nums, int key){
int low = 0;
int high = nums.size() - 1;
while(low <= high){
int mid = (low + high) / 2;
if(nums[mid] < key){
low = mid + 1;
}else if(nums[mid] > key){
high = mid - 1;
}else{
return mid;
}
}
return -1;
}
二、插值查找
当查找的序列非常极端时效率高,比如在序列:1,2,4,6,7,9,11,12,13,999,1000中查找999,若是用二分查找效率就很低了,需要查找3次。若用插值查找就只需要查找1次就行了。当数据非常大且极端时,插值查找的效率就体现得非常明显
int insert_search(vector<int> &nums, int key){
int low = 0;
int high = nums.size() - 1;
while(low <= high){
//因为除法会向下取整,所以mid的计算结果不会越界
int mid = low + (high-low)*(key-nums[low])/(nums[high]-nums[low]);
if(nums[mid] < key){
low = mid + 1;
}else if(nums[mid] > key){
high = mid - 1;
}else{
return mid;
}
}
return -1;
}
三、分块查找
在块内无序,块间有序的顺序表中查找元素。
首先需要建立索引表,索引表的每个元素记录每个分块的最大关键字和分块的存储区间。索引表元素数据结构如下:
typedef struct{
ElemType maxValue;//块内最大值
int low;
int high;
}Index;
分块查找算法过程:
- 在索引表中确定待查值所属的分块(可顺序,可折半)
- 在块内顺序查找
注:对索引表进行折半查找时,若索引表中不包含目标关键字,则折半查找最终停在 l o w < h i g h low<high low<high,要在 l o w low low所指分块中查找
当对顺序表中的元素进行插入或删除的时候,除了顺序表中元素都要移动以外,还需要修改索引表元素的数据。效率太低,一般的我们可以改进为链式存储,如下:
插入元素时,只需要挂在最后即可。删除元素时,修改指针即可。
四、B树
基本概念:
- 终端结点: 最下层含有实际数据的结点
- 叶子结点: 最下层表示空值的结点
- B树的阶: B树中所有结点的孩子个数的最大值称为B树的阶
一棵m阶B树或为B树,或为满足以下特性的m叉树:
- 树中每个结点最多有 m m m棵子树,即至多含有 m − 1 m-1 m−1关键字
- 若根结点不是终端结点,至少有两棵子树(包含在绝对平衡里)
- 除了根结点外的所有非叶结点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉棵子树,即至少含有 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1个关键字。(每个结点尽可能存满,树不会太深)
- 所有的叶结点都在同一层次上,这些结点都代表空
- 对于任何一个非叶结点,左右子树高度相同,即绝对平衡
- 对于任何一个非叶结点中的任何一个元素,左指针所指块包含元素<该元素<右指针所指块包含元素,类似于排序树
5叉排序树结点的定义
struct Node{
ElemType keys[4]; //最多4个关键字
struct Node* child[5]; //最多5个孩子
int high; //结点中关键字数量
};
含有n个关键字的m叉B树,最小高度和最大高度
最小高度——让每个结点尽可能满,有
m
−
1
m-1
m−1个关键字,
m
m
m个分叉,则有:
n
≤
(
m
−
1
)
(
1
+
m
2
+
m
3
+
.
.
.
+
m
h
−
1
)
=
m
h
−
1
n\leq(m-1)(1+m^2+m^3+...+m^{h-1})=m^h-1
n≤(m−1)(1+m2+m3+...+mh−1)=mh−1,因此
h
≥
l
o
g
m
(
n
+
1
)
\qquad\qquad\qquad\qquad\qquad h\ge log_m(n+1)
h≥logm(n+1)
最大高度——让每层分叉尽可能少,即根结点只有2个分叉,其他结点只有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉个分叉,每层结点至少有:第一层1、第二层2、第三层 2 ⌈ m / 2 ⌉ 2\lceil m/2 \rceil 2⌈m/2⌉…第 h h h层 2 ( ⌈ m / 2 ⌉ ) h − 2 2(\lceil m/2 \rceil)^{h-2} 2(⌈m/2⌉)h−2。则第 h + 1 h+1 h+1层共有叶子结点(失败结点): 2 ( ⌈ m / 2 ⌉ ) h − 1 2(\lceil m/2 \rceil)^{h-1} 2(⌈m/2⌉)h−1
由于
n
n
n个关键字的B树必有
n
+
1
n+1
n+1个结点,则
n
+
1
≥
2
(
⌈
m
/
2
⌉
)
h
−
1
n+1\ge2(\lceil m/2 \rceil)^{h-1}
n+1≥2(⌈m/2⌉)h−1,即
h
≤
l
o
g
⌈
m
/
2
⌉
n
+
1
2
+
1
\qquad\qquad\qquad\qquad\quad h\le log_{\lceil m/2 \rceil}\frac{n+1}{2}+1
h≤log⌈m/2⌉2n+1+1
B树的插入操作
1.结点分裂
在插入
k
e
y
key
key后,若导致原结点关键字数超过上限,则从中间位置(
⌈
m
/
2
⌉
\lceil m/2 \rceil
⌈m/2⌉)将其中的关键字分为两部分,左部分包含关键字放在原结点中,右部分关键字放在新结点中,中间结点成为两者的父结点,分裂结果如下:
插入88、90、99
接着分裂
插入70、83、87
接着分裂
插入92、93、94,分裂完成后
插入73、74、75
分裂终端结点
此时可以看到根结点关键字太多了,需要分裂,按照同样的道理,分裂结果如下:
对于分裂操作的总结:
- 对m阶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树:
删除60后:
注:若待删除的关键字处于终端结点,则直接删除该关键字。同时还需检查结点关键字个数是否满足
⌈
m
/
2
⌉
−
1
≤
n
≤
m
−
1
\lceil m/2 \rceil-1\le n\le m-1
⌈m/2⌉−1≤n≤m−1
删除80,此时根结点为空,可以利用当前关键字的直接前驱和直接后继填补根结点
直接前驱: 当前关键字左子树“最右下”的元素,即左子树最大的关键字
直接后继: 当前关键字右子树“最左下”的元素,即右子树最小的关键字
下面利用直接后继填补根结点
注:删除非终端结点80的操作,转换成了删除终端结点80的操作。故对非终端结点关键字的删除,必然可以抓换为对终端结点关键字的删除
接着,我们删除关键字38,删除结果如下:
可以看到,就是直接把38干掉,70提上去,49填下来,分别补到对应的位置
注:当右兄弟有充足的关键字时,用当前结点的后继(49),和后继的后继(70) 来填补
接下来,删除90,删除结果如下:
同理,就是直接把90干掉,88填下来,87提上去,分别补到对应的位置
注:当左兄弟有充足的关键字时,用当前结点的前驱(88),和前驱的前驱(87) 来填补
接着删除49,先干掉49,结果如下:
此时,对于最坐下的结点,只有25这一个关键字,而右兄弟又没有多余的关键字借给他。现在,我们将关键字25、70、71、72合并,结果如下:
然而,此时关键字73所在的结点不符合B树的特性,我们将73、82、87、93合并,结果如下:
注:兄弟不够借时,则将关键字删除后的结点与左(或右)兄弟结点,以及父结点中的关键字合并
总结:
五、B+树
B树与B+树对比
而B树中,所有结点的关键字 互不重复
总结: