树结构大家都不陌生,但是真正应用到代码中的有这么几棵树—二叉排序树、平衡二叉树、B树、B+树、红黑树,无论你有没有了解过,今天看完这个博客统统栽到自己家门前。
二叉树
定义:
是另一种树形结构,他的特点是每个结点之多只有两颗子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次数不能任意颠倒。
性质:
- 1.在二叉树的第i层上之多有2的(i-1)次幂个结点(i>=1)
- 2.深度为k的二叉树之多有2的k次幂 -1 的结点 (k>=1)
- 3.对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
二叉树节点结构:

二叉树的遍历:
1.先序遍历
//递归实现先序遍历
public static void rec_first(Tree_Node tree){
System.out.println(tree.value);
Tree_Node l = tree.lchild;
if(l!=null){
rec_first(l);
}
Tree_Node r = tree.rchild;
if(r!=null){
rec_first(r);
}
}
/**
* 非递归实现,使用栈结构进行实现
*/
public static void stack_first(Tree_Node tree ){
Stack_demo1<Tree_Node> stack = new Stack_demo1<>();
while(tree != null || !stack.isEmpty() )
{
while(tree != null)
{
System.out.println(tree.value);
Nodeclass<Tree_Node> ss = new Nodeclass<>();
ss.setData(tree);
stack.push(ss);
tree = tree.lchild;
}
if(!stack.isEmpty())
{
tree = stack.pop().getData();
// System.out.println(tree.value);
// System.out.println(tree.value);
if (tree.rchild!=null){
tree = tree.rchild;
}else{
tree = stack.pop().getData();
if (tree!=null) {
tree = tree.rchild;
}
}
}
}
}
2.中序遍历:
//递归实现中序遍历
public static void rec_second(Tree_Node tree){
Tree_Node l = tree.lchild;
if(l!=null){
rec_second(l);
}
System.out.println(tree.value);
Tree_Node r = tree.rchild;
if(r!=null){
rec_second(r);
}
}
/**
* 非递归实现,使用栈结构进行实现
*/
public static void stack_second(Tree_Node tree ){
Stack_demo1<Tree_Node> stack = new Stack_demo1<>();
while(tree != null || !stack.isEmpty() )
{
while(tree != null)
{
Nodeclass<Tree_Node> ss = new Nodeclass<>();
ss.setData(tree);
stack.push(ss);
tree = tree.lchild;
}
if(!stack.isEmpty())
{
tree = stack.pop().getData();
System.out.println(tree.value);
// System.out.println(tree.value);
// System.out.println(tree.value);
if (tree.rchild!=null){
tree = tree.rchild;
}else{
tree = stack.pop().getData();
if (tree!=null) {
System.out.println(tree.value);
tree = tree.rchild;
}
}
}
}
}
3.后续遍历:
//递归实现后序遍历
public static void rec_third(Tree_Node tree){
Tree_Node l = tree.lchild;
if(l!=null){
rec_third(l);
}
Tree_Node r = tree.rchild;
if(r!=null){
rec_third(r);
}
System.out.print(tree.value+" -> " );
}
/**
* 非递归实现,使用栈结构进行实现
*/
public static void stack_third(Tree_Node tree ){
Stack_demo1<Tree_Node> stack = new Stack_demo1<>();
Stack_demo1<Tree_Node> stack2 = new Stack_demo1<>();
while(tree!=null||stack!=null){
if (tree!=null){
Nodeclass<Tree_Node> node = new Nodeclass<>();
node.setData(tree);
stack.push(node);
stack2.push(node);
// System.out.println(stack2.top().getData().value);
if (tree.rchild!=null) {
tree = tree.rchild;
}
}else{
tree = stack.pop().getData();
if (tree.lchild!=null) {
tree = tree.lchild;
}else{
tree = stack.pop().getData();
// System.out.println(tree.value);
tree = tree.lchild;
// System.out.println("");
// System.out.println(tree.value);
}
}
}
}
下面开始进入真正内容:
二叉排序树 BFS
定义:
二叉排序树,也叫二叉查找树。
二叉排序树或者是一棵空树,或者是一颗具有下列特性的非空二叉树:
- 若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值
- 若右子树非空,则右子树上所有结点关键字值均大于根节点的关键字值
- 左,右子树本身也分别是一棵二叉排序树
根据二叉排序树的定义,有左子树结点值<根结点值<右子树结点值,所以,对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
一个简单的二叉排序树模型:

二叉排序树的插入:
/**
* 定义添加数据的方法,这不是静态方法,树的根节点调用,传入要插入节点
*/
public void add(Node_tree t){
//如果t的val值比这个节点的val值大,则往这个节点的右子树添加,不过前提是,右子树没有值,否则执行递归
if(t.getVal()>val){
if(rch==null){
rch=t;
}else{
rch.add(t);
}
}else{
//如果t的val值比这个节点的val值小,则往这个节点的左子树添加,不过前提是,左子树没有值,否则执行递归
if(lch==null){
lch=t;
}else{
lch.add(t);
}
}
}
二叉排序树的删除:
/**
* 定义删除的方法,在这里有必要说一下,这么删除操作是有两种方法的,
* 如果删除一个节点,
* 可以找他左子树中最大的数
* 也可以将其右子树的最小的数
* 还需要考虑一个问题就是找到的这个数是否有子节点,
* 1、没有子节点,那就是最好的情况,
* 2、有子节点,这种情况只可能有一个子节点,左或者右
* 应该先将这个子节点连接到当前节点的父节点
* 采用找右子树的最小的数的方法
*
* 删除节点分为三种情况:
* 1、待删除节点有两个子节点
* 2、待删除节点有一个子节点
* 3、待删除节点没有子节点
* 第二第三种情况可以放在一起说
*
*/
public static void del(Node_tree node,int n){
//找到这个节点
Node_tree thisNode = check(node,n);
//找到这个节点的父节点
Node_tree parent = getparent(node,n);
//1、这个节点不存在的话,结束方法
if(thisNode == null){
return;
}
if(thisNode.getLch()!=null&&thisNode.getRch()!=null){
//待删除节点左右子节点都有
Node_tree rm = right_min(thisNode);
//获得这个后继节点的父亲
Node_tree rm_par = getparent(node,rm.getVal());
//将这个后继节点的值给待删除节点
thisNode.setVal(rm.getVal());
//对这个后继节点进行删除,因为它肯定是只有单个子节点或者没有
del_one(rm,rm_par);
return;
}else{
// 2、待删除节点有一个子节点
// 3、待删除节点没有子节点
del_one(thisNode,parent);
}
}
/**
* 封装一个方法,用来删除有一个子节点或者没有子节点的节点
*/
public static void del_one(Node_tree thisNode,Node_tree parent){
//2、待删除节点有一个子节点
// 3、待删除节点没有子节点
if(thisNode.getVal()>parent.getVal()){
//待删除节点是父节点的右子节点
if(thisNode.getRch()!=null){
parent.setRch(thisNode.getRch());
}else if(thisNode.getLch()!=null){
parent.setRch(thisNode.getLch());
}else{
parent.setRch(null);
}
}else{
//待删除节点是父节点的左子节点
if(thisNode.getRch()!=null){
parent.setRch(thisNode.getRch());
}else if(thisNode.getLch()!=null){
parent.setRch(thisNode.getLch());
}else{
parent.setLch(null);
}
}
}
二叉排序树的查找:
以上面图片为例,查找值为9的节点
- 找到根节点(10),发现比10小,向左子树查询
- 找到左子树根节点(5),发现比5大,向其右子树查询
- 发现相等,返回

二叉平衡树 AVL
定义:
为了避免树的高度增长过快,降低二叉排序树的性能,我们规定在插入和删除二叉树结点时,要保证任意节点的左、右子树高度差的绝对值最多不超过1,将这样的二叉树称之为平衡二叉树
定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1,0,11.平衡二叉树可以是一颗空树,或者是具有下列性质的二叉树:
2.①它的左子树和右子树都是平衡二叉树,且左子树和右子树 的高度差的绝对值不超过1
图片中节点旁边的数字为平衡因子的值
二叉排序树保证平衡点的基本思想:每当在二叉排序树中插入(删除)一个结点时,首先要检查其插入路径上的结点是否因为此次操作而导致了了不平衡。如果导致了不平衡,则先找到插入路径和是哪个离插入点最近的而平衡因子绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
插入0

关于旋转,后面文章会进行介绍,我学习时发现这个文章不错
二叉平衡树查找
在平衡二叉树上进行查找的过程和二叉排序树相同,因此,在查找的过程中和给定值进行比较的关键字个数不超过树的深度。假设以Nh表示深度为h的平衡树中含有的最少结点数。显然N0=0,N1=1,N2=2,并且有Nh=N(h-1)+N(h-2)+1
B树
定义:
我们首先要知道多路查找树的概念,它的定义如下:
多路查找树(multi-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。
B树是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶(Order)
一个m阶的B树具有如下属性:
如果根结点不是叶结点,则其至少有两棵子树。
每一个非根的分支结点都有k-1个元素和k个孩子,其中[m/2]≤ k ≤ m。每一个叶子结点 n都有k-1个元素,其中[m/2]≤ k ≤ m。
所有叶子结点都位于同一层次。
所有分支结点包含下列信息数据( n, A0, K1, A1, K2, A2, …, Kn, An ),其中: Ki( i=1, 2, …, n ) 为关键字,且Ki<Ki+1( i=1, 2, …,n-1 ); Ai ( i=0, 2, …, n) 为指向子树根结点的指针,且指针Ai-1所指子树中所有结点的关键字均小于Ki( i=1,2, …, n ) ,An 所指子树中所有结点的关键字均大于Kn,n ( [m/2]-1 ≤ n ≤ m-1 ) 为关键字的个数(或 n+1为子树的个数)。
模拟一个2-3树的创建过程,以此来帮助理解
其具有的性质:
- 一个2结点包含一个元素和两个孩子(或没有孩子),和二叉排序树一致,左子树包含的元素小于该元素,右子树包含的元素大于该元素。但是这个2结点要么有两个孩子,要么没有孩子,不能只有一个孩子。
- 一个3结点包含两个元素和三个孩子(或没有孩子),左子树、较小元素、中间子树、较大元素和右子树也按照从小到大排序。一个3结点要么有三个孩子,要么没有孩子。
- 2-3树的所有叶子结点都在同一层次上。

创建步骤:
1.创建值为1的节点

2.插入7,将节点升级为3节点

3.接下来插入4,可以发现根结点已经是3结点了,因为必须是平衡的,所以只能把根结点拆开,变为3个2结点,如下所示

4.插入9时,因为9比4大,所以插入到右侧,而7所在结点可以升级为3结点,所以插入结果如下所示:

5.接下来要插入15,因为9所在结点已经是3结点,但是它的父结点4是2结点,所以可以把4所在结点升级,因为3结点必须有三个孩子,所以7和9所在结点需要拆分,如下所示:

6.接下来插入13和6时,对应节点都可以升级,所以插入结果如下:

7.接下来插入元素5时,发现6所在结点已经是3结点,而父结点,也就是根结点也是3结点了,这时只能再次拆分。首先,5、6、7中间的数是6,我们把它提出来,它应该位于4和9中间,如下所示:

因为3结点只能有两个元素,所以根结点也必须拆分,结果如下:

8.可以发现,根结点拆分后使得树的高度增加了。接下来插入8,10,3也是重复步骤,结果如下:

9.至此,再插入元素12、14、2时也变得十分简单了,结果如下:

10.最后插入11,可以发现它在10和12之间,而父结点也是3结点,所以10和12要拆分,9和13也要拆分,11应该和6一起升级为3结点,结果如下:

B树的删除:
有以下几种情况的删除:
1.删除叶节点,直接删除
2.删除有孩子的节点,将孩子节点进行升级或者拆分,替代原节点(不过这里要注意的是,如果替换之后,子树的深度发生改变,那么就需要将使得树整体不平衡,所以需要将子树的上一节点在改变性质的情况下向下移动一层)
B树的应用:
B树大量应用在数据库和文件系统当中。
它的设计思想是,将相关数据尽量集中在一起,以便一次读取多个数据,减少硬盘操作次数。B树算法减少定位记录时所经历的中间过程,从而加快存取速度。
假定一个节点可以容纳100个值,那么3层的B树可以容纳100万个数据,如果换成二叉查找树,则需要20层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在100万个数据中查找目标值,只需要读取两次硬盘。
如mongoDB数据库使用,单次查询平均快于Mysql(但侧面来看Mysql至少平均查询耗时差不多)
B+树
一颗比B树更帅的兄弟,查询速度比B树更快
一棵 B+ 树需要满足以下条件:
节点的子树数和关键字数相同(B 树是关键字数比子树数少一) 节点的关键字表示的是子树中的最大数,在子树中同样含有这个数据
叶子节点包含了全部数据,同时符合左小右大的顺序
一个例子:

简单概括下 B+ 树的三个特点:
- 关键字数和子树相同
- 非叶子节点仅用作索引,它的关键字和子节点有重复元素
- 叶子节点用指针连在一起
首先第一点不用特别介绍了,在 B 树中,节点的关键字用于在查询时确定查询区间,因此关键字数比子树数少一;而在 B+ 树中,节点的关键字代表子树的最大值,因此关键字数等于子树数。
第二点,除叶子节点外的所有节点的关键字,都在它的下一级子树中同样存在,最后所有数据都存储在叶子节点中。
根节点的最大关键字其实就表示整个 B+ 树的最大元素。
第三点,叶子节点包含了全部的数据,并且按顺序排列,B+ 树使用一个链表将它们排列起来,这样在查询时效率更快。
由于 B+ 树的中间节点不含有实际数据,只有子树的最大数据和子树指针,因此磁盘页中可以容纳更多节点元素,也就是说同样数据情况下**,B+ 树会 B 树更加“矮胖”**,因此查询效率更快。
B+ 树的查找必会查到叶子节点,更加稳定。
有时候需要查询某个范围内的数据,由于 B+ 树的叶子节点是一个有序链表,只需在叶子节点上遍历即可,不用像 B 树那样挨个中序遍历比较大小。
B+ 树的三个优点:
层级更低,IO 次数更少
每次都需要查询到叶子节点,查询性能稳定
叶子节点形成有序链表,范围查询方便
B+树的创建过程:
淘到一个牛皮的动态图片,B+树创建过程一目了然

图片来源网址:https://blog.csdn.net/luzhensmart/article/details/88617936
B+树的应用:
mysql使用B+树作为索引:
本文深入讲解二叉树、二叉排序树、平衡二叉树、B树、B+树等树结构,涵盖定义、性质、遍历算法及应用场景,特别强调B+树在数据库索引中的优势。


922

被折叠的 条评论
为什么被折叠?



