1. 平衡二叉搜索树(Balanced Binary Search Tree)
平衡:当节点数量固定时,左右子树的高度越接近,这颗二叉树就越平衡,即高度越低。最理想的平衡就是像完全二叉树和满二叉树一样,高度最小。
常见平衡二叉搜索树:
- AVL树
- 红黑树
改进方案:
在二叉搜索树的基础上进行改进,在节点添加、删除之后,尽量让二叉树恢复平衡。
- 改进前:
- 改进后:
但如果调整的次数过多,也有可能会增加时间复杂度。因此使用尽量少的调整次数达到适度的平衡即可。
2. AVL树
2.1 AVL概念
- 平衡因子(Balance Factor):某节点的左右子树的高度差(左 - 右)。
- 特点:每个节点的平衡因子只能是 1 0 -1 否则称为失衡,即每个节点的左右高度差不能超过 1 。
- 搜索、添加、删除的时间复杂度都是O(logn)。
2.2 添加
2.2.1 失衡情况
在添加元素时最坏情况可能会导致所有的祖先节点都会失衡,但并不会导致非祖先节点和父节点失衡。
比如下方是一棵树的一部分:
如果添加元素13:
就会导致其除父节点外的祖先节点失衡一直到根节点都是处于失衡的状态。
2.2.2 LL-右旋转(单旋)
LL:其左子节点的左子几点导致其失衡。
如下图:在添加之前节点n
的左右高度T0 = T1
,父节点p
的高度T2 = T0 + 1
,祖先节点g
的高度T3 = T2 + T0
,现在处于平衡状态。
添加元素之后:g
右高度T3 = T2 + T0 + 1
,它的平衡因子就变成了2,导致祖先节点g
失衡。
解决方案:让g
进行右旋转一次。让p
的右子节点变为g
的左子节点,将g
变为p
的右子节点,使p
变为这颗子树的根节点。修改节点的parent
指向,按顺序修改节点g
和p
的高度。调整之后会不仅会使改树保持AVL树的平衡,也依然会保持搜索树的性质。
g.left = p.right;
p.right = g;
调整指向:
修改节点结构。
2.2.3 RR-右旋转(单旋)
RR:其右子节点的右子节点导致其失衡。
此时处于平衡状态:
对T3
添加一个子节点,会导致g
的平衡因子变为-2。
解决方案:
g.right = p.left;
p.left = g;
然后让p
成为子树的根节点。之后修改parent
属性,按顺序修改g
和p
的高度。
修改指向:
调整结构:
2.2.4 LR - RR 左旋转,LL右旋转(双旋)
如下图要在添加新节点(红色方块)时,需要先找左子节点再找右子节点,称为LR。
添加之后会时g
的平衡因子变为2,导致失衡。
解决方案:
- 先对
p
进行左旋转
左旋转之后图示:显然没有恢复平衡。
- 再进行右旋转之后恢复平衡:
2.2.5 RL - LL右旋转,RR 左旋转(双旋)
在添加时先找右子节点再找左子节点的情况称为RL。
解决方案:
-
先右旋转
-
再左旋转
-
恢复平衡
2.2.6 接口设计
因为AVL树是由BST树的特殊分支,因此直接继承BST树即可。
- BST树新增方法:由子类去实现
/**
* 添加节点之后处理,由子类去实现
* @param node
*/
protected void afterAdd(Node<E> node){
}
- 在
add()
方法中调用:
/**
* 添加元素
* @param e
*/
public void add(E e){
elementNotNullCheck(e);
// 添加第一个节点
if (root == null){
root = new Node<>(e,null);
size++;
// 添加节点之后的处理
afterAdd(root);
return;
}
// 记录父节点
Node<E> parent = root;
// 记录当前比较的节点
Node<E> node = root;
// 记录比较值
int cmp = 0;
while (node != null){
// 获取与当前节点的比较值
cmp = compare(e, node.e);
// 保存父节点
parent = node;
if (cmp > 0){ // 大于当前节点的值,就找右子节点
node = node.right;
}else if (cmp < 0){ // 小于当前节点的值就找左子节点
node = node.left;
}else { // 相等
// 用传入的值覆盖掉原先该节点的值
node.e = e;
return;
}
}
// 根据最后一次的比较值判断放在左子节点还是右子节点。
Node<E> nowNode = new Node<>(e, parent);
if (cmp > 0){
parent.right = nowNode;
}else {
parent.left = nowNode;
}
// 添加节点之后的处理
afterAdd(nowNode);
size++;
}
2.2.6 接口实现
这里主要实现的是
afterAdd()
方法。
2.2.6.1 完善AVL节点类
- 新增height属性,因为BST树和二叉树中不需要去维护改属性,因此这里在AVL树中新增节点类并包含height属性,并继承
BinaryTree.Node
节点。
private static class AVLNode<E> extends Node<E>{
// 高度
// 每次新增时的节点都是叶子节点,叶子节点的高度为1。
int height = 1;
public AVLNode(E e, Node<E> parent) {
super(e, parent);
}
}
- 新增
crateNode(E e, Node<E> parent)
接口:因为BST树的添加方法中的节点是二叉树的节点,导致无法使用AVL树的节点去维护height
属性。因此这里在二叉树中新增接口由子类去实现。
/**
* 创建节点接口
* @param e
* @param parent
* @return
*/
protected Node<E> createNode(E e, Node<E> parent){
return new Node<>(e,parent);
}
- 修改add(E e):直接调用
createNode
即可。
/**
* 添加元素
* @param e
*/
public void add(E e){
elementNotNullCheck(e);
// 添加第一个节点
if (root == null){
root = createNode(e,null);
size++;
// 添加节点之后的处理
afterAdd(root);
return;
}
// 记录父节点
Node<E> parent = root;
// 记录当前比较的节点
Node<E> node = root;
// 记录比较值
int cmp = 0;
while (node != null){
// 获取与当前节点的比较值
cmp = compare(e, node.e);
// 保存父节点
parent = node;
if (cmp > 0){ // 大于当前节点的值,就找右子节点
node = node.right;
}else if (cmp < 0){ // 小于当前节点的值就找左子节点
node = node.left;
}else { // 相等
// 用传入的值覆盖掉原先该节点的值
node.e = e;
return;
}
}
// 根据最后一次的比较值判断放在左子节点还是右子节点。
Node<E> nowNode = createNode(e,parent);
if (cmp > 0){
parent.right = nowNode;
}else {
parent.left = nowNode;
}
// 添加节点之后的处理
afterAdd(nowNode);
size++;
}
- 重写接口:在AVL树中重写
createNode
接口:
@Override
protected Node<E> createNode(E e, Node<E> parent) {
return new AVLNode<E>(e,parent);
}
- 计算平衡因子:在AVL树中新增计算平衡因子的方法。
/**
* 获取平衡因子
* @return
*/
public int balanceFactor(){
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
2.2.6.2 更新正常节点高度
- 分析导致失衡节点因为失衡是添加节点导致的,因此找到最先失衡的父节点就可以处理掉失衡,因此需要一直往
parent
方向去找节点。 - 判断是否平衡
/**
* 判断是否平衡
* @param node
* @return
*/
private boolean isBalance(Node<E> node){
return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
}
-
更新高度:
①在AVLNode
节点中新增更新高度方法:/** * 更新当前节点的高度 */ private void updateHeight(){ int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height; int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height; height = 1 + Math.max(leftHeight,rightHeight); }
②在外层封装更新高度方法
/** * 更新节点高度 * @param node */ private void updateHeight(Node<E> node){ ((AVLNode<E>)node).updateHeight(); }
2.2.6.3 恢复平衡
-
在二叉树的节点类新增判断是左子节点还是右子节点操作:
/** * 判断是不是父节点的左子树 * @return */ public boolean isLeftChild(){ return parent != null && this == parent.left; } /** * 判断是不是父节点的右子树 * @return */ public boolean isRightChild(){ return parent != null && this == parent.right; }
2.2.6.3.1 左旋转
- RR情况:
- 修改指向
Node<E> parent = grand.right;
parent.right = grand.left;
parent.left = grand;
- 修改parent:
// 更新 p 的 parent 使成为当前子树的根节点
parent.parent = grand.parent;
if (grand.isLeftChild()){
grand.parent.left = parent;
}else if (grand.isRightChild()){
grand.parent.right = parent;
}else { // grand 是根节点
root = parent;
}
// 更新 p.left 的 parent
if (grand.right != null){
grand.right.parent = grand;
}
// 更新 g 的 parent
grand.parent = parent;
- 更新高度
// 更新高度
updateHeight(grand);
updateHeight(parent);
2.2.6.3.2 右旋转
- LL情况:
- 修改指向
Node<E> parent = grand.left;
grand.left = parent.right;
parent.right = grand;
- 更新parent
// 更新 p 的 parent 使成为当前子树的根节点
parent.parent = grand.parent;
if (grand.isLeftChild()){
grand.parent.left = parent;
}else if (grand.isRightChild()){
grand.parent.right = parent;
}else { // grand 是根节点
root = parent;
}
// 更新 p.left 的 parent
if (grand.left != null){
grand.left.parent = grand;
}
// 更新 g 的 parent
grand.parent = parent;
- 更新高度
// 更新高度
updateHeight(grand);
updateHeight(parent);
2.2.6.3.3 整合
既然左旋转和右旋转中更新部分的代码都是一样的(除更新parent的左或者右不同),不如直接封装成一个方法。
/**
* 旋转之后 更新 parent 和 height
* @param grand
* @param parent
* @param child
*/
private void afterRotate(Node<E> grand,Node<E> parent,Node<E> child){
// 更新 parent
// 更新 p 的 parent 使成为当前子树的根节点
parent.parent = grand.parent;
if (grand.isLeftChild()){
grand.parent.left = parent;
}else if (grand.isRightChild()){
grand.parent.right = parent;
}else { // grand 是根节点
root = parent;
}
// 更新 p.left 的 parent
if (child != null){
child.parent = grand;
}
// 更新 g 的 parent
grand.parent = parent;
// 更新高度
updateHeight(grand);
updateHeight(parent);
}
- 左旋转:
/**
* 左旋
* @param grand
*/
private void rotateLeft(Node<E> grand){
Node<E> parent = grand.right;
Node<E> child = parent.left;
grand.right = child;
parent.left = grand;
afterRotate(grand, parent, child);
}
- 右旋转:
/**
* 右旋
* @param grand
*/
private void rotateRight(Node<E> grand){
Node<E> parent = grand.left;
Node<E> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
- 整合恢复平衡方法:
/**
* 恢复平衡
* @param grand 最先(高度最低,最往下)失衡的节点
*/
private void reBalance(Node<E> grand){
// 获取node比较高的子节点
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()){ // L
if (node.isLeftChild()){ // LL
rotateRight(grand);
}else { // LR
rotateLeft(parent);
rotateRight(grand);
}
}else { // R
if (node.isLeftChild()){ // RL
rotateRight(parent);
rotateLeft(grand);
}else { // RR
rotateLeft(grand);
}
}
}
- 整合
afterAdd()
方法
@Override
protected void afterAdd(Node<E> node) {
while (( node = node.parent ) != null){
if (isBalance(node)){
// 更新高度
// 这里的node最起码就是新节点的父节点
updateHeight(node);
}else {
// 恢复平衡
// 这里是高度最低的不平衡节点
reBalance(node);
// 退出循环使树恢复平衡
break;
}
}
}
2.2.6.4 统一恢复平衡方法
如图可见,因为二叉搜索树是有序的,如果让其恢复平衡,可以套用一个统一的模板,只需要传入相应的节点即可。
/**
* 统一版恢复平衡
* @param grand
*/
private void reBalancePro(Node<E> grand){
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()){ // L
if (node.isLeftChild()){ // LL
rotate(grand,
node.left, node, node.right,
parent,
parent.right, grand, grand.right);
}else { // LR
rotate(grand,
parent.left, parent, node.left,
node,
node.right, grand, grand.right);
}
}else { // R
if (node.isLeftChild()){ // RL
rotate(grand,
grand.left, grand, node.left,
node,
node.right, parent, parent.right);
}else { // RR
rotate(grand,
grand.left, grand, parent.left,
parent,
node.left, node, node.right);
}
}
}
/**
* 旋转
* @param r 子树根节点
* @param a
* @param b
* @param c
* @param d
* @param e
* @param f
* @param g
*/
private void rotate(
Node<E> r,
Node<E> a, Node<E> b, Node<E> c,
Node<E> d,
Node<E> e, Node<E> f, Node<E> g){
// 让 d 成为这颗子树的根节点
d.parent = r.parent;
if (r.isRightChild()){
r.parent.right = d;
}else if (r.isLeftChild()){
r.parent.left = d;
}else {
root = d;
}
// a b c
b.left = a;
if (a != null){
a.parent = b;
}
b.right = c;
if (c != null){
c.parent = b;
}
// 因为修改了 b 的左右子树,所以需要更新高度
updateHeight(b);
// e f g
f.left = e;
if (e != null){
e.parent = f;
}
f.right = g;
if (g != null){
g.parent = f;
}
// 因为修改了 f 的左右子树,所以需要更新高度
updateHeight(f);
// b d f
d.left = b;
b.parent = d;
d.right = f;
f.parent = d;
updateHeight(d);
}
2.2.6.5 测试
-
BST
-
AVL
2.3 删除
执行删除时只可能会导致父节点删除,其它的节点不会有影响。
如图删除节点16:
删除之后:只会改变其父节点15的平衡因子,不会改变其高度(除非只有被删除节点一个子节点,但是这种情况不会导致失衡),因此在只会造成其父节点或者祖先节点中的一个节点失衡。
删除的四种情况也和添加一样:但如果在恢复平衡的过程中改变了这颗子树的高度,就可能会导致这颗子树的父节点失衡,可能会一直持续到root节点,最坏可导致恢复O(logn)次。
2.3.1 设计接口
- 在二叉搜索树中新增:
/**
* 删除节点之后的处理,由子类去实现
* @param node
*/
protected void afterRemove(Node<E> node){
}
- 在
remove()
方法中调用afterRemove
,这里调用需要等节点真正删除完毕之后调用:
/**
* 根据传入的节点执行删除
* @param node
*/
private void remove(Node<E> node){
if (node == null){
return;
}
size--;
// 判断度为2
if (node.hasTwoChildren()){
// 获取后继节点
Node<E> s = successor(node);
// 后继节点的值覆盖当前节点的值
node.e = s.e;
// 将node指向s节点
node = s;
}
// 获取被删除节点的子节点,如果左子节点为空则获取右子节点;
// 如果子节点都为空,则表示该节点是叶子节点返回null。
Node<E> relacement = node.left != null ? node.left : node.right;
if (relacement != null){ // node 是度为1的节点
// 更改parent
relacement.parent = node.parent;
// 更改parent的left(right)的指向
if (node.parent == null){ // node是度为1的根节点。
root = relacement;
}else if (node == node.parent.left){ // 被删除节点是父节点的左子节点
node.parent.left = relacement;
} else { // 被删除节点是父节点的右子节点
node.parent.right = relacement;
}
// 删除之后处理
afterRemove(node);
}else if(node.parent == null){ // node是叶子节点并且是根节点
root = null;
// 删除之后处理
afterRemove(node);
}else { //node 是普通叶子节点
if (node == node.parent.left){ // 当前节点是父节点的右子节点
node.parent.left = null;
}else { // 是父节点的左子节点
node.parent.right = null;
}
// 删除之后处理
afterRemove(node);
}
}
2.3.2 实现接口
@Override
protected void afterRemove(Node<E> node) {
while (( node = node.parent ) != null){
if (isBalance(node)){
// 更新高度
// 这里的node最起码就是新节点的父节点
updateHeight(node);
}else {
// 恢复平衡
// 这里是高度最低的不平衡节点
reBalancePro(node);
// 恢复之后不能退出,因为可能会导致父节点也失衡
}
}
}
2.4 总结
- 添加:可能会导致所有的祖先节点都失衡,只要使其高度恢复平衡,整棵树就可以恢复平衡。
- 删除:只可能回导致父节点失衡,恢复平衡后可能会导致所有的祖先节点失衡。
- 平均复杂度:
①搜索:O(logn);
②添加:O(logn),最多需要O(1)次旋转操作;
③删除:O(logn),最多需要O(logn)次旋转操作。