一、相关概念
1.1 问题引出
-
需求:给你一个数列{1,2,3,4,5,6},要求创建一棵二叉排序树(
BST
)。 -
存在问题分析:
- 左子树全部为空,从形式上看,更像一个单链表。
- 插入速度没有影响。
- 查询速度明显降低(因为需要依次比较), 不能发挥二叉排序树的优势,因为每次还需要比较左子树,其查询速度比单链表还慢。
-
示意图:
- 解决方案:使用平衡二叉树(
AVL
)。
1.2 基本介绍
-
概念:平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为
AVL
树, 可以保证查询效率较高。 -
特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
-
平衡二叉树的常用实现方法有红黑树、
AVL
、替罪羊树、Treap
、伸展树等。 -
示意图:
二、基本应用
2.1 左旋转
-
需求:给你一个元素为{4,3,6,5,7,8}的数列,创建出对应的平衡二叉树。
-
场景:假设我们有一棵二叉排序树(如下图),当你的右子树的高度比左子树高度要高的时候,我们需要通过左旋转的方式来降低右子树的高度,从而构建出平衡二叉树。
-
示意图:
- 代码示例:
/**
* 左旋转。
*/
private void leftRotate() {
// 1.以当前根节点的值,创建新的节点。
Node newNode = new Node(this.getId());
// 2.把新节点的左子树设置为当前节点的左子树。
newNode.setLeft(this.getLeft());
// 3.把新节点的右子树设置为当前节点的右子树的左子树。
Node r = this.getRight();
newNode.setRight(r.getLeft());
// 4.把当前节点的值替换成右子节点的值。
this.setId(r.getId());
// 5.把当前节点的右子树设置成当前节点右子树的右子树。
this.setRight(r.getRight());
// 6.把当前节点的左子树设置成新的节点。
this.setLeft(newNode);
}
2.2 右旋转
-
需求:给你一个元素为{10,12,8,9,7,6}的数列,创建出对应的平衡二叉树。
-
与左旋转类似的,我们需要降低左子树高度时,就要进行右旋转操作。
-
示意图:
- 代码示例:
/**
* 右旋转。
* 操作方式与左旋转相反。
*/
private void rightRotate() {
Node newNode = new Node(this.getId());
newNode.setRight(this.getRight());
Node l = this.getLeft();
newNode.setLeft(l.getRight());
this.setId(l.getId());
this.setLeft(l.getLeft());
this.setRight(newNode);
}
2.3 双旋转
- 前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。
- 单旋转问题分析-示意图:
- 双旋转-示意图:
- 代码示例:
/**
* 双旋转。
*/
private void doubleRotate() {
// 当添加完一个节点后,如果: (右子树的高度-左子树的高度) > 1 , 进行左旋转。
if ((this.getRightHeight() - this.getLeftHeight()) > 1) {
// 如果它的右子树的左子树的高度,大于它的右子树的右子树的高度。
Node r = this.getRight();
if (null != r && r.getLeftHeight() > r.getRightHeight()) {
// 先对右子节点进行右旋转。
r.rightRotate();
// 然后再对当前节点进行左旋转。
this.leftRotate();
} else {
// 否则,直接进行左旋转即可。
this.leftRotate();
}
// 操作完成,退出。
return;
}
// 与上一个操作相反。
if ((this.getLeftHeight() - this.getRightHeight()) > 1) {
Node l = this.getLeft();
if (null != l && l.getRightHeight() > l.getLeftHeight()) {
l.leftRotate();
this.rightRotate();
} else {
this.rightRotate();
}
}
}
三、完整代码
public class AVLTreeDemo {
public static void main(String[] args) {
// 数列。
int[] arr = {10, 11, 7, 6, 8, 9};
// 测试一:不使用双旋转添加。
AVLTree noUseDoubleRotateTree = new AVLTree();
for (int i : arr) {
noUseDoubleRotateTree.add(new Node(i), false);
}
printTreeInfo(noUseDoubleRotateTree);
System.out.println();
// 中序遍历,结果如下:
// 6、7、8、9、10、11
// 树的总高度=[4],其中左子树高度=[3],右子树高度=[1],当前根节点=Node:{id:10}。
System.out.println("-----------------------------");
// 测试二:使用双旋转添加。
AVLTree useDoubleRotateTree = new AVLTree();
for (int i : arr) {
// 添加时,开启双旋转操作。
useDoubleRotateTree.add(new Node(i), true);
}
printTreeInfo(useDoubleRotateTree);
System.out.println();
// 中序遍历,结果如下:
// 6、7、8、9、10、11
// 树的总高度=[3],其中左子树高度=[2],右子树高度=[2],当前根节点=Node:{id:8}。
}
/**
* 遍历打印节点及树的高度信息。
*
* @param avlTree 平衡二叉树
*/
public static void printTreeInfo(AVLTree avlTree) {
System.out.println("中序遍历,结果如下:");
avlTree.infixOrder();
Node root = avlTree.getRoot();
System.out.printf(
"树的总高度=[%d],其中左子树高度=[%d],右子树高度=[%d],当前根节点=%s。",
root.getHeight(),
root.getLeftHeight(),
root.getRightHeight(),
root
);
}
}
/**
* 平衡二叉树。
*/
class AVLTree {
private Node root;
public Node getRoot() {
return root;
}
public void setRoot(Node root) {
this.root = root;
}
public void add(Node node, boolean useDoubleRotate) {
if (null == this.getRoot()) {
this.setRoot(node);
} else {
this.getRoot().add(node, useDoubleRotate);
}
}
public void infixOrder() {
if (null == this.getRoot()) {
System.out.println("binary tree is null.");
} else {
this.getRoot().infixOrder();
}
}
}
/**
* 节点。
*/
class Node {
private int id;
private Node left;
private Node right;
public Node(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
@Override
public String toString() {
return "Node:{id:" + id + "}";
}
/**
* 双旋转。
*
* @param use 决定是否使用双旋转(该参数仅仅是为了对比效果)。
*/
private void doubleRotate(boolean use) {
if (use) {
// 当添加完一个节点后,如果: (右子树的高度-左子树的高度) > 1 , 进行左旋转。
if ((this.getRightHeight() - this.getLeftHeight()) > 1) {
// 如果它的右子树的左子树的高度,大于它的右子树的右子树的高度。
Node r = this.getRight();
if (null != r && r.getLeftHeight() > r.getRightHeight()) {
// 先对右子节点进行右旋转。
r.rightRotate();
// 然后再对当前节点进行左旋转。
this.leftRotate();
} else {
// 否则,直接进行左旋转即可。
this.leftRotate();
}
// 操作完成,退出。
return;
}
// 与上一个操作相反。
if ((this.getLeftHeight() - this.getRightHeight()) > 1) {
Node l = this.getLeft();
if (null != l && l.getRightHeight() > l.getLeftHeight()) {
l.leftRotate();
this.rightRotate();
} else {
this.rightRotate();
}
}
}
}
/**
* 左旋转。
*/
private void leftRotate() {
// 1.以当前根节点的值,创建新的节点。
Node newNode = new Node(this.getId());
// 2.把新节点的左子树设置为当前节点的左子树。
newNode.setLeft(this.getLeft());
// 3.把新节点的右子树设置为当前节点的右子树的左子树。
Node r = this.getRight();
newNode.setRight(r.getLeft());
// 4.把当前节点的值替换成右子节点的值。
this.setId(r.getId());
// 5.把当前节点的右子树设置成当前节点右子树的右子树。
this.setRight(r.getRight());
// 6.把当前节点的左子树设置成新的节点。
this.setLeft(newNode);
}
/**
* 右旋转。
* 操作方式与左旋转相反。
*/
private void rightRotate() {
Node newNode = new Node(this.getId());
newNode.setRight(this.getRight());
Node l = this.getLeft();
newNode.setLeft(l.getRight());
this.setId(l.getId());
this.setLeft(l.getLeft());
this.setRight(newNode);
}
/**
* 得到左子树高度。
*
* @return int
*/
public int getLeftHeight() {
if (null == this.getLeft()) {
return 0;
} else {
return this.getLeft().getHeight();
}
}
/**
* 得到右子树高度。
*
* @return int
*/
public int getRightHeight() {
if (null == this.getRight()) {
return 0;
} else {
return this.getRight().getHeight();
}
}
/**
* 返回以该节点为根的树的高度。
*
* @return int
*/
public int getHeight() {
int leftMaxHeight;
int rightMaxHeight;
if (null == this.getLeft()) {
leftMaxHeight = 0;
} else {
// 左子树递归获取高度。
leftMaxHeight = this.getLeft().getHeight();
}
if (null == this.getRight()) {
rightMaxHeight = 0;
} else {
// 右子树递归获取高度。
rightMaxHeight = this.getRight().getHeight();
}
// 比较最大高度。
return Math.max(leftMaxHeight, rightMaxHeight) + 1;
}
/**
* 添加节点。
*
* @param node 节点
* @param useDoubleRotate 是否使用双旋转
*/
public void add(Node node, boolean useDoubleRotate) {
if (null == node) {
return;
}
// 判断传入的节点的值,和当前子树的根节点的值关系。
if (this.getId() > node.getId()) {
if (null == this.getLeft()) {
this.setLeft(node);
} else {
// 左子树递归添加。
this.getLeft().add(node, useDoubleRotate);
}
} else {
if (null == this.getRight()) {
this.setRight(node);
} else {
// 右子树递归添加。
this.getRight().add(node, useDoubleRotate);
}
}
// 双旋转。
this.doubleRotate(useDoubleRotate);
}
/**
* 中序遍历。
*/
public void infixOrder() {
if (null != this.getLeft()) {
this.getLeft().infixOrder();
}
System.out.println(this);
if (null != this.getRight()) {
this.getRight().infixOrder();
}
}
}
四、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。