注:本文同步发布于稀土掘金。
6 平衡二叉树(AVL树)
在进行二叉排序树的构造时,我们可能会因为根结点的选择,构造出不同的二叉树,如下所示,对于同一个数组,可以因根结点的不同,构造出的二叉树完全不一样:
这两种二叉树的查找时间复杂度不同,左侧的时间复杂度为O(logn),右侧则为O(n),显然左侧二叉树的查找效率会更高。
为了提高查找效率,我们提出了平衡二叉树:平衡二叉树,Self-Balancing Binary Search Tree或Height-Balanced Binary Search Tree,是一种特殊的二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
二叉树上结点的左子树深度减去右子树深度的值被称为平衡因子BF(Balance Factor),而平衡二叉树上所有结点的平衡因子只可能是-1、0或1,否则该二叉查找树不能被称为平衡二叉树。
距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树,我们称之为最小不平衡子树。如下图所示,当新插入结点37时,距离它最近的平衡因子绝对值超过1的结点是58,所以从58开始以下的子树为最小不平衡子树,如下图所示:
6.1 平衡二叉树实现原理
平衡二叉树构建的基本思想,就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡树,在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
根据平衡二叉树的特性,插入结点后,若树中出现BF大于1或者小于-1的结点时,需要对该结点进行旋转操作,如以下场景,为更清晰地理解,我们在每个结点下面添加三个框,从左至右分别是左子树深度、BF以及右子树深度:
如何旋转呢?规则是:结点的BF大于1时,围绕左子树往右转;结点的BF小于-1时,围绕右子树往左转;转完后若仍出现BF大于1或者小于-1的结点时,继续转,直至平衡。
我们分别来对以上四种场景进行旋转,首先是以下场景:
结点3的BF大于1,于是围绕其左孩子右转,先将结点3的左子树深度改为0,BF同步调整(左子树深度减去右子树深度),同时将其转为左孩子的右子结点:
然后计算新的父结点,即结点2的右子树深度,公式是Math.max(rightChild.leftDeep, rightChild.rightDeep) + 1,即结点2的右孩子的最高深度加1,此处是Math.max(0,0)+1=1,同步调整结点2的BF:
此时所有结点的BF都在-1~1之间,已旋转完毕。
若在进行右旋时,发现左孩子有右子结点怎么办?如下图:
我们需要将结点4围绕结点2右旋,理论上应该旋转作为结点2的右子结点,但结点2已有右子结点。
这时,我们比较结点4与结点2的右孩子,可以发现,结点4的值一定会大于结点2的右孩子(否则它就应该在结点4的右子树上了),于是我们直接将结点4作为结点2的右孩子的右孩子,同步修改涉及的三个结点的BF、左子树深度和右子树深度,如下图:
这时发现结点2的BF小于-1,所以需要继续左转:
涉及的结点BF都在-1~1之间,旋转结束。
于是可以有以下结论:
(1) 插入后,应检查插入结点、插入结点的父结点以及所有祖宗结点的BF;
(2) 当出现结点的BF小于-1时,以该结点的右孩子为中心,进行左旋转;当出现结点的BF大于1时,以该结点的左孩子为中心,进行右旋转;
(3) 向左旋转时,若右孩子已有左孩子,则被旋转的结点作为右孩子的左孩子的左孩子;向右旋转时,若左孩子已有右孩子,则被旋转的结点作为左孩子的右孩子的右孩子;
(4) 被左旋的结点的右子树深度应重置为0;被右旋的结点的左子树深度应重置为0;
(5) 所有结点的左子树深度,应为其左孩子、右孩子的左子树深度、右子树深度中的最大值加1;
(6) 旋转后重新检查被旋转的结点的父结点以及祖父结点,若再次出现BF不在-1~1之间的结点时,按照旋转规则继续旋转;
下面我们依据以上规则,将以下数组转化成平衡二叉树:
将元素3置入树中,不需要任何处理;将元素2转入树中,按规则它应为结点3的左孩子:
接下来插入元素1,它应该为结点2的子结点:
这时发现,结点3的BF为2,于是将其右旋:
然后插入元素4,所有结点的BF值均正常:
然后插入元素5,插入后结点3的BF为-2,向左旋转:
然后插入元素6,结点2的BF为-2,于是向左旋转:
旋转后,结点3的BF为2,于是向右旋转,使得所有结点的BF在-1和1之间:
继续插入结点7,发现结点5的BF为-2,于是将它向左旋转:
继续插入结点10,所有结点的BF都正常:
插入结点9,发现结点7、6、4的BF都为-2,从下往上开始处理,先将结点7向左旋转:
旋转后结点10的BF变成了2,将其右转,旋转后所有结点的BF都正常:
插入结点8,发现结点6和结点4的BF都是-2,先将结点6左转:
发现结点9的BF为2,于是将其右转:
然后发现结点8的BF为-2,于是将其左转,所有结点的BF都在-1至1之间,插入结束:
结点定义代码实现如下所示:
import lombok.Data;
/**
* Tree Node
*
* @author Korbin
* @date 2023-04-23 11:35:08
**/
@Data
public class TreeNode<T> {
/**
* balance factor
**/
private int balanceFactor;
/**
* value of node
**/
private T data;
/**
* left child
**/
private TreeNode<T> leftChild;
/**
* left deep
**/
private int leftDeep;
/**
* parent node
**/
private TreeNode<T> parent;
/**
* right child
**/
private TreeNode<T> rightChild;
/**
* right deep
**/
private int rightDeep;
/**
* get balance factor, it must be leftDeep - rightDeep
**/
public int getBalanceFactor() {
return leftDeep - rightDeep;
}
@Override
public String toString() {
return "Data is " + data + ", parent is " + ((null != parent) ? parent.getData() : "null") +
", left child is " + ((null != leftChild) ? leftChild.getData() : "null") + ", right child is " +
((null != rightChild) ? rightChild.getData() : "null") + ", left deep is " + leftDeep +
", right deep is " + rightDeep + ", balance factor is " + getBalanceFactor();
}
}
完整的平衡二叉树生成代码如下所示:
/**
* Binary Sort Tree
*
* @author Korbin
* @date 2023-04-23 11:21:57
**/
public class BinarySortTree<T extends Comparable<T>> {
/**
* root node
**/
private TreeNode<T> root;
/**
* 从插入的结点开始向上回溯到root,若碰到结点的BF小于-1则向左旋转,若有结点的BF大于1则向右旋转
*
* @param insertNode 插入的结点
* @author Korbin
* @date 2023-11-15 18:49:54
**/
public void checkAndRotate(TreeNode<T> insertNode) {
System.out.println("check " + insertNode.getData());
TreeNode<T> parent = insertNode.getParent();
while (null != parent) {
if (null != parent.getLeftChild() && parent.getLeftChild().getData().equals(insertNode.getData())) {
parent.setLeftDeep(Math.max(insertNode.getLeftDeep(), insertNode.getRightDeep()) + 1);
} else if (null != parent.getRightChild() &&
parent.getRightChild().getData().equals(insertNode.getData())) {
parent.setRightDeep(Math.max(insertNode.getLeftDeep(), insertNode.getRightDeep()) + 1);
}
if (parent.getBalanceFactor() < -1) {
System.out.println("将结点" + parent.getData() + "向左旋转");
leftRotate(parent);
checkAndRotate(parent);
} else if (parent.getBalanceFactor() > 1) {
System.out.println("将结点" + parent.getData() + "向右旋转");
rightRotate(parent);
checkAndRotate(parent);
} else {
insertNode = parent;
parent = parent.getParent();
}
}
}
/**
* create tree from array
* <p>
* the first element of data is the root node value
*
* @param data array
* @param isSelfBalancingBinarySearchTree is a self balancing binary search tree
* @author Korbin
* @date 2023-04-23 11:38:06
**/
public void createTreeFromArray(T[] data, boolean isSelfBalancingBinarySearchTree) {
// set the first element as the root node
root = new TreeNode<>();
root.setData(data[0]);
root.setLeftDeep(0);
root.setRightDeep(0);
TreeNode<T> node = root;
// iterate data
for (int i = 1; i < data.length; i++) {
TreeNode<T> newNode = new TreeNode<>();
newNode.setData(data[i]);
newNode.setLeftDeep(0);
newNode.setRightDeep(0);
while (null != node) {
if (data[i].compareTo(node.getData()) <= 0) {
// the value of data[i] little than node data or equals to node data
TreeNode<T> leftNode = node.getLeftChild();
if (null == leftNode) {
// if node has no left child
// set data[i] as node's left child
node.setLeftChild(newNode);
newNode.setParent(node);
break;
} else {
// if node has left child
// set node as left child and continue
node = leftNode;
}
} else {
// the value of data[i] greater than node data
TreeNode<T> rightNode = node.getRightChild();
if (null == rightNode) {
// if node has no right child
// set data[i] as node's right child
node.setRightChild(newNode);
newNode.setParent(node);
break;
} else {
// if node has right child
// set node as right child and continue
node = rightNode;
}
}
}
if (isSelfBalancingBinarySearchTree) {
// 插入后从插入的节点位置开始往上回溯,直到Root结点,判断是否存在不平衡结点
checkAndRotate(newNode);
}
node = root;
}
}
/**
* find node who's value equals key
*
* @param key key to find
* @return searched node or null
* @author Korbin
* @date 2023-04-23 14:36:49
**/
public TreeNode<T> findNode(T key) {
return findNode(root, key);
}
/**
* find node who's value equals key in tree
*
* @param tree root node of tree
* @param key key to find
* @return searched node or null
* @author Korbin
* @date 2023-04-23 14:35:31
**/
private TreeNode<T> findNode(TreeNode<T> tree, T key) {
if (null == tree) {
return null;
}
if (key.equals(tree.getData())) {
// found node
return tree;
}
if (key.compareTo(tree.getData()) < 0) {
// find from left tree
return findNode(tree.getLeftChild(), key);
} else {
// find from right tree
return findNode(tree.getRightChild(), key);
}
}
/**
* 向左旋转
*
* @param node 要被旋转的结点
* @author Korbin
* @date 2023-11-15 18:49:34
**/
public void leftRotate(TreeNode<T> node) {
// 要左转的是bf小于-1的节点
// 需要将其转到其右子节点下方,作为右子节点的左子节点
// 存在两种情况:1 其右子节点无左子节点;2 其右子节点有左子节点;
// 取出父节点
TreeNode<T> parent = node.getParent();
// 取出其右子节点
TreeNode<T> rightChild = node.getRightChild();
// 取出右子节点的左子节点
TreeNode<T> rightChildLeftChild = rightChild.getLeftChild();
// 如果右子节点没有左子节点,则直接转
if (null == rightChildLeftChild) {
// 当前节点的rightDeep重置为0
node.setRightDeep(0);
node.setRightChild(null);
// 当前节点的父改为其右子节点
node.setParent(rightChild);
// 当前节点的右子节点作为新的父亲,先设置leftChild
rightChild.setLeftChild(node);
// 然后因为左边增加了节点,所以leftDeep要变化
// 新的leftDeep应为新的左子节点,即node的最高深度,再加上左子节点这一深度
rightChild.setLeftDeep(Math.max(node.getLeftDeep(), node.getRightDeep()) + 1);
if (null != parent) {
// rightChild的父节点发生变化
rightChild.setParent(parent);
// 先判断节点是左节点还是右节点
if (null != parent.getLeftChild() && parent.getLeftChild().getData().equals(node.getData())) {
// 左节点
parent.setLeftChild(rightChild);
// 父亲的leftDeep,为其子的leftDeep和rightDeep中最大的值,加上子节点本身这个深度为1的节点
parent.setLeftDeep(Math.max(rightChild.getLeftDeep(), rightChild.getRightDeep()) + 1);
} else {
// 右节点
parent.setRightChild(rightChild);
parent.setRightDeep(Math.max(rightChild.getLeftDeep(), rightChild.getRightDeep()) + 1);
}
} else {
rightChild.setParent(null);
root = rightChild;
}
} else {
// 如果右子节点有左子节点,则该左子节点肯定大于当前节点
// 设置当前节点的rightDeep为0
node.setRightDeep(0);
node.setRightChild(null);
// 把当前节点转为“右孩子的左孩子“的子节点
rightChildLeftChild.setLeftChild(node);
node.setParent(rightChildLeftChild);
// 设置“右孩子的左孩子“的rightDeep
rightChildLeftChild.setLeftDeep(Math.max(node.getLeftDeep(), node.getRightDeep()) + 1);
// 设置右孩子的leftDeep
rightChild.setLeftDeep(Math.max(rightChildLeftChild.getLeftDeep(), rightChildLeftChild.getRightDeep()) + 1);
// 设置parent
if (null != parent) {
rightChild.setParent(parent);
if (null != parent.getLeftChild() && parent.getLeftChild().getData().equals(node.getData())) {
// 原来为左孩子
parent.setLeftChild(rightChild);
parent.setLeftDeep(Math.max(rightChild.getLeftDeep(), rightChild.getRightDeep()) + 1);
} else {
// 原来为右孩子
parent.setRightChild(rightChild);
parent.setRightDeep(Math.max(rightChild.getLeftDeep(), rightChild.getRightDeep()) + 1);
}
} else {
rightChild.setParent(null);
root = rightChild;
}
}
}
/**
* 向右旋转
*
* @param node 要被旋转的结点
* @author Korbin
* @date 2023-11-15 18:49:14
**/
public void rightRotate(TreeNode<T> node) {
// 要右转的是bf大于1的节点
// 需要将其转到其左子节点下方,作为左子节点的右子节点
// 存在两种情况:1 其左子节点无右子节点;2 其左子节点有右子节点;
// 取出父节点
TreeNode<T> parent = node.getParent();
// 取出其左子节点
TreeNode<T> leftChild = node.getLeftChild();
// 取出左子节点的右子节点
TreeNode<T> leftChildRightChild = leftChild.getRightChild();
// 如果左子节点没有右子节点,则直接转
if (null == leftChildRightChild) {
// 当前节点的leftDeep重置为0
node.setLeftDeep(0);
node.setLeftChild(null);
// 当前节点的父改为其左子节点
node.setParent(leftChild);
// 当前节点的左子节点作为新的父亲,先设置rightChild
leftChild.setRightChild(node);
// 然后因为右边增加了节点,所以rightDeep要变化
// 新的rightDeep应为新的右子节点,即node的最高深度,再加上右子节点这一深度
leftChild.setRightDeep(Math.max(node.getLeftDeep(), node.getRightDeep()) + 1);
if (null != parent) {
// leftChild的父节点发生变化
leftChild.setParent(parent);
// 先判断节点是左节点还是右节点
if (null != parent.getLeftChild() && parent.getLeftChild().getData().equals(node.getData())) {
// 左节点
parent.setLeftChild(leftChild);
// 父亲的leftDeep,为其子的leftDeep和rightDeep中最大的值,加上子节点本身这个深度为1的节点
parent.setLeftDeep(Math.max(leftChild.getLeftDeep(), leftChild.getRightDeep()) + 1);
} else {
// 右节点
parent.setRightChild(leftChild);
parent.setRightDeep(Math.max(leftChild.getLeftDeep(), leftChild.getRightDeep()) + 1);
}
} else {
leftChild.setParent(null);
root = leftChild;
}
} else {
// 如果左子节点有右子节点,则该右子节点肯定小于当前节点
// 设置当前节点的leftDeep为0
node.setLeftDeep(0);
node.setLeftChild(null);
// 把当前节点转为“左孩子的右孩子“的子节点
leftChildRightChild.setRightChild(node);
node.setParent(leftChildRightChild);
// 设置“左孩子的右孩子“的rightDeep
leftChildRightChild.setRightDeep(Math.max(node.getLeftDeep(), node.getRightDeep()) + 1);
// 设置左孩子的rightDeep
leftChild.setRightDeep(Math.max(leftChildRightChild.getLeftDeep(), leftChildRightChild.getRightDeep()) + 1);
// 设置parent
if (null != parent) {
leftChild.setParent(parent);
if (null != parent.getLeftChild() && parent.getLeftChild().getData().equals(node.getData())) {
// 原来为左孩子
parent.setLeftChild(leftChild);
parent.setLeftDeep(Math.max(leftChild.getLeftDeep(), leftChild.getRightDeep()) + 1);
} else {
// 原来为右孩子
parent.setRightChild(leftChild);
parent.setRightDeep(Math.max(leftChild.getLeftDeep(), leftChild.getRightDeep()) + 1);
}
} else {
leftChild.setParent(null);
root = leftChild;
}
}
}
}