文章目录
1、问题描述
- 二叉排序树是为了提高序列的插入与查询操作的效率
- 而在特殊情况下,其效率非但不能提高,反而可能会降低
- 为了解决这种情况,需要引入平衡二叉树来保证插入查询
2、基本介绍
3、从BST到AVL的转化
1、概念定义
首先需要明确一个概念:当某结点的左子树高度与右子树高度的绝对值相差不超过1时,就称该二叉树为平衡二叉树
其中:
-
当该结点
(右子树高度-左子树高度)>1
时,就规定该结点需要左旋转 -
当该结点
(左子树高度-右子树高度) >1
时,就规定该结点需要右旋转
2、分析
- 在正式进行旋转之前,还需要对左右子树的高度进行获取
// 返回该结点的左子结点的高度
public int leftHeight() {
if (this.left == null) {
return 0;
}
return this.left.height();
}
// 返回该结点的右子结点的高度
public int rightHeight() {
if (this.right == null) {
return 0;
}
return this.right.height();
}
- 二叉树的高度
// 返回以该结点为根结点的树的高度
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
- 由于平衡二叉树是对二叉排序树的优化,因此二叉排序树中的增、删、查等方法依旧适用于平衡二叉树
/**
* @author zhihua.li
* @date 2021/3/2 - 8:11
**/
/**
* @program: DataStructure
* @description: AVL树结点结构
* @author: zhihua li
* @create: 2021-03-02 08:11
*/
public class Node {
private int value;
private Node left;
private Node right;
public Node(int value) {
this.value = value;
}
// 添加结点,递归添加结点并满足二叉排序树的要求
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的结点的值和当前子树的根结点的值的关系
if (node.value < value) {
if (this.left == null) {
// 若当前结点的左子树为空,则将node结点作为当前结点的左子结点
this.left = node;
} else {
this.left.add(node); //递归地向左子树添加
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
// 中序遍历二叉树
public void midList() {
if (this.left != null) {
this.left.midList();
}
System.out.println(this);
if (this.right != null) {
this.right.midList();
}
}
/**
* @param value 希望删除的结点的值
* @Method search
* @Author zhihua.Li
* @Version 1.0
* @Description 删除结点时,先要查找该结点是否存在于二叉树中
* @Return Node 若找到则返回该结点,否则返回null
* @Exception
* @Date 2021/3/1 9:06
*/
public Node search(int value) {
if (this.value == value) {
return this;
} else if (this.value > value && this.left != null) {
return this.left.search(value);
} else if (this.value < value && this.right != null) {
return this.right.search(value);
} else {
return null;
}
}
/**
* @param value
* @Method searchParent
* @Author zhihua.Li
* @Version 1.0
* @Description 删除结点时,也需要查找要删除结点的父结点
* @Return Node
* @Exception
* @Date 2021/3/1 9:09
*/
public Node searchParent(int value) {
// 若当前结点就是要删除结点点的父结点就直接返回,否则就继续递归寻找该父结点
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else {
// 若当前结点的value小于目标value且左子结点不为空,则向左子树递归查找
if (this.left != null && this.value > value) {
return this.left.searchParent(value);
} else if (this.right != null && this.value <= value) {
return this.right.searchParent(value);
} else {
// 没有找到父结点
return null;
}
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
3、代码实现
1、左旋转
由于右子树高度与左子树高度相差大于1,因此需要对其进行左旋转
动画演示:
基本思路:
代码实现:
// 左旋转
private void leftRotate() {
// 以当前结点的值创建新结点
Node node = new Node(value);
// 当前结点的左子树设为新结点的左子树
node.left = left;
// 当前结点的右子树的左子结点设置为新结点的右子树
node.right = right.left;
// 新结点的值修改为当前结点的右子结点的值
value = right.value;
// 当前结点的右子树设置为右子树的右子树
right = right.right;
// 当前结点的左子结点设置为新的结点
left = node;
}
将左旋转添加到插入结点的方法中,使结点在插入二叉排序树时自动进行平衡
注意:这里并没有完全写完代码,还需要分析一种特殊情况下的自动平衡问题,见下文
// 添加结点,递归添加结点并满足二叉排序树的要求
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的结点的值和当前子树的根结点的值的关系
if (node.value < value) {
if (this.left == null) {
// 若当前结点的左子树为空,则将node结点作为当前结点的左子结点
this.left = node;
} else {
this.left.add(node); //递归地向左子树添加
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
if (rightHeight() - leftHeight() > 1) {
leftRotate();
}
代码测试:
@Test
public void test() {
// 1、需要进行左旋转的情况(右子树高度大于左子树高度)
int[] arr = {4, 3, 6, 5, 7, 8};
AVLTree avl = new AVLTree();
for (int i = 0; i < arr.length; i++) {
avl.addNode(new Node(arr[i]));
}
System.out.println("中序遍历:");
avl.midList();
System.out.println("当前二叉树高度为:"+avl.root.height());
System.out.println("左子树高度:"+avl.root.leftHeight());
System.out.println("右子树高度:"+avl.root.rightHeight());
}
测试结果为:
2、右旋转
由于左子树高度与右子树高度相差大于1,因此需要对其进行右旋转
动图演示:
基本思路:
代码实现(具体分析过程见上图):
// 右旋转
private void rightRotate() {
Node node = new Node(value);
node.right = right;
node.left = left.right;
value = left.value;
left = left.left;
right = node;
}
和上面的左旋转一样,同样需要在插入节点的方法中对二叉排序树进行右旋转来平衡
// 添加结点,递归添加结点并满足二叉排序树的要求
public void add(Node node) {
if (node == null) {
return;
}
// 判断传入的结点的值和当前子树的根结点的值的关系
if (node.value < value) {
if (this.left == null) {
// 若当前结点的左子树为空,则将node结点作为当前结点的左子结点
this.left = node;
} else {
this.left.add(node); //递归地向左子树添加
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
if (rightHeight() - leftHeight() > 1) {
leftRotate();
} else if (leftHeight() - rightHeight() > 1) {
rightRotate();
}
}
代码测试:
// 2、需要进行右旋转的情况(左子树高度大于右子树高度)
int[] arr = {10,12,8,9,7,6};
AVLTree avl = new AVLTree();
for (int i = 0; i < arr.length; i++) {
avl.addNode(new Node(arr[i]));
}
System.out.println("中序遍历:");
avl.midList();
System.out.println("当前二叉树高度为:"+avl.root.height());
System.out.println("左子树高度:"+avl.root.leftHeight());
System.out.println("右子树高度:"+avl.root.rightHeight());
测试结果为:
3、双旋转
根据上面的两张图片可以看出,在平衡二叉排序树时,可能需要旋转一次以上,因此,对这种情况进行分析讨论
对于需要旋转多次的情况,其问题出在了根节点的左右子树中仍需要进行旋转以达到平衡
代码测试:
// 在插入结点的方法最后添加如下代码:
if (rightHeight() - leftHeight() > 1) {
// 待旋转结点还可能出现对根结点进行旋转后依旧不是平衡二叉树因此需要再次对其左右子树进行判断
if (right != null && right.leftHeight() > right.rightHeight()) {
// 若当前结点的右子树的左子树的高度大于当前结点的右子树的右子树的高度,就先对当前结点的右子树进行右旋转,对其右子树递归
right.rightRotate();
}
// 之后再对该结点进行左旋转
leftRotate();
} else if (leftHeight() - rightHeight() > 1) {
if (left != null && left.rightHeight() > left.leftHeight()) {
// 若当前结点的左子树的右子树的高度大于当前结点的左子树的左子树的高度,就先对当前结点的左子树进行左旋转,对其左子树递归
left.leftRotate();
}
// 之后再对该结点进行右旋转
rightRotate();
}
}
测试结果为: