11_树结构实际应用(3)
平衡二叉树(AVL 树)
1、二叉排序树的问题
- 看一个案例(说明二叉排序树可能的问题),给你一个数列{ 1,2,3,4,5,6 } ,要求创建一颗二叉排序树(BST),并分析问题所在:
- 左子树全部为空,从形式上看,更像一个单链表;
- 插入速度没有影响;
- 查询速度明显降低(因为需要依次比较),不能发挥BST 的优势,因为每次还需要比较左子,其查询速度比单链表还慢。
- 解决方案-平衡二叉树(AVL)。
2、平衡二叉树基本介绍
-
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
-
平衡二叉树具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
-
平衡二叉树的常用实现方法有红黑树、AVL(这个AVL指的是算法)、替罪羊树、Treap、伸展树等。
-
注意:平衡二叉树一定是二叉排序树!!!
-
举例说明,看看下面哪些是AVL树, 为什么?
3、平衡二叉树思路分析
① 计算子树高度
left == null ? 0 : left.height()
是求左子树的高度;right == null ? 0 : right.height()
是求右子树的高度;- 上述两个表达式取最大值,即为当前子树的高度。
/**
* 获取当前节点作为根节点的树的高度
* @return 返回当前节点作为根节点的树的高度
*/
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
- 但是上述方法有明显弊端,就是寻找高度的时间很长,因此采用以下方法:
/**
* 获取当前节点作为根节点的树的高度
* @return 返回当前节点作为根节点的树的高度
*/
public int height() {
int maxHeights = left == null ? 0 : left.height;
this.height = (right == null ? maxHeights : Math.max(maxHeights, right.height)) + 1;
return this.height;
}
- 每一个节点都记录自己的高度。
- 上述做法并不是最优,最优的是使用平衡因子,但是因为我刚开始学就是用高度的,而且现在暂时没时间了,因此暂时不改。
② 左旋转(RR型)
-
以下图片来源网址:https://blog.csdn.net/qq_24336773/article/details/81712866
-
由于在A的右孩子®的右子树®上插入新节点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根节点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕B节点逆时针旋转一样。
- RR型调整的一般形式如下图所示,表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:
- 将A的右孩子B提升为新的根节点;
- 将原来的根结点A降为B的左孩子;
- 各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。
③ 右旋转(LL型)
- 由于在A的左孩子(L)的左子树(L)上插入新节点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下图是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根节点,其余两个节点分别作为左右孩子节点才能平衡,A节点就好像是绕B节点顺时针旋转一样。
- LL型调整的一般形式如下图所示,表示在A的左孩子B的左子树BL(不一定为空)中插入节点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:
- 将A的左孩子B提升为新的根节点;
- 将原来的根节点A降为B的右孩子;
- 各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。
④ 左右旋转(LR型)
- 由于在A的左孩子(L)的右子树®上插入新节点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。下图是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
- LR型调整的一般形式如下图所示,表示在A的左孩子B的右子树(根节点为C,不一定为空(我的理解:空的意思是插入了节点到C位置,A的右子树为空))中插入节点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:
- 将B的右孩子C提升为新的根节点;
- 将原来的根节点A降为C的右孩子;
- 各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。
⑤ 右左旋转(RL型)
- 由于在A的右孩子®的左子树(L)上插入新节点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。下图是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
- RL型调整的一般形式如下图所示,表示在A的右孩子B的左子树(根节点为C,不一定为空)中插入节点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:
- 将B的右孩子C提升为新的根节点;
- 将原来的根节点A降为C的左孩子;
- 各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。
⑥ 旋转总结
-
总结来源:https://blog.csdn.net/qq_25806863/article/details/74755131
-
总结(设a为当前子树根节点):
插入方式 | 描述 | 旋转方式 |
---|---|---|
LL | 在a的左子树根节点的左子树上插入节点而破坏平衡 | 右旋转 |
RR | 在a的右子树根节点的右子树上插入节点而破坏平衡 | 左旋转 |
LR | 在a的左子树根节点的右子树上插入节点而破坏平衡 | 先左旋后右旋 |
RL | 在a的右子树根节点的左子树上插入节点而破坏平衡 | 先右旋后左旋 |
⑥ 插入
- AVL树插入一个新的元素后,可能会出现AVL树的某一棵子树不平衡,因此需要调整;
- 从插入的位置往上回溯,回溯过程中计算回溯路径中出现的所有节点的平衡因子,当平衡因子>1就进行调整;
- 要注意的是,调整的时候,被调整的那棵子树的父节点必须在场,不然即使调整完,父节点的指针还是没变;
- 不平衡的情况参照上述情况;
- 可以通过递归来隐式调用栈保存父节点,返回的时候对父节点的子树进行调整,这是我的做法;
- 也可以直接循环来遍历树,用显示栈保存父节点的方式,在弹栈的时候对弹出栈的节点的子树进行调整。(我没这样做,只是觉得能这样做,但是代码的可读性或许不高,因此没这样做)
⑦ 删除
-
最麻烦的操作,在二叉排序树删除的基础上,需要递归(不一定要递归,用栈来保存之前的数据也行,递归方便)去寻找,找到待删除元素后,进行删除操作:
- 如果只是叶子或者只有一棵子树的节点,那么删除后,回溯过程中对被删除节点的父节点进行调整;
- 如果被删除的节点左右都有子树,我的做法是从被删除节点的左边删除并获取最大节点替换被删除节点,然后往上回溯调整父节点;
- 需要注意的是,当从被删除节点的左边删除最大节点的时候,回溯过程中也需要调整。
-
被删除的节点是根则需要额外判断。
-
如果找不到被删除的节点,则返回null,同时回溯过程中不需要调整任何节点。
⑧ 查找
- 与二叉排序树一样,直接二分查找即可。查找值小就往左找,大就往右找。
- 假设有1亿个数据(假设,我的程序不调整jvm堆的话,最多只能弄1w个数据多些,而且建树很慢(毕竟牺牲了插入和删除来为查找提供方便)),avl树的最大高度(根节点高度为1,它的子节点高度为2,以此类推)为log2(1000000)+1=27层,即使查找到叶子节点的数据,也只是比较了27次;对于普通的二叉排序树,极端情况下甚至出现1亿层的树,极端情况下要比较1亿次。
⑨ 更新
- 我并没有实现更新,但是觉得如果真要更新的话,那么我觉得可以先将要更新的节点删除并得到,然后更新值后,再插入。
4、代码实现
① 节点及计算子树高度
- 节点:
/**
* 静态内部类: AVL树节点
*/
static class Node {
/**
* 节点值
*/
private final Integer value;
/**
* 左节点
*/
private Node left;
/**
* 右节点
*/
private Node right;
/**
* 树的高度
*/
private int height;
public Node(Integer value) {
this.value = value;
}
/**
* 获取当前节点作为根节点的树的高度
* @return 返回当前节点作为根节点的树的高度
*/
public int height() {
int maxHeights = left == null ? 0 : left.height;
this.height = (right == null ? maxHeights : Math.max(maxHeights, right.height)) + 1;
return this.height;
}
public Integer getValue() {
return value;
}
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
- 获取节点高度:
/**
* 获取当前节点为根的树的高度
* @param node 当前节点
* @return 返回以当前节点为根的树的高度
*/
public int getNodeHeight(Node node) {
return node == null ? 0 : node.height();
}
② 左旋转
- 代码实现:
/**
* 左旋转(RR型)
* @param oldRoot 子树旧根节点
* @return 返回新根节点
*/
private Node leftRotate(Node oldRoot) {
Node newRoot = oldRoot.right;
oldRoot.right = newRoot.left;
newRoot.left = oldRoot;
// 当旋转结束后, 只有旧和新节点高度改变
oldRoot.height = getNodeHeight(oldRoot);
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
- 旋转完后,当前子树的根变更了,于是需要返回当前子树的新根,以下旋转都是要返回新根。
- 同时,旋转结束后,需要更新高度,在旋转结束后,高度变更的就只有
oldRoot
和newRoot
,因为此时oldRoot
获得了newRoot
的左子树,newRoot
则获得了oldRoot
,要先变更oldRoot
高度才可以变更newRoot
高度。
③ 右旋转
- 代码实现:
/**
* 右旋转(LL型)
* @param oldRoot 子树旧根节点
* @return 返回新根节点
*/
private Node rightRotate(Node oldRoot) {
Node newRoot = oldRoot.left;
oldRoot.left = newRoot.right;
newRoot.right = oldRoot;
oldRoot.height = getNodeHeight(oldRoot);
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
- 左右旋和右左旋都是使用上述的代码组合而成。
④ 调整平衡
- 代码实现:
/**
* 调整以curRoot为根节点的树的平衡, 同时无论是否调整平衡结束, curRoot都要更新高度
* @param curRoot 当前子树的根节点
* @return 返回当前子树的新的根节点
*/
private Node adjustBalance(Node curRoot) {
if (curRoot == null) {
return null;
}
// 调整平衡
Node newRoot = curRoot;
if (getNodeHeight(curRoot.right) - getNodeHeight(curRoot.left) > BALANCE_FACTORY) {
// 右子树高度高, 判断是什么型(这里不需要担心空指针, 最少右边高度为2, 否则不会进来)
if (getNodeHeight(curRoot.right.right) < getNodeHeight(curRoot.right.left)) {
// 这里是curRoot的右子树的左子树高度大于curRoot右子树的右子树(外层if判断curNode到它的子节点是L/R型,
// 这里的内层if判断curNode是在右边节点的哪个子节点被插入了新节点导致失衡)
// RL型(先右旋右子树(这里即使cur的右子树就两个节点也没关系, 只是出现对指针赋值null而已), 再左旋curRoot)
curRoot.right = rightRotate(curRoot.right);
}
// RR型(RL型转为了RR型)
newRoot = leftRotate(curRoot);
} else if (getNodeHeight(curRoot.left) - getNodeHeight(curRoot.right) > BALANCE_FACTORY) {
// 左子树高度高, 其它解释参考上述解释
if (getNodeHeight(curRoot.left.left) < getNodeHeight(curRoot.left.right)) {
// LR型
curRoot.left = leftRotate(curRoot.left);
}
// LL型(LR型转为了LL型)
newRoot = rightRotate(curRoot);
}
// 更新该节点的高度(如果当前子树根节点没调整过, 但是它子树底下调整过, 那么回溯的时候, 会往上更新高度, 这里就是回溯时候无论是否
// adjust都要更新高度)
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
- 每一次回溯过程都一定要调整,不止有调整平衡的原因,也有更新高度的原因,举个例子:
- 假设插入一个7,然后此时根节点值为7,高度为1;再插入一个6,此时6节点是根节点左边,插入回溯过程中,调整根节点,但是因为没有失去平衡,因此不会旋转树;然后更新高度,更新高度的时候发现左节点不为null,但右节点为null,那就取左节点的高度+1作为新的高度;
- 插入代码在下面。
⑤ 插入
- 代码实现:
/**
* 返回树的根节点
* @return 返回树的根节点
*/
public Node getRoot() {
return this.root;
}
/**
* 插入一个值到AVL树中
* @param value 要插入的值
*/
public void add(int value) {
if (this.root == null) {
this.root = new Node(value);
this.root.height = 1;
} else {
// 如果整棵子树的是根节点失衡, 那么调整完后会返回新的根节点给root
this.root = add(value, this.root);
}
}
/**
* 插入一个节点
* @param value 要插入的值
* @param curRoot 当前子树的根节点
* @return 返回当前子树调整过后的新的根节点
*/
private Node add(int value, Node curRoot) {
if (curRoot == null) {
Node node = new Node(value);
node.height = 1;
return node;
}
if (value < curRoot.value) {
// 子树可能发生调整, 因此要获取新的调整后的值
curRoot.left = add(value, curRoot.left);
} else {
curRoot.right = add(value, curRoot.right);
}
// 当前节点需要调整
return adjustBalance(curRoot);
}
⑥ 删除
- 代码实现:
/**
* 根据value删除节点
* @param value 根据该value删除节点
* @return 如果删除成功返回删除节点; 否则返回null
*/
public Node deleteNode(int value) {
if (this.root == null) {
return null;
}
return deleteNode(value, this.root, null);
}
/**
* 查找要删除的节点并删除, 然后调整树的操作
* @param value 根据该值找到待删除的节点
* @param curNode 判断当前节点是否是待删除结点
* @param parentNode 当前节点的父节点
* @return 返回被删除的节点
*/
private Node deleteNode(int value, Node curNode, Node parentNode) {
if (curNode == null) {
return null;
}
// 这个是记录parentNode在递归删除结束回来的时候要调整哪个方向, 提前记录是因为删除后parentNode的子节点可能会变
// 这样后面就不知道要调整哪边
boolean isLeft = parentNode != null && parentNode.left == curNode;
Node deleteNode;
if (value < curNode.value) {
deleteNode = deleteNode(value, curNode.left, curNode);
} else if (value > curNode.value) {
deleteNode = deleteNode(value, curNode.right, curNode);
} else {
// 开始删除
deleteNode = deleteNode(curNode, parentNode);
}
if (deleteNode == null) {
// 删除失败(不需要调整, 因为树没有变)
return null;
}
// 调整
if (parentNode == null) {
// 调整根节点(这一步是删除元素后最后一次调整树)
this.root = adjustBalance(this.root);
} else {
if (isLeft) {
// 如果是parentNode的左子树中有节点(但不一定是它的儿子节点)被删除了, 就调整它
parentNode.left = adjustBalance(parentNode.left);
} else {
parentNode.right = adjustBalance(parentNode.right);
}
}
return deleteNode;
}
/**
* 删除掉要删除的节点(单纯删除)
* @param deleteNode 待删除节点
* @param parentNode 待删除节点的父节点
* @return 返回被删除的节点
*/
private Node deleteNode(Node deleteNode, Node parentNode) {
// 1.叶子节点
if (isLeaf(deleteNode)) {
if (parentNode == null) {
this.root = null;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = null;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = null;
}
return deleteNode;
}
// 2.待删除节点只有一棵子树
// 2.1待删除节点只有一棵左子树
if (deleteNode.left != null && deleteNode.right == null) {
if (parentNode == null) {
this.root = deleteNode.left;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = deleteNode.left;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = deleteNode.left;
}
deleteNode.left = null;
return deleteNode;
}
// 2.2待删除节点只有一棵右子树
if (deleteNode.right != null && deleteNode.left == null) {
if (parentNode == null) {
this.root = deleteNode.right;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = deleteNode.right;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = deleteNode.right;
}
deleteNode.right = null;
return deleteNode;
}
// 3.待删除节点有两棵子树(从待删除节点的左子树中找到最大的那个节点代替它)
// 替换之后, parentNode的一棵子树可能失衡, 因此出去这个方法后, 会调整的
Node replaceNode = deleteSecondMaxNode(deleteNode);
replaceNode.left = deleteNode.left;
replaceNode.right = deleteNode.right;
deleteNode.left = null;
deleteNode.right = null;
if (parentNode == null) {
this.root = replaceNode;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = replaceNode;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = replaceNode;
}
// 调整子树的操作交给上一层去操作当前子树
return deleteNode;
}
/**
* 寻找删除返回当前节点中次大的那个节点
* @param node 当前节点
* @return 返回被删除的节点
*/
private Node deleteSecondMaxNode(Node node) {
// 这个方法交给删除中出现待删除节点有两个子树的情况,
// 因此不判断node的子树是否为空
if (node.left.right == null) {
Node deleteNode = node.left;
node.left = node.left.left;
// 此时需要调整的是node, 因此这里不需要调整, 交由上一层去调整
// 如果要调整某一个子树, 必须有它父节点, 不然调整完, 父节点的
// 指针没变, 因此要调整某棵子树, 它的父节点必须在场
return deleteNode;
}
return deleteMaxNode(node.left.right, node.left);
}
/**
* 寻找当前子树中最大的节点并删除返回
* @param curNode 当前节点
* @param parentNode 当前节点父节点
* @return 返回当前子树中的最大的节点
*/
private Node deleteMaxNode(Node curNode, Node parentNode) {
Node deleteNode;
if (curNode.right != null) {
// 获得要删除的节点返回, 顺便调整下
deleteNode = deleteMaxNode(curNode.right, curNode);
parentNode.right = adjustBalance(parentNode.right);
} else {
// 当前节点就是要删除的节点
deleteNode = curNode;
// 删除(如果要调整parentNode, 但是要到它父节点在场的时候才调整, 这样能在调整完后改变它父节点的指针)
parentNode.right = curNode.left;
curNode.left = null;
}
return deleteNode;
}
/**
* 判断node是否是叶子节点
* @param node node
* @return 如果node非null且左右节点均为空, 则返回true; 否则返回false
*/
public boolean isLeaf(Node node) {
return node != null && node.left == null && node.right == null;
}
- 在删除这里,每次回溯都会adjus,因此高度和平衡也会随之更新。
⑦ 查找
- 代码实现:
/**
* 根据要查找的值查找这个值是否在树中
* @param value 要查找的值
* @return 如果该值在树中返回true; 否则返回false
*/
public boolean search(int value) {
return searchNode(value) != null;
}
/**
* 根据值查找节点
* @param value 要查找的节点的值
* @return 返回查找到的节点
*/
public Node searchNode(int value) {
if (this.root == null) {
return null;
}
return searchNode(value, this.root);
}
/**
* 根据值查找节点
* @param value 要查找的节点的值
* @param curNode 当前节点
* @return 返回查找到的节点
*/
private Node searchNode(int value, Node curNode) {
if (curNode == null) {
// 找不到, 返回空
return null;
}
/// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (value == curNode.value) {
return curNode;
} else if (value < curNode.value) {
return searchNode(value, curNode.left);
} else {
return searchNode(value, curNode.right);
}
}
- 注释中的代码是用来测试和排序二叉树相比下的查找性能的,因为jvm堆不会很大,那么开辟太多空间的话会堆满,而且我觉得调整jvm堆也没有必要,因此加一个限制:每查找一个节点的时候会消耗时间,以此来判断两种树的查找时间性能。
5、测试代码
- 注意:测试的数据以及方法都是我随便弄的,毕竟我对真实场景不了解。
① 测试AVL树和BST树在极端数据下的高度
- 测试代码1:
@Test
public void test01() {
// 降序顺序的数列
int[] array = getReverseOrderArray(100);
BinarySortTree binarySortTree = new BinarySortTree(array);
AVLTree avlTree = new AVLTree(array);
// debug看树的构成, length=11的时候发现此时bst类似于单链表, avl的任意节点平衡因子都小于等于1
System.out.println("BST中序遍历:");
binarySortTree.midTraversal();
System.out.println("AVL树中序遍历:");
avlTree.midTraversal();
// avl高度为floor(log(n))+1, 这是我自己算的, 如果错了再改
// length为100的时候, bst高度为100, avl高度为7
System.out.println(binarySortTree.getNodeHeight(binarySortTree.getRoot()));
System.out.println(avlTree.getNodeHeight(avlTree.getRoot()));
}
public int[] getReverseOrderArray(int length) {
int[] array = new int[length];
for (int i = 0; i < array.length; i++) {
array[i] = length - i;
}
return array;
}
- 测试结果:
BST中序遍历:
Node{value=1}
Node{value=2}
Node{value=3}
Node{value=4}
Node{value=5}
Node{value=6}
// 这里省略了中间
Node{value=98}
Node{value=99}
Node{value=100}
AVL树中序遍历:
Node{value=1}
Node{value=2}
Node{value=3}
Node{value=4}
Node{value=5}
Node{value=6}
// 这里省略了中间
Node{value=98}
Node{value=99}
Node{value=100}
100
7
- 可以看到,极端情况下,排序二叉树退化成链表,高度达到了100,而AVL高度为7。
② 模拟测试AVL树和BST树在极端情况下的查找性能
- 测试代码2、3:
@Test
public void test02() {
// 这个方法的测试需要在查找的时候增添一个sleep使得每查找一个节点就sleep 1ms
int[] array = getReverseOrderArray(10000);
BinarySortTree binarySortTree = new BinarySortTree(array);
System.out.println(binarySortTree.getNodeHeight(binarySortTree.getRoot()));
long time = System.currentTimeMillis();
System.out.println(binarySortTree.search(1));
// 17210ms 1w数据下
System.out.println(System.currentTimeMillis() - time);
}
@Test
public void test03() {
// 这个方法的测试需要在查找的时候增添一个sleep使得每查找一个节点就sleep 1ms
int[] array = getReverseOrderArray(10000000);
AVLTree avlTree = new AVLTree(array);
System.out.println(avlTree.getNodeHeight(avlTree.getRoot()));
long time = System.currentTimeMillis();
System.out.println(avlTree.search(1));
// 24ms 1w数据下
System.out.println(System.currentTimeMillis() - time);
}
- 测试结果02:
10000
true
17880
- 测试结果03(查找数为1,如果是非睡眠是0ms):
24
true
39
- 测试结果03(查找数为0):
24
false
31
-
在上述测试数据中,第一行是计算两种树的层数;第二行数判断是否找到了要查找的数,第三行是查找用的ms数。(要注意的是,我设置了每次查找到一个节点,需要sleep 1ms)
-
这里为了更好的凸显出AVL树的查找性能,AVL树的数据是BST的一千倍,同时数据也是有序的,并且找的那一个数据是倒数第一个;
-
因为我在实现二叉排序树的插入的时候,用的是递归形式,因此插入到1w数据就差不多要栈溢出了(逆序且是有序插入),而对于AVL树而言栈溢出的可能不大,倒是要担心jvm堆空间满的情况,因为插入的时候即使1亿数据最深层也是26层,不过感觉内存放不下。
-
在进行初始化AVL树的时候,非常慢。(在优化后,才发现AVL树插入的大部分时间都在查找高度上,因此每个节点记录自己的高度,速度快很多,在建1000w数据的AVL树的时候,感觉速度居然比没优化前建100000数据的AVL树还要快)。
③ 测试是否所有节点插入了AVL树
- 测试代码4、5:
@Test
public void test04() {
// 测试是不是所有节点插入进去了
int[] array = getReverseOrderArray(1000);
AVLTree avlTree = new AVLTree(array);
// 这里
for (int i = 0; i < 1002; i++) {
if (!avlTree.search(i)) {
System.out.println(i + "找不到");
}
}
}
@Test
public void test05() {
// 测试是不是所有节点插入进去了(插进去且有序了, 但是遍历相同值的顺序的时候就不是按照插入的来了)
int[] array = {1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4, 2, 1, 2, 3, 2, 1};
AVLTree avlTree = new AVLTree(array);
System.out.println("=======================");
avlTree.midTraversal();
}
- 测试结果4:
0找不到
1001找不到
- 测试结果5:
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=1}
Node{value=2}
Node{value=2}
Node{value=2}
Node{value=2}
Node{value=2}
Node{value=2}
Node{value=3}
Node{value=3}
Node{value=4}
- 注意:这里出现的相同value的节点不一定是按插入的顺序遍历出来的。
④ 测试删除
- 测试代码6、7:
@Test
public void test06() {
int[] array = {7, 3, 10, 12, 5, 1, 9, 11, 32, 15, 13};
AVLTree avlTree = new AVLTree(array);
avlTree.midTraversal();
System.out.println("开始删除:");
System.out.println(avlTree.deleteNode(12));
System.out.println(avlTree.deleteNode(11));
System.out.println(avlTree.deleteNode(10));
System.out.println(avlTree.deleteNode(100));
System.out.println("删除结束:");
System.out.println("高度:" + avlTree.getNodeHeight(avlTree.getRoot()));
avlTree.midTraversal();
}
@Test
public void test07() {
int[] array = getReverseOrderArray(100);
AVLTree avlTree = new AVLTree(array);
avlTree.midTraversal();
AVLTree.Node delete = avlTree.deleteNode(49);
System.out.println(delete);
avlTree.midTraversal();
System.out.println(avlTree.getNodeHeight(avlTree.getRoot()));
}
- 测试结果6:
Node{value=1}
Node{value=3}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=10}
Node{value=11}
Node{value=12}
Node{value=13}
Node{value=15}
Node{value=32}
开始删除:
Node{value=12}
Node{value=11}
Node{value=10}
null
删除结束:
高度:4
Node{value=1}
Node{value=3}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=13}
Node{value=15}
Node{value=32}
再删一个:
Node{value=1}
高度:4
Node{value=3}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=13}
Node{value=15}
Node{value=32}
- 这里最后剩下7个元素高度为4,通过debug发现这棵树长这样:
7
/ \
3 15
\ / \
5 9 32
\
13
- 每一个节点的平衡因子都不大于1。
- 试一下在这个基础上删除5,这样就会失衡,形成RL型,然后右旋15(转成RR型)左旋7后,9会成为新的根节点:
// 前面一样
再删一个:
Node{value=3}
高度:3
根节点:Node{value=9}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=13}
Node{value=15}
Node{value=32}
- debug发现这棵树长这样:
9
/ \
7 15
/ / \
5 13 32
- 测试结果7:
Node{value=1}
Node{value=2}
Node{value=3}
// 省略
Node{value=48}
Node{value=49}
Node{value=50}
// 省略
Node{value=98}
Node{value=99}
Node{value=100}
删除的元素为:Node{value=49}
Node{value=1}
Node{value=2}
Node{value=3}
// 省略
Node{value=45}
Node{value=46}
Node{value=47}
Node{value=48}
Node{value=50}
Node{value=51}
Node{value=52}
// 省略
Node{value=98}
Node{value=99}
Node{value=100}
高度为:7
6、完整代码
- 代码实现:
public class AVLTree {
/**
* 静态内部类: AVL树节点
*/
static class Node {
/**
* 节点值
*/
private final Integer value;
/**
* 左节点
*/
private Node left;
/**
* 右节点
*/
private Node right;
/**
* 树的高度
*/
private int height;
public Node(Integer value) {
this.value = value;
}
/**
* 获取当前节点作为根节点的树的高度
* @return 返回当前节点作为根节点的树的高度
*/
public int height() {
int maxHeights = left == null ? 0 : left.height;
this.height = (right == null ? maxHeights : Math.max(maxHeights, right.height)) + 1;
return this.height;
}
public Integer getValue() {
return value;
}
public Node getLeft() {
return left;
}
public Node getRight() {
return right;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
/**
* 根节点
*/
private Node root;
public AVLTree(int value) {
this.root = new Node(value);
}
public AVLTree(int[] array) {
if (array != null) {
for (int num : array) {
add(num);
}
}
}
/**
* 返回树的根节点
* @return 返回树的根节点
*/
public Node getRoot() {
return this.root;
}
/**
* 插入一个值到AVL树中
* @param value 要插入的值
*/
public void add(int value) {
if (this.root == null) {
this.root = new Node(value);
this.root.height = 1;
} else {
// 如果整棵子树的是根节点失衡, 那么调整完后会返回新的根节点给root
this.root = add(value, this.root);
}
}
/**
* 插入一个节点
* @param value 要插入的值
* @param curRoot 当前子树的根节点
* @return 返回当前子树调整过后的新的根节点
*/
private Node add(int value, Node curRoot) {
if (curRoot == null) {
Node node = new Node(value);
node.height = 1;
return node;
}
if (value < curRoot.value) {
// 子树可能发生调整, 因此要获取新的调整后的值
curRoot.left = add(value, curRoot.left);
} else {
curRoot.right = add(value, curRoot.right);
}
// 当前节点需要调整
return adjustBalance(curRoot);
}
/**
* 调整以curRoot为根节点的树的平衡, 同时无论是否调整平衡结束, curRoot都要更新高度
* @param curRoot 当前子树的根节点
* @return 返回当前子树的新的根节点
*/
private Node adjustBalance(Node curRoot) {
if (curRoot == null) {
return null;
}
// 调整平衡
Node newRoot = curRoot;
if (getNodeHeight(curRoot.right) - getNodeHeight(curRoot.left) > 1) {
// 右子树高度高, 判断是什么型(这里不需要担心空指针, 最少右边高度为2, 否则不会进来)
if (getNodeHeight(curRoot.right.right) < getNodeHeight(curRoot.right.left)) {
// 这里是curRoot的右子树的左子树高度大于curRoot右子树的右子树(外层if判断curNode到它的子节点是L/R型,
// 这里的内层if判断curNode是在右边节点的哪个子节点被插入了新节点导致失衡)
// RL型(先右旋右子树(这里即使cur的右子树就两个节点也没关系, 只是出现对指针赋值null而已), 再左旋curRoot)
curRoot.right = rightRotate(curRoot.right);
}
// RR型(RL型转为了RR型)
newRoot = leftRotate(curRoot);
} else if (getNodeHeight(curRoot.left) - getNodeHeight(curRoot.right) > 1) {
// 左子树高度高, 其它解释参考上述解释
if (getNodeHeight(curRoot.left.left) < getNodeHeight(curRoot.left.right)) {
// LR型
curRoot.left = leftRotate(curRoot.left);
}
// LL型(LR型转为了LL型)
newRoot = rightRotate(curRoot);
}
// 更新该节点的高度(如果当前子树根节点没调整过, 但是它子树底下调整过, 那么回溯的时候, 会往上更新高度, 这里就是回溯时候无论是否
// adjust都要更新高度)
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
/**
* 获取当前节点为根的树的高度
* @param node 当前节点
* @return 返回以当前节点为根的树的高度
*/
public int getNodeHeight(Node node) {
return node == null ? 0 : node.height();
}
/**
* 左旋转(RR型)
* @param oldRoot 子树旧根节点
* @return 返回新根节点
*/
private Node leftRotate(Node oldRoot) {
Node newRoot = oldRoot.right;
oldRoot.right = newRoot.left;
newRoot.left = oldRoot;
// 当旋转结束后, 只有旧和新节点高度改变
oldRoot.height = getNodeHeight(oldRoot);
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
/**
* 右旋转(LL型)
* @param oldRoot 子树旧根节点
* @return 返回新根节点
*/
private Node rightRotate(Node oldRoot) {
Node newRoot = oldRoot.left;
oldRoot.left = newRoot.right;
newRoot.right = oldRoot;
oldRoot.height = getNodeHeight(oldRoot);
newRoot.height = getNodeHeight(newRoot);
return newRoot;
}
/**
* 中序遍历根节点
*/
public void midTraversal() {
if (this.root != null) {
midTraversal(this.root);
}
}
/**
* 中序遍历当前传入节点
* @param curNode 当前节点
*/
private void midTraversal(Node curNode) {
if (curNode.left != null) {
midTraversal(curNode.left);
}
System.out.println(curNode);
if (curNode.right != null) {
midTraversal(curNode.right);
}
}
/**
* 根据要查找的值查找这个值是否在树中
* @param value 要查找的值
* @return 如果该值在树中返回true; 否则返回false
*/
public boolean search(int value) {
return searchNode(value) != null;
}
/**
* 根据值查找节点
* @param value 要查找的节点的值
* @return 返回查找到的节点
*/
public Node searchNode(int value) {
if (this.root == null) {
return null;
}
return searchNode(value, this.root);
}
/**
* 根据值查找节点
* @param value 要查找的节点的值
* @param curNode 当前节点
* @return 返回查找到的节点
*/
private Node searchNode(int value, Node curNode) {
if (curNode == null) {
// 找不到, 返回空
return null;
}
/// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
if (value == curNode.value) {
return curNode;
} else if (value < curNode.value) {
return searchNode(value, curNode.left);
} else {
return searchNode(value, curNode.right);
}
}
/**
* 根据value删除节点
* @param value 根据该value删除节点
* @return 如果删除成功返回删除节点; 否则返回null
*/
public Node deleteNode(int value) {
if (this.root == null) {
return null;
}
return deleteNode(value, this.root, null);
}
/**
* 查找要删除的节点并删除, 然后调整树的操作
* @param value 根据该值找到待删除的节点
* @param curNode 判断当前节点是否是待删除结点
* @param parentNode 当前节点的父节点
* @return 返回被删除的节点
*/
private Node deleteNode(int value, Node curNode, Node parentNode) {
if (curNode == null) {
return null;
}
// 这个是记录parentNode在递归删除结束回来的时候要调整哪个方向, 提前记录是因为删除后parentNode的子节点可能会变
// 这样后面就不知道要调整哪边
boolean isLeft = parentNode != null && parentNode.left == curNode;
Node deleteNode;
if (value < curNode.value) {
deleteNode = deleteNode(value, curNode.left, curNode);
} else if (value > curNode.value) {
deleteNode = deleteNode(value, curNode.right, curNode);
} else {
// 开始删除
deleteNode = deleteNode(curNode, parentNode);
}
if (deleteNode == null) {
// 删除失败(不需要调整, 因为树没有变)
return null;
}
// 调整
if (parentNode == null) {
// 调整根节点(这一步是删除元素后最后一次调整树)
this.root = adjustBalance(this.root);
} else {
if (isLeft) {
// 如果是parentNode的左子树中有节点(但不一定是它的儿子节点)被删除了, 就调整它
parentNode.left = adjustBalance(parentNode.left);
} else {
parentNode.right = adjustBalance(parentNode.right);
}
}
return deleteNode;
}
/**
* 删除掉要删除的节点(单纯删除)
* @param deleteNode 待删除节点
* @param parentNode 待删除节点的父节点
* @return 返回被删除的节点
*/
private Node deleteNode(Node deleteNode, Node parentNode) {
// 1.叶子节点
if (isLeaf(deleteNode)) {
if (parentNode == null) {
this.root = null;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = null;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = null;
}
return deleteNode;
}
// 2.待删除节点只有一棵子树
// 2.1待删除节点只有一棵左子树
if (deleteNode.left != null && deleteNode.right == null) {
if (parentNode == null) {
this.root = deleteNode.left;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = deleteNode.left;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = deleteNode.left;
}
deleteNode.left = null;
return deleteNode;
}
// 2.2待删除节点只有一棵右子树
if (deleteNode.right != null && deleteNode.left == null) {
if (parentNode == null) {
this.root = deleteNode.right;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = deleteNode.right;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = deleteNode.right;
}
deleteNode.right = null;
return deleteNode;
}
// 3.待删除节点有两棵子树(从待删除节点的左子树中找到最大的那个节点代替它)
// 替换之后, parentNode的一棵子树可能失衡, 因此出去这个方法后, 会调整的
Node replaceNode = deleteSecondMaxNode(deleteNode);
replaceNode.left = deleteNode.left;
replaceNode.right = deleteNode.right;
deleteNode.left = null;
deleteNode.right = null;
if (parentNode == null) {
this.root = replaceNode;
} else if (parentNode.left != null && parentNode.left == deleteNode) {
parentNode.left = replaceNode;
} else if (parentNode.right != null && parentNode.right == deleteNode) {
parentNode.right = replaceNode;
}
// 调整子树的操作交给上一层去操作当前子树
return deleteNode;
}
/**
* 寻找删除返回当前节点中次大的那个节点
* @param node 当前节点
* @return 返回被删除的节点
*/
private Node deleteSecondMaxNode(Node node) {
// 这个方法交给删除中出现待删除节点有两个子树的情况,
// 因此不判断node的子树是否为空
if (node.left.right == null) {
Node deleteNode = node.left;
node.left = node.left.left;
// 此时需要调整的是node, 因此这里不需要调整, 交由上一层去调整
// 如果要调整某一个子树, 必须有它父节点, 不然调整完, 父节点的
// 指针没变, 因此要调整某棵子树, 它的父节点必须在场
return deleteNode;
}
return deleteMaxNode(node.left.right, node.left);
}
/**
* 寻找当前子树中最大的节点并删除返回
* @param curNode 当前节点
* @param parentNode 当前节点父节点
* @return 返回当前子树中的最大的节点
*/
private Node deleteMaxNode(Node curNode, Node parentNode) {
Node deleteNode;
if (curNode.right != null) {
// 获得要删除的节点返回, 顺便调整下
deleteNode = deleteMaxNode(curNode.right, curNode);
parentNode.right = adjustBalance(parentNode.right);
} else {
// 当前节点就是要删除的节点
deleteNode = curNode;
// 删除(如果要调整parentNode, 但是要到它父节点在场的时候才调整, 这样能在调整完后改变它父节点的指针)
parentNode.right = curNode.left;
curNode.left = null;
}
return deleteNode;
}
/**
* 判断node是否是叶子节点
* @param node node
* @return 如果node非null且左右节点均为空, 则返回true; 否则返回false
*/
public boolean isLeaf(Node node) {
return node != null && node.left == null && node.right == null;
}
}
7、补充:平衡二叉树节点的高度
-
在刷leetcode1382题的时候,因为手动建树且旋转过程中不将节点高度缓存起来,那么每次获取高度都需要花很长时间,最终导致建树超时;
-
因此我仔细看了下,发现左旋或右旋后,只有新和旧节点的高度是改变了的,对于它们的子节点,它们底下的子树也没变,高度也不会改变,因此高度很明显可以缓存起来;
-
顺便讲讲这道题的解法吧,因为是二叉搜索树转平衡二叉树,而又因为是二叉搜索树,所以中序遍历后获取有序的数组,然后二分法,插入值到新的树中,这样就不需要旋转了,速度快很多。