查找大法
1、顺序查找与二分法查找
2、顺序二叉树与平衡二叉树
顺序二叉树
-
题目:下列哪个序列不是顺序二叉树的搜索序列
如: a 1 , a 2 , a 3 , a 4 , a 5 , a 6 a_1, a_2, a_3, a_4,a_5,a_6 a1,a2,a3,a4,a5,a6
如何判断:对于 a i a_i ai来说,如果 a i + 1 > a i a_{i+1}>a_i ai+1>ai,那么,所有 n > i 都有 a n > a i n>i都有a_n>a_i n>i都有an>ai。
-
计算顺序二叉树的成功查找长度
A S L 成功 = ∑ 第 i 层节点数 ∗ i 节点总数 ASL_{成功} = \frac{\sum{第i层节点数*i}}{节点总数} ASL成功=节点总数∑第i层节点数∗i
-
计算顺序二叉树的失败查找长度
A S L 失败 = ∑ 空链的父节点高度 空链的个数 ASL_{失败} = \frac{\sum{空链的父节点高度}}{空链的个数} ASL失败=空链的个数∑空链的父节点高度
平衡二叉树
-
平衡因子:左子树的高度减去右子树的高度, h 左 − h 右 h_左-h_右 h左−h右
-
分支节点:度不为0的节点
-
平衡二叉树的旋转
-
对节点
A
左旋:1、
A
右孩子的左孩子变成A
的右孩子:A->right = A->right->left
2、
A
的右孩子取代A
的位置3、
A
变成右孩子的左孩子:A->right->left = A
-
对节点
A
右旋:1、
A
左孩子的右孩子变成A
的左孩子:A->left = A->left->right
2、
A
的左孩子取代A的位置3、
A
变成左孩子的右孩子:A->left->right = A
-
-
平衡二叉树的插入(王道书 P 274 P_{274} P274)
-
基本假设:找到插入路径上离插入点最近的不平衡节点
A
,A
一般是插入树C
的爷爷,设B
是C
的父亲第一是找不平衡节点
第二是
C
可能是新节点也可能不是,是在C
的子树或者C
处插入,但是C
没有失衡第三是
B
必须是插入路径上的节点,插入路径和排序二叉树一样的 -
核心思想:将中间大小的节点旋转到
A
的位置,这里很混乱,没关系,看下面的 -
1、
LL
平衡旋转:A
的**左(L)孩子是B
,B
的左(L)**孩子是C
1)右单旋转即可
2)对
A
点右旋一次即可3)核心思想:显然有
A>B>C
,最后右旋一下,B(中间大小)
取代了A
-
2、
RR
平衡旋转:A
的**右(R)孩子是B
,B
的右(R)**孩子是C
1)左单旋转即可
2)对
A
点左旋一次即可3)核心思想:显然有
A<B<C
,最后右旋一下,B(中间大小)
取代了A
-
3、
LR
平衡旋转:A
的**左(L)孩子是B
,B
的右(R)**孩子是C
1)先左后右旋转
2)先对
B
左旋,后对A
右旋3)核心思想:显然是
B<C<A
,最后是把C
旋转到A
的位置4)解释:
- 对
B
左旋:将C
提升到B
的位置,提升第一次,C
变成儿子,B
变成孙子 - 对
A
右旋:将C
提升到A
的位置,提升第二次,C
变成爷爷,A
变成儿子
- 对
-
4、
RL
平衡旋转:A
的**右(R)孩子是B
,B
的左(L)**孩子是C
1)先右后左旋转
2)先对
B
右旋,后对A
左旋3)核心思想:显然是
B>C>A
,最后是把C
旋转到A
的位置4)解释:
- 对
B
右旋:将C
提升到B
的位置,提升第一次,C
变成儿子,B
变成孙子 - 对
A
左旋:将C
提升到A
的位置,提升第二次,C
变成爷爷,A
变成儿子
- 对
-
-
平衡二叉树的删除
-
基本假设:
w
是需要删除的节点,从w
向上面找,离w
最近且失衡的节点为z
z
有左右子树,左右子树高度差大于1,设高度较高的子树根节点为y
y
有左右子树,左右子树高度差小于等于1,设高度较高的子树根节点为x
如果
y
的左右子树高度相等,那么x
任意左还是右还是有,
z
是爷爷,y
是父亲,x
是孙子 -
核心思想:还是把中间大小的节点旋转到
z
处 -
1、
LL
平衡旋转:z
的**左(L)孩子是y
,y
的左(L)**孩子是x
1)右单旋转即可
2)对
z
点右旋一次即可3)核心思想:显然有
z>y>x
,最后右旋一下,y(中间大小)
取代了z
-
2、
RR
平衡旋转:z
的**右(R)孩子是y
,y
的右(R)**孩子是x
1)左单旋转即可
2)对
z
点左旋一次即可3)核心思想:显然有
z<y<x
,最后右旋一下,y(中间大小)
取代了z
-
3、
LR
平衡旋转:z
的**左(L)孩子是y
,y
的右(R)**孩子是x
1)先左后右旋转
2)先对
y
左旋,后对z
右旋3)核心思想:显然是
y<x<z
,最后是把x
旋转到z
的位置 -
4、
RL
平衡旋转:z
的**右(R)孩子是y
,y
的左(L)**孩子是x
1)先右后左旋转
2)先对
y
右旋,后对z
左旋3)核心思想:显然是
y>x>z
,最后是把x
旋转到z
的位置
-
-
树高为
h
时,节点数n
的最小值怎么计算1、先给出答案: n ( h ) m i n = n ( h − 1 ) m i n + n ( h − 2 ) m i n + 1 n(h)_{min} = n(h-1)_{min} + n(h-2)_{min} + 1 n(h)min=n(h−1)min+n(h−2)min+1
且 n ( 0 ) m i n = 0 ; n ( 1 ) m i n = 1 ; n ( 2 ) m i n = 2 ; n(0)_{min} = 0;n(1)_{min} = 1 ; n(2)_{min} = 2; n(0)min=0;n(1)min=1;n(2)min=2; 这个应该很好得知,就像下面图示一样,
2、证明吧:首先,设定 n ( x ) m i n n(x)_{min} n(x)min为树高为 x x x时,最少需要的节点数目
3、假设现在有两棵 A V L AVL AVL,且 A V L 1 AVL_1 AVL1的树高为
h-1
, A V L 2 AVL_2 AVL2的树高为h-2
4、树高为
h
的AVL
,左右子树也是一颗AVL
,且左右子树的树高差不超过1
5、也就是说,左右子树的树高要么都为
h-1
(方法1); 要么一个为
h-1
,一个为h-2
(方法2)。6、既然想要追求节点少,那么肯定是树高最小最好,肯定选择方法2。
不妨设左子树为
h-1
,右子树为h-2
7、那就可以用上述的 A V L 1 和 A V L 2 AVL_1 和 AVL_2 AVL1和AVL2来作为
AVL
的两棵子树,再加上一个新的根节点8、那么节点数目递推公式就是: n ( h ) m i n = n ( h − 1 ) m i n + n ( h − 2 ) m i n + 1 n(h)_{min} = n(h-1)_{min} + n(h-2)_{min} + 1 n(h)min=n(h−1)min+n(h−2)min+1
{ n ( h ) m i n 高度为 h 的最小节点数目 n ( h − 1 ) m i n 高度为 h − 1 左子树的最小节点数目 n ( h − 2 ) m i n 高度为 h − 2 右子树的最小节点数目 1 额外的根节点数目 \begin{cases} n(h)_{min} & 高度为 h 的最小节点数目\\ n(h-1)_{min} & 高度为 h-1 左子树的最小节点数目\\ n(h-2)_{min} & 高度为 h-2 右子树的最小节点数目\\ 1 & 额外的根节点数目 \end{cases} ⎩ ⎨ ⎧n(h)minn(h−1)minn(h−2)min1高度为h的最小节点数目高度为h−1左子树的最小节点数目高度为h−2右子树的最小节点数目额外的根节点数目
3、红黑树(916目前不考,先留坑)
4、B树
首先,B-Tree称为B树或者B-树,也就是说B树和B-树其实是一个东西,本篇用B树这个名字。B树是用来外查找的,也就说在磁盘上面查找,因为可能内容太多,不适合全部加载到内存中来。
-
高度与关键字数关系先给出: l o g m ( n + 1 ) ≤ h ≤ l o g ⌈ m / 2 ⌉ ( n + 1 2 ) + 1 log_m(n+1) \leq h \leq log_{\lceil m/2 \rceil}(\frac{n+1}{2})+1 logm(n+1)≤h≤log⌈m/2⌉(2n+1)+1
1、
n
是关键字数,m
是阶数,h
是树高2、每个节点关键字最多,那么树高度最低: n ≤ ( m − 1 ) ( 1 + m + m 2 + m 3 + . . . + m h − 1 ) n \leq (m-1)(1+m+m^2+m^3+...+m^{h-1}) n≤(m−1)(1+m+m2+m3+...+mh−1)
3、每个节点关键字最少,那么树高度最高
-
B树的一些概念
1、终端节点:下图的倒数第二层节点就是终端节点(也就是最后一层非空节点)
2、叶子节点:最后一层的
null
节点,也就是空节点叫做叶子节点。
-
B树的特性(这几个特性决定B树的插入删除操作)
1、树中每个节点之多有
m
棵子树,至多有m-1
个关键字2、若根节点不是终端节点,则至少有两棵子树。(对于根节点不要求子树很多)
3、除了根节点之外的所有非叶子节点,至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉(向上取整)棵子树,至少含有 ⌈ ( m − 1 ) / 2 ⌉ \lceil (m-1)/2\rceil ⌈(m−1)/2⌉个关键字。
4、所有的非叶子节点的结构如下:n是总数,P是子树,K是关键字
|n
|P0
|K1
|P1
|K2
|...
|Kn
|Pn
|Ki
关键字小于Pi
子树的所有关键字,Ki
关键字大于Pi-1
子树的所有关键字,且关键字是有序的。5、所有的叶节点都出现在同一层次,并且不携带信息,也就是为
null
。说明每棵子树高相同的。 -
B树的查找
1、先将一个节点的信息读入内存,然后用顺序查找或者折半查找的方法,找到关键字或者找到子树。
2、如果找到关键字就结束,如果找到子树就将当前节点写入磁盘,重复上述过程
-
B树的插入
核心:还是需要满足B树的五条性质。
步骤:
-
找到最低层的某个终端节点
-
进行插入,分情况情况讨论
如果插入该关键字时候,该终端节点的关键字个数仍然是 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil-1,m-1] [⌈m/2⌉−1,m−1]这个区间中,则直接插入
如果插入关键字时候,关键字数目溢出了,那么,就先将关键字插入对应位置得到一串新的关键字序列,取得这串新关键字序列中间值
m
,并以m
将序列分割成两段。将m
插入到本节点的父节点,并将两段序列变成父节点的两个子树孩子。同时判断父节点是否超过了,超过了继续向上迭代。直到根节点仍然超过了,那么就新建根节点,B树高加1。
-
-
B树的删除
核心:还是满足B树的五条性质
步骤:
-
找到需要删除的节点,直接删除
-
看是否仍然是B树
删除节点是非终端节点的关键字:
- 用其前驱或者后继节点替代就行,这样就相当于删除了其前驱或者后继节点
- 然后依次迭代,知道变成删除终端节点
删除节点是终端节点的关键字:
-
如果删除了关键字之后,关键字个数仍然是 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil-1,m-1] [⌈m/2⌉−1,m−1],直接删除,而且终端节点没有子树,所以不用考虑子树如何处理。
-
如果删除了关键字之后,关键字个数不够 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉
其左(或右)兄弟够借一个节点给他也就是左(或右)兄弟节点关键字借出一个仍然满足 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil-1,m-1] [⌈m/2⌉−1,m−1],那么就用其前驱(或后继)关键字替换此位置,其**前驱的前驱(或后继的后继)替换前驱(或后继)**关键字的位置
如果其左右兄弟都不够借:说明本节点以及兄弟节点的关键字个数肯定等于 ⌈ m / 2 ⌉ − 1 \lceil m/2 \rceil-1 ⌈m/2⌉−1,这样,就可以将两兄弟以及两兄弟在父节点中包围的关键字合并成一个新节点。至于这样为什么不会超过关键字个数的要求呢?看下面的解释!!!
-
-
为什么B树的关键字要求为** [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2 \rceil-1,m-1] [⌈m/2⌉−1,m−1]**呢?
首先,关键字最大值确实为 m − 1 m-1 m−1,这个没问题,因为是有m棵子树,关键字可以少一个,仍然能够走正确的路径寻找。可以参看性质4
然后,前面说到删除关键字时候,兄弟节点需要合并的情况。那么本节点以及兄弟节点的关键字个数肯定等于关键字个数最小值才行的。不妨设最小值为
n
要求合并之后关键字相加仍然满足关键字个数要求。有:
( n − 1 ) + 1 + n ≤ m − 1 (n-1)+1+n \leq m-1 (n−1)+1+n≤m−1:
(n-1)
是需要删除一个关键字,1
是父节点中的一个关键字,n
是兄弟节点的关键字个数n ≤ m − 1 2 n \leq \frac{m-1}{2} n≤2m−1,但是需要关键字多,这样查找效率高,所以 n = m − 1 2 n = \frac{m-1}{2} n=2m−1,考虑到奇偶, n = ⌈ m / 2 ⌉ − 1 n=\lceil m/2 \rceil-1 n=⌈m/2⌉−1为通式(带入 m = 5 和 m = 4 m=5和m=4 m=5和m=4很容易验证正确性)
5、B+树
B+树是数据库的出现而出现的B树的一种变形,它的插入以及删除和B树的操作基本类似,现在只讲讲它的性质以及和B树的不同点。
B+树的一些概念
- 叶子节点:这个和B树不同,这里就是指最低层的非空节点,和普通的树一样
- 分支节点:非叶子节点
B+树的性质
- 1、每个分支节点最多有m棵子树(与B树没什么区别)
- 2、非叶根节点至少有两棵子树,其他每个分支节点至少有 ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉棵子树(有点区别)
- 3、节点的子树个数与关键字个数相等(有区别)
- 4、所有叶节点关键字才有包含数据的指针,且关键字有序(有区别)
- 5、所有分支节点中关键字仅包含子节点的指针,而且此关键字是子节点关键字的最大值(大区别)
B树与B+树区别
- 1、关键字个数不一样:B+树含有
n
个关键字和n
棵子树,B树含有n-1
个关键字和n
棵子树。- 2、关键字个数的范围不同: ⌈ m / 2 ⌉ ≤ N B + 关键字个数 ≤ m \lceil m/2 \rceil \leq N_{B+关键字个数} \leq m ⌈m/2⌉≤NB+关键字个数≤m( 2 ≤ N B + 根节点 ≤ m 2 \leq N_{B+根节点} \leq m 2≤NB+根节点≤m), ⌈ m / 2 ⌉ − 1 ≤ N B 树关键字个数 ≤ m − 1 \lceil m/2 \rceil -1 \leq N_{B树关键字个数} \leq m - 1 ⌈m/2⌉−1≤NB树关键字个数≤m−1( 1 ≤ N B 树根节点 ≤ m − 1 1 \leq N_{B树根节点} \leq m-1 1≤NB树根节点≤m−1)
- 3、B+树中,只有叶子节点包含数据索引,其他节点只包含子节点索引。B树中,所有非空节点都包含数据索引和子节点索引。
- 4、B+树中,关键字等于其子树的最大值;B树中,关键字大于其左边子树小于其右边子树。
- 5、查找操作有点不一样:B+树需要查找到叶子节点中关键字相等的地方,B树仅需要查找到关键字相等的地方。
- 6、B+树每个叶子节点直接以链表形式存储,所以,B+树还可以用链式查找方式查找
6、散列查找
通过散列函数,直接将关键字映射到地址。散列函数记为 H a s h ( K e y ) = A d d r ( 下标或者索引 ) Hash(Key) = Addr(下标或者索引) Hash(Key)=Addr(下标或者索引),理想情况下,如果没有发生碰撞,那么散列函数的查找效率时间复杂度为 O ( 1 ) O(1) O(1)。是一个典型的空间换时间的数据结构。
这一小节,最重要的数学科目是《数论》。
-
常见的散列函数
1、直接定址法: H a s h ( K e y ) = a × K e y + b Hash(Key) = a\times Key + b Hash(Key)=a×Key+b,这个不会发生碰撞,但是空位较多
2、除留余数法: H a s h ( K e y ) = K e y % p Hash(Key) = Key \%p Hash(Key)=Key%p,控制好p是关键,减少碰撞
3、数字分析法:根据关键字的数据特征,制定对应的哈希函数。
4、平方取中法:将关键字平方之后,取中间几位作为散列地址。
-
处理冲突的方法
1、开放定址法
- 线性探测法:发生冲突之后,就依次将地址加
k
(k=0,1,2,3……)知道找到空闲位置;寻找数据时也是先找到初步地址处,然后依次向下一个元素查找。 - 平方探测法:将上述的
k
换成 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 . . . 0^2,1^2,-1^2,2^2,-2^2... 02,12,−12,22,−22...,这样是为了不让元素太聚集,提高查找和插入效率。 - 双散列法:发生冲突后,用第二个散列函数计算地址增量。
- 伪随机序列法:增量为伪随机序列。我的认为是:伪随机序列是构建此数据结构提前生成的一个没有规律的数列。
2、拉链法:发生冲突后,用链表依次存储冲突元素在同一地址,因此,整个哈希地址块存储的不是元素,而是链表头指针。
- 线性探测法:发生冲突之后,就依次将地址加
-
性能分析
散列表查找效率主要取决于三个因素:散列函数,处理冲突的方法和装填因子
装填因子:记为 α , α = 表中记录数 n 散列表长度 m \alpha, \quad \alpha = \frac{表中记录数n}{散列表长度m} α,α=散列表长度m表中记录数n,可以看出装填因子依赖于两者的比值。