大家好,我是被白菜拱的猪。
一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。
一、写在前言
还有一天,后天就要回学校了,心里无比的激动,但是暑假开始之前信誓旦旦说要完成这个完成那个,到头来什么也没完成,SpringBoot没有学完,Vue也没有学完,数据结构还有部分内容,嗐,今天把树的剩余内容搞定,明天再学习图,算法部分就留到学校吧,过几天在重新制定好计划。开学了可不能在像暑假这么懒散了,得讲究效率。
二、平衡二叉树
(一)平衡二叉树介绍
在介绍一个新的东西时,我总是从三方面出发,用三个词概括为 why,what,how。即这个东西为什么会出现,出现的原因是什么,然后这到底是一个什么样的东西,即介绍他的概念,最后如何怎样去构造他,这里就是如何用代码来实现。下面就说说平衡二叉树为什么出现。
1、Why
首先平衡二叉树(Self-Balancing Binary Search Tree)本质来说是一种特殊的前面介绍的二叉排序树,正如它因为中带个search,所以他是弥补二叉排序树查找的缺陷,如下图所示:
这也是二叉排序树,与单链表非常的相似,插入效率不受影响,但是查找因为每次都要判断是否有左子树,所以查找的效率甚至不如单链表。所以平衡二叉树横空出世。
2、What
平衡二叉树又叫平衡二叉搜索树,又叫AVL树,这是发明出它的两个人的名字的缩写,它的查询效率比较高。
它的特点是对于任何一个结点,它的左子树和右子树的高度差小于等于1,这就保证了不会有上面说的那种单链表的形式的存在。
3、how
在《大话数据结构》这本书中,是这样构建二叉排序树的,每当插入一个结点,先检查是否因插入而导致破坏树的平衡性,假如破坏了平衡性,就去找最小不平衡子树,然后调整最小不平衡子树的结点的关系使其平衡,这里的难点在于如何找到最小不平衡子树,另外还有在子树旋转过后会出现不是二叉排序树,然后还要解决这个问题,我认为书中有些过于复杂,思想比较好理解,但是代码实现有些困难,于是避开了这种思想,直接由根结点出发,去旋转,具体思路见下叙述。
(二)平常二叉树构造
我们大体的思路是在创建二叉树的同时(即添加新的结点后)就去判断是否破坏平衡性,那么如何去判断平衡性是否被破坏呢,我们可以计算左右子树的高度。
1、左旋转
假如右子树的高度 - 左子树的高度 > 1,则要左旋转,如何左旋转呢?如何所示:
该树的右子树的高度为3,左子树的高度为1,相差为2>1,所以根结点向左旋转,根节点的右子结点变为根节点,然后之前根结点的右子结点变为右子结点的左子结点,这样读起来比较绕,其实画一下图就一目了然,图中3的右节点为5,现在变为5的左节点4,而根结点变为5。
有两种实现方法,一是直接将5这个结点当做跟结点,即取代3的位置,但是这里会出现一个问题,假如此时是将右子树左翻转,那么此时根节点就是5,那么利用这种思路,5的上面即父节点指向就会混乱,所以保险起见,我们将他们之前的指向关系不变,而指改变他们的值,5这个结点最后要变成根结点,那么我们将原先3这个的值变为5即可,然后重新创建一个结点来代表之前3这个结点,在这个新创的结点的基础上来指明他的左右子节点是谁就会相对的简单,且思路明了。
具体代码如下:
//左旋转
public void leftRotate() {
//1.根据当前结点的值新创建一个结点
Node newNode = new Node(this.value);
//2.让新结点的左子结点指向挂在当前结点的左子结点上
newNode.left = this.left;
//3.让新节点的右子结点指向当前结点的右子结点的左子节点
newNode.right = this.right.left;
//4.新结点就挂好了,然后来解决当前结点
this.value = this.right.value;
this.left = newNode;
this.right = this.right.right;
}
2、右旋转
右旋转的原理如左旋转一样,右旋转适用于左子树的高度 - 右子树的高度 > 1。具体思路见左旋转,这里不再做过多的叙述。
具体代码如下:
//右旋转
public void rightRotate() {
Node newNode = new Node(this.value);
newNode.right = this.right;
newNode.left = this.left.right;
this.value = this.left.value;
this.right = newNode;
this.left = this.left.left;
}
3、双旋转
你们以为这就结束了吗?nonono,还有一种特殊情况,让我这个灵魂画家作画一副,你们便知。
不难发现,假如是这种情况下经过上面的左旋转之后仍旧是不平衡的,问题就在于根结点的右子结点的左子树的高度大于右子树的高度,我们解决办法是先让根结点的右子树向右旋转然后根节点在像左旋转,这就是双旋转。
右旋转同样的道理。假如根节点的左子结点的右子树的高度大于左子树,先让右子树左旋转,然后根节点在右旋转。
部分代码如下:
//在添加之后就要判断是否破坏了树的平衡性
if((rightHeight() - leftHeight()) > 1) { //当右边高,就要左旋转
if(this.right != null && this.right.leftHeight() > this.right.rightHeight()) {
this.right.rightRotate();
}
leftRotate();
return;
}
if ((leftHeight() - rightHeight() > 1)) { //当左边高就要右旋转
if(this.left != null && this.left.rightHeight() > this.left.leftHeight()) {
this.left.leftRotate();
}
rightRotate();
return;
}
(三)代码实现
package com.codingboy.avltree;
/**
* @author: ljl
* @date: 2020/8/26 18:33
* @description: 平衡二叉树
*/
public class AVLTreeDemo {
public static void main(String[] args) {
//int[] arr = {3,1,5,4,7,6};
//int[] arr = {6,3,2,1,4,7};
int[] arr = {2,1,5,4,3,6};
AVLTree tree = new AVLTree();
for (int i = 0; i < arr.length ; i++) {
tree.addNode(new Node(arr[i]));
}
System.out.println(tree.root.height());
System.out.println(tree.root.right.height());
System.out.println(tree.root.left.height());
tree.infixOrder();
}
}
//二叉树
class AVLTree {
//根结点
Node root;
//中序遍历
public void infixOrder() {
if (root == null) {
System.out.println("该二叉排序树为空");
} else {
root.infixOrder();
}
}
//添加结点
public void addNode(Node node) {
if (root == null) {
root = node;
} else {
root.addNode(node);
}
}
}
//结点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//添加结点
public void addNode(Node node) {
if (node == null) {
return;
}
//然后跟当前结点值比较,小于则添加到其左边
if (node.value < this.value) {
if (this.left == null) { //左子结点为空,就插进去
this.left = node;
} else { //不为空就继续比较
this.left.addNode(node);
}
} else {
if (this.right == null) { //左子结点为空,就插进去
this.right = node;
} else { //不为空就继续比较
this.right.addNode(node);
}
}
//在添加之后就要判断是否破坏了树的平衡性
if((rightHeight() - leftHeight()) > 1) { //当右边高,就要左旋转
if(this.right != null && this.right.leftHeight() > this.right.rightHeight()) {
this.right.rightRotate();
}
leftRotate();
return;
}
if ((leftHeight() - rightHeight() > 1)) { //当左边高就要右旋转
if(this.left != null && this.left.rightHeight() > this.left.leftHeight()) {
this.left.leftRotate();
}
rightRotate();
return;
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
//递归求解树的高度
public int height() {
//左右子树的的最大值+1
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height()) + 1;
}
//当前结点的右子树高度
public int rightHeight() {
if (right == null ) {
return 0;
}else {
return right.height();
}
}
//当前结点的左子树高度
public int leftHeight() {
if (right == null ) {
return 0;
}else {
return left.height();
}
}
//左旋转
public void leftRotate() {
//1.根据当前结点的值新创建一个结点
Node newNode = new Node(this.value);
//2.让新结点的左子结点指向挂在当前结点的左子结点上
newNode.left = this.left;
//3.让新节点的右子结点指向当前结点的右子结点的左子节点
newNode.right = this.right.left;
//4.新结点就挂好了,然后来解决当前结点
this.value = this.right.value;
this.left = newNode;
this.right = this.right.right;
}
//右旋转
public void rightRotate() {
Node newNode = new Node(this.value);
newNode.right = this.right;
newNode.left = this.left.right;
this.value = this.left.value;
this.right = newNode;
this.left = this.left.left;
}
}
三、结束语
平衡二叉树这一块刚学习的时候有些难以理解,心里想,咦,视频里面怎么会这样写,这思路谁能记得住啊,还有这为什么要新创建一个结点了,但是随着自己看看书多加思考,然后又自己画了画图,才豁然开朗,选择新创建结点这个方法是在是秒。
学习遇见困难不要怕,就像小学看到初中的题目,初中看到高中的题,高中看到大学的题,哇好难,但一路走过来不就那会事嘛,第一遍不会,那就看第二遍,第二遍不会,那就看第三遍,practice makes perfect。