各种树型结构的大致实际应用场景:
AVL树:平衡二叉树之一,应用相对其他数据结构比较少,windows对进程地址空间的管理用到了AVL
红黑树:平衡二叉树,广泛应用在C++STL中,比如map和set,Java的TreeMap
B和B+树:主要用在文件系统以及数据库中做索引等
Trie 树: 字符匹配, IP 选路
动态查找树
动态查找树主要有二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree), 红黑树 (Red-Black Tree ),
都是典型的二叉查找树结构,查找的时间复杂度 O(log2-N) 与树的深度相关,降低树的深度会提高查找效率,于是有了多路的B-tree/B+-tree/ B*-tree (B~Tree)。
4.1 二叉查找树
参考: https://www.jianshu.com/p/3257d0a4fdf2
4.2 AVL 树
高度平衡的二叉树
通常,维护这种高度平衡所付出的代价比从中获得的效率收益还大;实际用不多;
更多的地方是用追求局部而不是非常严格整体平衡的红黑树。
当然,如果场景中对插入删除不频繁,只是对查找特别有要求,AVL还是优于红黑的。
使用场景:Windows对进程地址空间的管理用到了AVL树。
定义
平衡因子(Balance Tree, 简称BF): ()=ℎ−ℎ,其中ℎ和ℎ分别为T的左右子树的高度。
平衡二叉树(Balanced Binary Tree, 或称AVL树):
空树,或者任意结点的左右子树高度差的绝对值不超过1,即|()|≤1。
1.给定节点数为 n 的 AVL 树的最大高度为
设是高度为 的平衡二叉树的最小节点;
h | ||
---|---|---|
0 | 1 | 1 |
1 | 2 | 1 |
2 | 4 | 2 |
3 | 7 | 3 |
4 | 12 | 5 |
5 | 20 | 8 |
6 | 33 | 13 |
7 | 54 | 21 |
8 | 88 | 34 |
而 斐波那契序列:
当很大时,
故而,
结论:给定结点数为N的AVL树的最大高度为
AVL的调整
右单旋(RR旋转)
不平衡的“发现者”是Mar,“麻烦结点”是Nov,在发现者右子树的右边,因而叫RR插入,此时Mar 的平衡因子BF=-2,为保持平衡,需要RR旋转。
如图所示,基本思想是: 把B的左子树腾出来挂到A的右子树上,返回B作为当前子树的根
左单旋(LL旋转)
不平衡的“发现者”是Mar,“麻烦结点”是Apr,在发现者左子树的左边,因而叫LL插入,此时Mar和May 的平衡因子BF=2,为保持平衡,需要LL旋转.
如图所示,基本思想是: 把B的右子树腾出来挂到A的左子树上,返回B作为当前子树的根
左-右单旋(LR旋转)
不平衡的“发现者”是May,“麻烦结点”是Jan,在发现者左子树的右边,因而叫LR插入,需要LR旋转,即把Aug的右子树Mar,调整为根结点。
如图所示,基本思想是:先将B作为根结点进行RR单旋转化为LL插入,再将A作为根结点进行LL单旋(先 RR 再 LL)
右-左单旋(RL旋转)
不平衡的“发现者”是Aug,“麻烦结点”是Feb,在发现者右子树的左边,因而叫RL插入,需要RL旋转,即调整Jan的左子树Dec为Aug和Jan根结点.
如图所示,基本思想是:先将B作为根结点进行LL单旋转化为RR插入,再将A作为根结点进行RR单旋(先 LL 再 RR)
程序实现
应用:是否为同一颗二叉搜索树
题意理解
给定一个插入序列就可以唯一确定一棵二叉搜索树,然而一棵给定的二叉搜索树却可以由多种不同的插入序列得到。eg.序列{2,1,3}和{2,3,1}可以得到一样的结果。
那么:对于输入的各种插入序列,你是否可以判断他们是否能生成一样的二叉搜索树。
输入样例:
4 2
3 1 4 2
3 4 1 2
3 2 4 1
第1行代表序列长度为4,且有2个待比较序列;第2行为序列原型,3,4行为具体的待比较序列。
输出样例:
Yes
No
求解思路
1、分别建两棵搜索树
2、不建树
3、建一棵树,再判别其他序列是否与该树一致
- 搜索树表示;
- 建搜索树T;
- 判别一序列是否与搜索树T一致
程序框架
int main()
{
对每组数据:
1、读入N和L;
2、根据第一行序列建树T
3、根据树T分别判别后面
的L个序列是否能与T形成
同一搜索树并输出结果
return 0;
}
程序实现
4.2 红黑树
近似平衡的平衡二叉树;
通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍近似平衡的。
所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。
使用场景:
- 广泛用在C++的STL中。map和set都是用红黑树实现的
- 著名的linux进程调度Completely Fair Scheduler,用红黑树管理
- 进程控制块
- epoll在内核中的实现,用红黑树管理事件块
- nginx中,用红黑树管理timer等
- Java的TreeMap实现
4.3 B树 B+树
B树,B+树特点是一样的,是多路查找树,一般用于数据库系统中;
B树,B+树分支多层数少;使用时螚有效的减少磁盘IO次数避免磁盘频繁的查找;
使用场景:
- 主要用在文件系统以及数据库中做索引等,比如Mysql:B-Tree Index in MySql;
- 像mysql的数据库定义是可以指定B+ 索引还是hash索引;
树(也叫 B-树)
B-树是为了磁盘或其它存储设备而设计的一种多叉平衡查找树。
(1) B树结构
B-tree中,每个结点包含:
本结点所含关键字的个数;
指向父结点的指针;
关键字;
指向子结点的指针数组;
#define MAX 1000 //结点中关键字的最大数目:Max=m-1,m是B-树的阶
#define MIN 500 //非根结点中关键字的最小数目:Min=m/2-1
typedef int KeyType; //KeyType关键字类型由用户定义
typedef struct BTreeNode *BTree;
typedef struct BTreeNode{ //结点定义中省略了指向关键字代表的记录的指针
int keyNum; //结点中当前拥有的关键字的个数,keynum<
(2)B-tree的特点
B-tree是一种多路搜索树(并不是二叉的),对于一棵M阶树:
定义任意非叶子结点最多只有M个孩子;且M>2;
根结点的孩子数为[2, M],除非根结点为叶子节点;
除根结点以外的非叶子结点的儿子数为[M/2, M];
非叶子结点的关键字个数=指向儿子的指针个数-1;
每个非叶子结点存放至少M/2-1(取上整)和至多M-1个关键字;
非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
所有叶子结点位于同一层;
以M=3的一棵3阶B树为例:
(3)B-tree高度与复杂度
B树的高度是
而不是其它几种树的 ,其中:
为度数(每个节点包含的元素个数),即所谓的阶数;
为总元素个数或总关键字数。
B树查找的时间复杂度为,下面是参考推导过程:
其中M为设定的非叶子结点最多子树个数,N为关键字总数;所以B-树的性能总是等价于二分查找(与M值无关),也就没有AVL树平衡的问题。
(3)B树的基本操作
(1)查找操作
在B-树中查找给定关键字的方法类似于二叉排序树上的查找。不同的是在每个结点上确定向下查找的路径不一定是二路而是keynum+1路的。
对结点内的存放有序关键字序列的向量key[l..keynum] 用顺序查找或折半查找方法查找。若在某结点内找到待查的关键字K,则返回该结点的地址及K在key[1..keynum]中的位置;否则,确定K在某个key[i]和key[i+1]之间结点后,从磁盘中读son[i]所指的结点继续查找。直到在某结点中查找成功;或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。
(2)查找操作的时间开销
B-树上的查找有两个基本步骤:
1.在B-树中查找结点,该查找涉及读盘DiskRead操作,属外查找;
2.在结点内查找,该查找属内查找。
查找操作的时间为:
1.外查找的读盘次数不超过树高h,故其时间是O(h);
2.内查找中,每个结点内的关键字数目keynum
1.实际上外查找时间可能远远大于内查找时间。
2.B-树作为数据库文件时,打开文件之后就必须将根结点读人内存,而直至文件关闭之前,此根一直驻留在内存中,故查找时可以不计读入根结点的时间。
(3)插入操作
插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。
(4)删除操作
首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后是移动之后的情况;如果没有,直接删除后,移动之后的情况。
树
(1)B树和B+树的对比
一棵m阶的B+树和m阶的B树的异同点在于:
1.有n棵子树的结点中含有n-1 个关键字;
2.所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息)
3.所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
下面是一棵典型的 B+ 树:
(2)为什么说B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引?
(1) B+树的磁盘读写代价更低;
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小;
如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。
一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16 bytes,而一个关键字2 bytes,一个关键字具体信息指针2 bytes。
一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
(2) B+树查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
树
B*-tree是B+-tree的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),
B*树中非根和非叶子结点再增加指向兄弟的指针;
B树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。
下图是一棵典型的B*树:
4.4 Trie树
又名单词查找树,一种树形结构;常用来操作字符串;
它是不同字符串的相同前缀只保存一份。相对直接保存字符串肯定是节省空间的,但是它保存大量字符串时会很耗费内存。
类似的有
前缀树(prefix tree),后缀树(suffix tree),radix tree(patricia tree, compact prefix tree),crit-bit tree(解决耗费内存问题,double array trie。
使用场景:
字符匹配
前缀树:字符串快速检索,字符串排序,最长公共前缀,自动匹配前缀显示后缀;
后缀树:查找字符串s1在s2中,字符串s1在s2中出现的次数,字符串s1,s2最长公共部分,最长回文串;
radix tree:linux内核,nginx;IP选路
也是前缀匹配,一定程度会用到trie
参考资料:
https://www.zhihu.com/question/30527705
https://www.cnblogs.com/binyue/p/5072264.html
https://www.cnblogs.com/tgycoder/p/5077017.html