前言
上篇文章我们介绍了哈夫曼编码的具体应用——数据压缩和数据解压的相关思路以及二叉排序树的相关思路和具体实现。本文主要给大家介绍平衡二叉树和多路查找树,包括平衡二叉树的旋转、删除等相关操作的实现以及B树、B+树等相关的知识。首先介绍平衡二叉树的相关内容。
一、平衡二叉树
平衡二叉树也叫平衡二叉搜索树,同时也叫AVL树,可以保证查询效率较高。具有以下的特点:
- 1、它是一颗空树或它的左右两个子树的高度差的绝对值不超过1
- 2、左右两颗子树都是一颗平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
以下是平衡二叉树和非平衡二叉树的区别:
首先给大家介绍单旋转问题,这里先介绍左旋转。我们给定一个数组{4,3,6,5,7,8}
,创建出对应的平衡二叉树。具体思路如下:
接着介绍右旋转,我们以数组{10,12,8,9,7,6}
,创建出对应的平衡二叉树。具体思路如下:
我们在前面介绍了左旋转和右旋转,一般的非平衡二叉树通过上述的介绍就可以旋转为平衡二叉树。但是并非所有的非平衡二叉树通过以上介绍的左旋转和右旋转可以实现,这时候我们介绍双旋转。我们将数组{10,12,8,9,7,6}
通过右旋转后并没有得到平衡二叉树,具体的旋转过程如下:
通过上图我们不难发现:
1. 当符号右旋转的条件时
2. 如果它的左子树的右子树高度大于它的左子树的高度
3. 先对当前这个结点的左节点进行左旋转
4. 在对当前结点进行右旋转的操作即可
具体的解决思路如下:
当符合右旋转的条件时,如果它的左子树的右子树高度大于它的左子树高度。先对当前这个结点的左结点进行左旋转,在对当前结点进行右旋转操作即可。
具体相应的代码实现如下:
public class AVLTreeDemo {
public static void main(String[] args) {
//int[] arr = {4,3,6,5,7,8};
//int[] arr = { 10, 12, 8, 9, 7, 6 };
int[] arr = {
10, 11, 7, 6, 8, 9 };
//创建一个 AVLTree对象
AVLTree avlTree = new AVLTree();
//添加结点
for(int i=0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
//遍历
System.out.println("中序遍历");
avlTree.infixOrder();
System.out.println("在平衡处理~~");
System.out.println("树的高度=" + avlTree.getRoot().height()); //3
System.out.println("树的左子树高度=" + avlTree.getRoot().leftHeight()); // 2
System.out.println("树的右子树高度=" + avlTree.getRoot().rightHeight()); // 2
System.out.println("当前的根结点=" + avlTree.getRoot());//8
}
}
// 创建AVLTree
class AVLTree {
private Node root;
public Node getRoot() {
return root;
}
// 查找要删除的结点
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
// 查找父结点
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
public int delRightTreeMin(Node node) {
Node target = node;
// 循环的查找左子节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
delNode(target.value);
return target.value;
}
// 删除结点
public void delNode(int value) {
if (root == null) {
return;
} else {
// 1.需求先去找到要删除的结点 targetNode
Node targetNode = search(value);
// 如果没有找到要删除的结点
if (targetNode == null) {
return;
}
// 如果我们发现当前这颗二叉排序树只有一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
// 去找到targetNode的父结点
Node parent = searchParent(value);
// 如果要删除的结点是叶子结点
if (targetNode.left == null && targetNode.right == null) {
// 判断targetNode 是父结点的左子结点,还是右子结点
if (parent.left != null && parent.left.value == value) {
// 是左子结点
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {
// 是由子结点
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) {
// 删除有两颗子树的节点
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else {
// 删除只有一颗子树的结点
// 如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
// 如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.left;
} else {
// targetNode 是 parent 的右子结点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else {
// 如果要删除的结点有右子结点
if (parent != null) {
// 如果 targetNode 是 parent 的左子结点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else {
// 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
// 添加结点的方法
public void add(Node node) {
if (root == null) {
root = node;// 如果root为空则直接让root指向node
} else {
root.add(node);
}
}
// 中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
} else {
System.out.println("二叉排序树为空,不能遍历");
}
}
}
// 创建Node结点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
// 返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
// 返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
// 返回 以该结点为根结点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
//左旋转方法
private void leftRotate() {
//创建新的结点,以当前根结点的值
Node newNode = new Node(value);
//把新的结点的左子树设置成当前结点的左子树
newNode.left = left;
//把新的结点的右子树设置成带你过去结点的右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成当前结点右子树的右子树
right = right.right;
//把当前结点的左子树(左子结点)设置成新的结点
left = newNode;
}
//右旋转
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
public Node search(int value) {
if (value == this.value) {
// 找到就是该结点
return this;
} else if (value < this.value) {
// 如果查找的值小于当前结点,向左子树递归查找
// 如果左子结点为空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else {
// 如果查找的值不小于当前结点,向右子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
public Node searchParent(int value) {
// 如果当前结点就是要删除的结点的父结点,就返回
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
return this;
} else {
// 如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); // 向左子树递归查找
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); // 向右子树递归查找
} else {
return null; // 没有找到父结点
}
}
}
@Override
public String toString() {
return "Node [value=" + value + "]";
}
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的结点的值,和当前子树的根结点的值关系
if (node.value < this.value) {
// 如果当前结点左子结点为null
if (this.left == null) {
this.left = node;
} else {
// 递归的向左子树添加
this.left.add(node);
}
} else {
// 添加的结点的值大于 当前结点的值
if (this.right == null) {
this.right = node;
} else {
// 递归的向右子树添加
this.right.add(node);
}
}
//当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
if(rightHeight() - leftHeight() > 1) {
//如果它的右子树的左子树的高度大于它的右子树的右子树的高度
if(right != null && right.leftHeight() > right.rightHeight()) {
//先对右子结点进行右旋转
right.rightRotate();
//然后在对当前结点进行左旋转
leftRotate(); //左旋转..
} else {
//直接进行左旋转即可
leftRotate();
}
return ; //必须要!!!
}
//当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
if(leftHeight() - rightHeight() > 1) {
//如果它的左子树的右子树高度大于它的左子树的高度
if(left != null && left.rightHeight() > left.leftHeight()) {
//先对当前结点的左结点(左子树)->左旋转
left.leftRotate();
//再对当前结点进行右旋转
rightRotate();
} else {
//直接进行右旋转即可
rightRotate();
}
}
}
// 中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
具体的执行结果如下:
二、二叉树与B树
二叉树在查找的过程效率很高,但是也存在一定的问题,具体如下:
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多,就会存在问题,首先在构建二叉树时,需要多次进行I/O操作,节点也需要海量,这就影响到速度。这就需要多叉树了。在二叉树中,每个节点都是数据项,最多有两个节点。如果允许每个节点可以有更多的数据项和 更多的子结点,这就是多叉树。多叉树通过重新组织节点,减少树的高度。能对二叉树进行优化。以下就是2-3树:
B树就是通过重新组织节点,降低树的高度,并且减少I/O读写次数来提升查询速度。具体示意图如下:
如果B树通过重新组织节点,降低了树的高度。
文件系统及数据库系统的设计者利用了磁盘的预读原理,将一个节点的大小设为等于一个页,不过这里需要注意的是:页的大小通常是4K。这样每个节点只需要进行一次I/O就可以了。
另外将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素,B树广泛应用于文件存储系统以及数据库系统中。
三、2-3树
1、2-3树的所有叶子节点都在同一层
2、有两个子结点的节点叫二节点,二节点要么没有子结点,要么有两个子结点。
3、有三个子结点的节点叫三节点,三节点要么没有子结点,要么有三个子结点。
我们将数组{16,24,12,32,14,26,34,10,8,28,38,20}
构建成2-3树,并保证数据插入的大小顺序。具体过程如下图所示:
具体的插入规则如下:
1、2-3树的所有叶子节点都在同一层
2、有两个子结点的节点叫二节点,二节点要么没有子结点,要么有两个子结点。
3、有三个子结点的节点叫三节点,三节点要么没有子结点,要么有三个子结点。
当按照规则插入一个树到某个节点时,若不能满足上面的需求,就需要拆,先向上拆,如果上层满足,则拆本层。拆完之后的子树应该满足上面三个条件。
对于三个节点的子树的值大小仍然遵守二叉排序树的规则。
四、B树、B+树、B*树
其实我们前面介绍的2-3树就是B树中的一种,如果学过MySQL中的读者不难发现,在介绍MySQL的时候,我们索引的原理就是基于B树或者是B+树的。具体如图所示:
针对上图,我们对B树涉及到的专业术语做统一说明:
1、B树的阶:节点的最多子结点个数。比如:2-3树的阶为3,2-3-4树的阶为4
2、B-树的搜索:从根节点开始,对节点内的关键字(有序)列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子节点;重复,直到所对应的儿子指针为空,或已经是叶子节点。
3、关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放在数据中
4、搜索有可能在非叶子节点结束
5、其搜索性能等价于在关键字全集内做一次二分查找。
其实B+树是B树的一种变体。也是一种多路搜索树,具体如下:
针对以上的B+树图我们做以下的说明:
1、B+树的搜索与B树也基本相同,区别在于B+树只有达到叶子节点才命中,其性能等价于在关键字全集做一次二分查找
2、所有关键字都出现在叶子节点的链表中,且链表中的关键字恰好是有序的。
3、不可能在非叶子节点中命中
4、非叶子节点相当于叶子节点的索引,叶子结点相当于是存储数据的数据层
同理,B树是B+树的变体,在B+树的非根和非叶子节点在增加指向兄弟的指针。具体如图所示:
我们针对B树作以下说明:
B树定义了非叶子节点关键字个数至少为
(2/3)*M
,即块的最低使用率为2/3,而B+树的块的最低使用率为1/2.
2、从第1个特点我们可以看出:B*树分配新节点的概率要比B+树要低,空间使用率更高。
总结
上篇文章我们介绍了哈夫曼编码的具体应用——数据压缩和数据解压的相关思路以及二叉排序树的相关思路和具体实现。本文主要给大家介绍了平衡二叉树和多路查找树,包括平衡二叉树的旋转、删除等相关操作的实现以及B树、B+树等相关的知识。其实数据结构与算法是特别重要的,在编程中有至关重要的地位,因此,需要我们特别的掌握。生命不息,奋斗不止,我们每天努力,好好学习,不断提高自己的能力,相信自己一定会学有所获。加油!!!