二叉树
基本的概念问题:
- 二叉树:是指一个树节点最多有两个与之相连的节点(例如图1所示)
- 二叉排列树:一个节点的左侧叶子点数都小于该节点,右侧叶子节点都大于该节点(如图2所示)
- 满二叉树:所有的叶子结点都是在最后一层,其中节点的总数为(2^n-1)。(如图2所示)
- 完全二叉树:是指二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点左连续,倒数第二层的叶子结点右连续。(如图3所示)
- 第n层的最大节点数量为:2^(n-1)
二叉树的遍历
二叉树的遍历根据父节点的遍历顺序分为:前序遍历、中序遍历、后序遍历
- 前序遍历:首先遍历父节点,然后在遍历左子树、右叶子树
- 中序遍历:首先遍历左子树,父节点、遍历右子树
- 后序遍历:首先遍历左子树、右子树,最后是父节点
public class TreeNode {
public int value;
public TreeNode leftNode;
public TreeNode rightNode;
public TreeNode(int value) {
this.value = value;
}
@Override
public String toString() {
return "TreeNode [value=" + value + "]";
}
// 前序遍历
public void preOrder() {
System.out.println(this.value);
if(this.leftNode != null) {
this.leftNode.preOrder();
}
if(this.rightNode != null) {
this.rightNode.preOrder();
}
}
//中序遍历
public void midOrder() {
if(this.leftNode != null) {
this.leftNode.midOrder();
}
System.out.println(this.value);
if(this.rightNode != null) {
this.rightNode.midOrder();
}
}
// 后序遍历
public void lastOrder() {
if(this.leftNode != null) {
this.leftNode.lastOrder();
}
if(this.rightNode != null) {
this.rightNode.lastOrder();
}
System.out.println(this.value);
}
}
二叉树的查找:
前序查找
思路:
- 首先判断当前节点的value是否为要找的内容。如果相等,返回当前节点
- 如果不等,则判断当前节点的左子节点是否为空,如果不为空,则进行左递归查找。
- 如果左递归中没有找到节点,则继续判断右节点是否为空,如果不为空,则执行右递归查找,如果找到,则返回节点。
如果右递归没有找到节点,则返回为空
public TreeNode preOrderSearch(int value) {
if(this.value == value) {
return this;
}
TreeNode result = null;
if(this.leftNode != null) {
result = this.leftNode.preOrderSearch(value);
}
if(result != null) {
return result;
}
if(this.rightNode != null) {
result = this.rightNode.preOrderSearch(value);
}
return result;
}
中序查找:
- 首先判断当前的左子节点是否为空,如果不为空,则左递归中序查找
- 如果左递归找到节点,则返回;如果没有找到,就和当前节点进行比较,如果相等,则返回当前节点;如果不相等,则需要右递归中序查找
- 如果右递归中序查找找到节点,则返回该节点,如果没有找到,则返回空
//中序查找
public TreeNode midOrderSearch(int value) {
TreeNode res = null;
if(this.leftNode != null) {
res = this.leftNode.midOrderSearch(value);
}
if(res != null) {
return res;
}
if(this.value == value) {
return this;
}
if(this.rightNode != null) {
res = this.rightNode.midOrderSearch(value);
}
return res;
}
后序查找
- 首先判断左子节点是否为空,如果不为空,进行左递归的后序查找,如果找到,则返回该节点
- 如果左子节点为空或没有找到,则判断右子节点是否为空,如果不为空,则执行右递归的后序查找,如果找到节点就返回
如果右递归没有找到节点,就与当前节点进行比较,如果相等则返回当前节点;如果不相等,则返回为空。
//后序查找
public TreeNode lastOrderSearch(int value) {
TreeNode res = null;
if(this.leftNode != null) {
res = this.leftNode.lastOrderSearch(value);
}
if(res != null) return res;
if(this.rightNode != null) {
res = this.rightNode.lastOrderSearch(value);
}
if(res != null) return res;
if(this.value == value) return this;
return null;
}
删除树节点
规定:
- 如果删除的是叶子结点,则删除该节点
- 如果删除的是非叶子结点,则删除该树
思路:因为我们的二叉树是单向的,所以删除的时候不能到达该节点时在判断
- 删除时,首先判断该节点是否空,如果为空,直接返回。然后再判断是否为要删除的节点,如果是,则将root节点置空,并且返回。
- 如果删除的不是root节点,则判断左子节点是否为空。如果不为空,则递归左子节点
- 如果在左子节点中没有删除的节点,则继续递归右子节点。
顺序存储二叉树
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换为树,树也可以转换为数组。
要求:
- 二叉树节点以数组的形式进行存放
- 要求在遍历数组的时候,仍然可以以前序遍历、中序遍历和后序遍历的方式完成节点的遍历。
顺序二叉树的特点:
- 顺序二叉树通常只考虑完全二叉树
- 第n个元素的左子节点为 2*n + 1 (在数组中的位置)
- 第n个元素的右子节点为 2*n + 2 (在数组中的位置)
- 第n个元素的父节点为 (n-1)/2 (在数组中的位置)
n表示二叉树中第n个元素(从0开始)
具体的代码实现为:
import java.util.Arrays;
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] arr = {1, 2, 3, 4, 5 , 6, 7};
ArrBinaryTree tree = new ArrBinaryTree(arr);
System.out.println("打印原始数组:" + Arrays.toString(arr));
System.out.println("前序遍历:");
tree.preOrder(0);
System.out.println("中序遍历:");
tree.midOrder(0);
System.out.println("后序遍历:");
tree.lastOrder(0);
}
}
class ArrBinaryTree {
public int[] arr;
public ArrBinaryTree(int[] arr) {
// TODO Auto-generated constructor stub
this.arr = arr;
}
//进行前序遍历
public void preOrder(int index) {
if(arr == null || index < 0 ) {
System.out.println("输入的参数有问题");
return;
}
if(index >= arr.length) return;
System.out.println(arr[index]);
// 遍历左子树
preOrder(2 * index + 1);
// 遍历右子树
preOrder(2 * index + 2);
}
// 中序遍历
public void midOrder(int index) {
if(arr == null || index < 0 ) {
System.out.println("输入的参数有问题");
return;
}
if(index >= arr.length) return;
// 遍历左子树
midOrder(2 * index + 1);
System.out.println(arr[index]);
// 遍历右子树
midOrder(2 * index + 2);
}
// 后序遍历
public void lastOrder(int index) {
if(arr == null || index < 0 ) {
System.out.println("输入的参数有问题");
return;
}
if(index >= arr.length) return;
// 遍历左子树
lastOrder(2 * index + 1);
// 遍历右子树
lastOrder(2 * index + 2);
System.out.println(arr[index]);
}
}
线索二叉树
基本的概念:
- n个节点的二叉链表中含有n+1(2n-(n-1))个空指针域,利用二叉链表中的指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针。这种附加的指针被称之为“线索”。
- 这种加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索的性质不同,线索二叉树可以分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
- 一个节点的前一个节点,称之为前驱节点
- 一个节点的后一个节点,称之为后继节点
- 线索二叉树后,Node的left节点与right节点有可能指向子树或者指向前驱与后继节点。需要在节点中添加标记,标记left指向左子树或前驱。
平衡二叉树(AVL)
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树。
二叉查找树(二叉搜索树、BST)
若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
任意节点的左、右子树也分别为二叉查找树;
没有键值相等的节点。
霍夫曼树
带权路径最短的二叉树称为哈夫曼树或最优二叉树。
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”,有时也叫“赫夫曼树”或者“哈夫曼树”。
红黑树
红黑树是一颗特殊的二叉查找树,除了二叉查找树的要求外,它还具有以下特性:
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
旋转
前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
- 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了。
- 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。
- 变色:结点的颜色由红变黑或由黑变红。
查找
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
- 从根结点开始查找,把根结点设置为当前结点;
- 若当前结点为空,返回null;
- 若当前结点不为空,用当前结点的key跟查找key作比较;
- 若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;
插入
插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父结点很简单,跟查找操作区别不大:
- 从根结点开始查找;
- 若根结点为空,那么插入结点作为根结点,结束。
- 若根结点不为空,那么把根结点作为当前结点;
- 若当前结点为null,返回当前结点的父结点,结束。
- 若当前结点key等于查找key,那么该key所在结点就是插入结点,更新结点的值,结束。
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤4;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤4;
删除
红黑树的删除操作也包括两部分工作:一查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。
二叉树删除结点找替代结点有3种情情景:
- 情景1:若删除结点无子结点,直接删除
- 情景2:若删除结点只有一个子结点,用子结点替换删除结点
- 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点
B-tree(B-树或者B树)
B树也称B-树,它是一颗多路平衡查找树。二叉树我想大家都不陌生,其实,B树和后面讲到的B+树也是从最简单的二叉树变换而来的,并没有什么神秘的地方,下面我们来看看B树的定义。
- 每个节点最多有m-1个关键字(可以存有的键值对)。每个节点最多有m个子树。
- 根节点最少可以只有1个关键字。
- 非根节点至少有m/2个关键字。
- 每个节点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
- 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。
- 每个节点都存有索引和数据,也就是对应的key和value。
所以,根节点的关键字数量范围:1 <= k <= m-1
,非根节点的关键字数量范围:m/2 <= k <= m-1
。
另外,我们需要注意一个概念,描述一颗B树时需要指定它的阶数,阶数表示了一个节点最多有多少个孩子节点,一般用字母m表示阶数。
插入
插入的时候,我们需要记住一个规则:判断当前结点key的个数是否小于等于m-1,如果满足,直接插入即可,如果不满足,将节点的中间的key将这个节点分为左右两部分,中间的节点放到父节点中即可。
例子:在5阶B树中,结点最多有4个key,最少有2个key(注意:下面的节点统一用一个节点表示key和value)。
- 插入18,70,50,40
- 插入22
插入22时,发现这个节点的关键字已经大于4了,所以需要进行分裂,分裂的规则在上面已经讲了,分裂之后,如下。
- 接着插入23,25,39
删除
- 对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。
如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点
- 如果删除后不满足B树的要求,,所以,我们需要考虑向兄弟节点借元素。
- 如果兄弟节点中没有多余的节点(2个),那么应该首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点。移动之后,跟兄弟节点合并。
B树的删除操作相对于插入操作是相对复杂一些的,但是,你知道记住几种情况,一样可以很轻松的掌握的。
- 现在有一个初始状态是下面这样的B树,然后进行删除操作。
- 删除15,这种情况是删除叶子节点的元素,如果删除之后,节点数还是大于
m/2
,这种情况只要直接删除即可。
- 接着,我们把22删除,这种情况的规则:22是非叶子节点,对于非叶子节点的删除,我们需要用后继key(元素)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。对于删除22,需要将后继元素24移到被删除的22所在的节点。
此时发现26所在的节点只有一个元素,小于2个(m/2),这个节点不符合要求,这时候的规则(向兄弟节点借元素):如果删除叶子节点,如果删除元素后元素个数少于(m/2),并且它的兄弟节点的元素大于(m/2),也就是说兄弟节点的元素比最少值m/2还多,将先将父节点的元素移到该节点,然后将兄弟节点的元素再移动到父节点。这样就满足要求了。
我们看看操作过程就更加明白了。
- 接着删除28,删除叶子节点,删除后不满足要求,所以,我们需要考虑向兄弟节点借元素,但是,兄弟节点也没有多的节点(2个),借不了,怎么办呢?如果遇到这种情况,首先,还是将先将父节点的元素移到该节点,然后,将当前节点及它的兄弟节点中的key合并,形成一个新的节点。
移动之后,跟兄弟节点合并。
删除就只有上面的几种情况,根据不同的情况进行删除即可。
B+树
B+树其实和B树是非常相似的,我们首先看看相同点。
- 根节点至少一个元素
- 非根节点元素范围:m/2 <= k <= m-1
不同点。
- B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。
- 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
- 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
- 父节点存有右孩子的第一个元素的索引。
插入
对于插入操作很简单,只需要记住一个技巧即可:当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的。
下面以一颗5阶B+树的插入过程为例,5阶B+树的节点最少2个元素,最多4个元素。
- 插入5,10,15,20
- 插入25,此时元素数量大于4个了,分裂
- 接着插入26,30,继续分裂
删除节点
叶子节点有指针的存在,向兄弟节点借元素时,不需要通过父节点了,而是可以直接通过兄弟节移动即可(前提是兄弟节点的元素大于m/2),然后更新父节点的索引;如果兄弟节点的元素不大于m/2(兄弟节点也没有多余的元素),则将当前节点和兄弟节点合并,并且删除父节点中的key。
- 初始状态
- 删除10,删除后,不满足要求,发现左边兄弟节点有多余的元素,所以去借元素,最后,修改父节点索引
- 删除元素5,发现不满足要求,并且发现左右兄弟节点都没有多余的元素,所以,可以选择和兄弟节点合并,最后修改父节点索引
- 发现父节点索引也不满足条件,所以,需要做跟上面一步一样的操作
与B树相比较
B+树相对于B树有一些自己的优势,可以归结为下面几点。
- 单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了。
- 所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。
- 所有的叶子节点形成了一个有序链表,更加便于查找。
特点
1、B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
2、B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
3、B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
4、B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
B*树
B-tree是B+-tree的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),**B树中非根和非叶子结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。给出了一个简单实例,如下图所示: