开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第23天,点击查看活动详情
学习恋上数据结构与算法笔记
1.二叉搜索树的复杂度分析
1.1 比较平衡的情况
如果是按照 7、4、9、2、5、8、11 的顺序添加节点
添加、删除、搜索节点的时间复杂度均为 ,比较的次数与树的高度有关,树的高度是多少,就会比较多少次,所以时间复杂度也可以视为 。
1.2极端情况 退化为链表
但是如果是从小到大添加节点。
当
n比较大时,两者的性能差异比较大比如
n = 1000000时,二叉搜索树的最低高度是20
1.3 退化成链表的另一种情况
删除节点时也可能会导致二叉搜索树退化成链表
删除节点之后的二叉树
综上所述,得到结论:添加、删除节点时,都可能会导致二叉搜索树退化成链表。
提出问题:有没有办法防止二叉搜索树退化成链表?让添加、删除、搜索的复杂度维持在
2.平衡
平衡:当节点数量固定时,左右子树的高度越接近,这棵二叉树就越平衡(高度越低)
第一种二叉树
第二种二叉树
第三种二叉树
第四种二叉树
从上到下,越来越平衡
3.理想平衡
最理想的平衡,就是像完全二叉树、满二叉树那样,高度是最小的
4.如何改进二叉搜索树?
首先,节点的添加、删除顺序是无法限制的【无法决定树改变的顺序】,可以认为是随机的。所以,改进方案是:在节点的添加、删除操作之后,想办法让二叉搜索树恢复平衡(减小树的高度)
根节点 7 的左子树高度为 2 ,对应的右子树的高度为 4 ,不平衡。
经过调整之后节点 12 变成了节点 10 和节点 14 的父节点,这样调整之后根节点 7 的右子树的高度为 3。
调整之后树的整体高度减小了。
如果接着继续调整节点的位置,完全可以达到理想平衡,但是付出的代价可能会比较大。比如调整的次数会比较多,反而增加了时间复杂度。
总结来说,比较合理的改进方案是:用尽量少的调整次数达到适度平衡即可。一棵达到适度平衡的二叉搜索树,可以称之为:平衡二叉搜索树
5.平衡二叉搜索树(Balanced Binary Search Tree)
英文简称为:BBST。经典常见的平衡二叉搜索树有:
AVL树Windows NT内核中广泛使用
- 红黑树
C++ STL(比如map、set)Java的TreeMap、TreeSet、HashMap、HashSetLinux的进程调度Nginx的timer管理
一般也称它们为:自平衡的二叉搜索树(Self-balancing Binary Search Tree)
6.AVL树
AVL 树是最早发明的自平衡二叉搜索树之一 。AVL 取名于两位发明者的名字 G. M. Adelson-Velsky 和 E. M. Landis(来自苏联的科学家)
平衡因子( Balance Factor ):某结点的左右子树的高度差 。
AVL 树的特点:
- 每个节点的平衡因子只可能是
1、0、-1(绝对值 ,如果超过 1,称之为失衡) - 每个节点的左右子树高度差不超过
1 - 搜索、添加、删除的时间复杂度是
叶子节点的平衡因子一定是 0。
下面这棵树是一棵 AVL 树。
7.平衡对比
输入数据:35, 37, 34, 56, 25, 62, 57, 9, 74, 32, 94, 80, 75, 100, 16, 82
8.简单的继承结构
AVL 树和红黑树就是在 BST 的基础上增加了自平衡的功能,即在添加/删除元素之后做一些调整来保证修改之后的树还是平衡的。
9.添加导致的失衡
往下面这棵子树中添加 13,最坏情况:可能会导致所有祖先节点都失衡, 但是父节点、非祖先节点,都不可能失衡。
添加了节点 13 之后,节点 14 的平衡因子变成了 2,其父节点 15 的平衡因子变成了2,其父节点 9 的平衡因子变成了 -2,因此最坏情况下节点 9 上面的所有祖先节点都失衡【因为这部分整个高度增加了 1】
但是添加位置(本例中为节点 12) 必然不可能失衡,原因是:首先添加之前节点 12 是平衡的:情况①:节点 12 是叶子节点,那么添加之后肯定还是平衡的;情况②:节点 12 左边有一个节点,在 12 右边添加 13 之后对于节点 12 来说平衡因子为 0,还是平衡的。注意不可能存在其他的情况,如果节点 12 右边有元素的话,新加的节点的父节点就不可能是 12 了。
只会导致
13 -> 12 -> 14 -> 15 -> 9 …这条线上的非父节点失衡,而节点4,6,8等节点是绝对不会失衡的
9.1 LL – 右旋转(单旋)
LL的含义是:表现形式为Left-Left【失去平衡的节点是它左边的左边的节点导致的】,解决办法是右旋
添加必然发生在最角落里面。n-> node 节点 p -> parent 父节点 g -> grandparent 祖父节点
初始状态:
在节点 n 的左子树上添加一个节点之后,节点 g 失衡:
LL 表示 Left-Left,失去平衡的节点 g 失衡的原因是其左边的左边的节点 n 里面的子节点导致的。如何能够让节点 g 恢复平衡?一般这里的做法是右旋转。
g.left = p.right 先动高的
p.right = g 后动低的
让p成为这棵子树的根节点
可以看到最右边展示的效果经过旋转之后已经是平衡的了。需要注意的是:由于添加之前 T0、T1、T2、 T3 就是平衡的,而且新加的节点并不会导致T0、T1、T2、 T3发生变化,因此只需要不需要考虑这些位置的失衡。
仍然是一棵二叉搜索树:T0 < n < T1 < p < T2 < g < T3
整棵树都达到平衡
这一次操作不会导致调整后p 的父节点失衡,因为下面是平衡的,上面自然也是平衡的。【状态3与状态1的高度相同,旋转之后整棵子树的高度没有发生变化】
还需要注意维护的内容
T2、p、g 的 parent 属性
先后更新 g、p 的高度
protected static class Node<E> {
E element;
Node<E> left;
Node<E> right;
Node<E> parent;
int height; // 增加一个高度属性,用来计算平衡因子
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
public boolean isLeaf() {
return left == null && right == null;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
}
旋转之后 g 和 p 的左右子树发生了变化,比如 g 的左子树变成了 T2,p 的右子树变成了 g ,左右子树都发生了变化,这意味着树的高度发生了变化,需要重新计算。注意更新的顺序:必须是先更新 g,再更新 p,原因是旋转之后 g 已经变成了 p 的子节点了。【先更新矮的,再更新高的】
9.2 RR – 左旋转(单旋)
RR的含义是:表现形式为Right-Right【失去平衡的节点是它右边的右边的节点导致的】,解决办法是左旋
初始状态:
在节点 n 的右子树上添加一个节点之后,节点 g 失衡:
g.right = p.left
p.left = g
让p成为这棵子树的根节点
仍然是一棵二叉搜索树:T0 < n < T1 < p < T2 < g < T3
整棵树都达到平衡
还需要注意维护的内容
T2、p、g 的 parent 属性
先后更新 g、p 的高度
9.3 LR – RR左旋转,LL右旋转(双旋)
LR的含义是:表现形式为Left-Right【失去平衡的节点是它左边的右边的节点导致的】,解决办法是双旋
初始情况:
往 T1 或 T2 的下面添加一个节点,导致节点 g 失衡:
解决办法是:先对 p 进行 RR 左旋转:
即将 p 的右边指向 n 的左边, n 的左边指向 p,p 的 parent 指向 n,即让 n 成为这棵子树的根节点。可以看到又回到了 LL 的情况,对 g 进行右旋转即可。
9.4 RL – LL右旋转,RR左旋转(双旋)
RL的含义是:表现形式为Right-Left【失去平衡的节点是它右边的左边的节点导致的】,解决办法是双旋
初始状态:
往 T1 或 T2 的下面添加一个节点,导致节点 g 失衡:
解决办法是:先对 p 进行 LL 右旋转:
9.5 代码
在添加节点之后调整树的结构
注意这里使用了模板方法的设计模式
在 BST.java 中添加 afterAdd(Node<E> node) 方法:
/**
* 添加node之后的调整
* 子类实现这个方法 比如在AVL树中
* @param node 新添加的节点
*/
protected void afterAdd(Node<E> node) {
}
将这个方法插入到 add() 方法中:
public void add(E element) {
elementNotNullCheck(element);
if (root == null) {
root = new Node<>(element, null);
size++;
// 新添加节点之后的处理
afterAdd(root);
return;
}
Node<E> node = root;
Node<E> parent = null;
int cmp = 0;
while (node != null) {
cmp = compare(element, node.element);
parent = node;
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else {
node.element = element;
return;
}
}
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
// 新添加节点之后的处理
afterAdd(newNode);
}
在子类 AVLTree.java 中实现这个方法:
/**
* @param node 新添加的节点
*/
@Override
protected void afterAdd(Node<E> node) {
// 找到所有失衡节点中高度最低的一个节点[最近的失衡父节点],让它恢复平衡即可
while((node = node.parent) != null) {
if(node 是否平衡) {
} else {
}
}
}
判断是否平衡需要计算平衡因子,因此需要给节点增加一个高度的属性,可以考虑添加到 BinaryTree.java 中的 Node 属性中,但是是不合适的,因为一般的二叉树中并不需要一个高度的概念,一般的如二叉搜索树中的节点不需要这个属性【每次new 一个节点就会多一个没用的属性】,只有在 AVL 树中才需要添加。
如下:直接添加在这个静态内部类中是不合适的。
/**
* 结点类型
*
* @param <E>
*/
protected static class Node<E> {
E element;
Node<E> left;
Node<E> right;
Node<E> parent;
int height; // 高度
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
public boolean isLeaf() {
return left == null && right == null;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
}
合适的做法是在 AVLTree.java 中再写一个节点类
private static class AVLNode<E> extends Node<E> {
int height;
public AVLNode(E element, Node<E> parent) {
super(element, parent);
}
}
但是这样又有问题了,在 BST.java 中实现添加的时候,往树中添加节点的时候使用的还是 new Node<>() 通用的节点(普通二叉树的节点)。解决办法是在 BinaryTree.java 中再提供一个接口给子类:
/**
* 默认情况下返回一个通用的节点
* @param element
* @param parent
* @return
*/
protected Node<E> createNode(E element, Node<E> parent) {
return new Node<>(element, parent);
}
来到 BST.java 中:
public void add(E element) {
elementNotNullCheck(element);
if (root == null) {
// 修改
root = createNode(element, null);
size++;
afterAdd(root);
return;
}
Node<E> node = root;
Node<E> parent = null;
int cmp = 0;
while (node != null) {
cmp = compare(element, node.element);
parent = node;
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else {
node.element = element;
return;
}
}
// 看看插入到父节点的哪个位置
Node<E> newNode = createNode(element, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
afterAdd(newNode);
}
最后在 AVLTree.java 文件中重写 createNode() 方法,这样在调用的时候会优先执行子类中的方法。
@Override
protected Node<E> createNode(E element, Node<E> parent) {
return new AVLNode<>(element, parent);
}
注意这里也为后面的红黑树埋下了伏笔,即红黑树里面也可以声明自己的节点并创建自己的节点
增加计算平衡因子代码(注意需要强转,因为父类中节点类型是 Node,没有 height 这个属性)
private boolean isBalanced(Node<E> node) {
// 能够进来说明一定是一个AVLNode,直接强转即可
return Math.abs(((AVLNode<E>) node).balanceFactor()) <= 1;
}
/**
*
* @param <E>
*/
private static class AVLNode<E> extends Node<E> {
int height = 1; // 创建叶子节点高度 + 1
public AVLNode(E element, Node<E> parent) {
super(element, parent);
}
public int balanceFactor() {
// 将左节点强转为AVLNode<E>类型的节点
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
}
存在的问题:如何更新高度。由于新添加的节点一定是叶子节点,叶子节点的高度可以在构造器里面直接指定 int height = 1; ,真正需要维护的是父节点的高度。
每次加入一个新的节点的时候自动更新高度。
最后实现的代码:
/**
* @param node 新添加的节点
*/
@Override
protected void afterAdd(Node<E> node) {
// 找到所有失衡节点中高度最低的一个节点,让它恢复平衡即可
while((node = node.parent) != null) {
// 进入循环的node一定是新加入node的一个parent
if(isBalanced(node)) {
// 如果是平衡的,顺便更新高度
updateHeight(node);
} else {
// 恢复平衡
}
}
}
@Override
protected Node<E> createNode(E element, Node<E> parent) {
return new AVLNode<>(element, parent);
}
private boolean isBalanced(Node<E> node) {
// 能够进来说明一定是一个AVLNode,直接强转即可
return Math.abs(((AVLNode<E>) node).balanceFactor()) <= 1;
}
// 更新节点高度(封装一层写强转的代码)
private void updateHeight(Node<E> node) {
((AVLNode<E>) node).updateHeight();
}
/**
*
* @param <E>
*/
private static class AVLNode<E> extends Node<E> {
int height = 1;
public AVLNode(E element, Node<E> parent) {
super(element, parent);
}
public int balanceFactor() {
// 将左节点强转为AVLNode<E>类型的节点
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
// 更新自己的高度
public 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);
}
}
恢复平衡的逻辑
判断旋转需要判断是左子树还是右子树,判断依据是左右子树中高度更高的子树。
在 BinaryTree.java 的静态内部类 Node 中增加方法:
/**
* 判断自己是左子树
*/
public boolean isLeftChild() {
return parent != null && this == parent.left;
}
/**
* 判断自己是左子树
*/
public boolean isRightChild() {
return parent != null && this == parent.right;
}
在 AVLTree.java 文件中的 AVLNode 类中增加方法:
/**
* 返回左右子树中更高的子树的根节点
*/
public Node<E> tallerChild() {
int leftHeight = left == null ? 0 : ((AVLNode<E>) left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>) right).height;
if (leftHeight > rightHeight) return left;
if (leftHeight < rightHeight) return right;
// 高度一样的时候返回同方向的(我是父节点的哪一边就返回哪一边)
return isLeftChild() ? left : right;
}
遇到第一个不平衡的节点,恢复平衡。
protected void afterAdd(Node<E> node) {
// 找到所有失衡节点中高度最低的一个节点,让它恢复平衡即可
while ((node = node.parent) != null) {
// 进入循环的node一定是新加入node的一个parent
if (isBalanced(node)) {
// 如果是平衡的,顺便更新高度
updateHeight(node);
} else {
// 遇到第一个不平衡的节点,需要恢复平衡
rebalance(node);
// 整棵树恢复平衡
break;
}
}
}
恢复平衡(旋转)的具体逻辑如下:
/**
* 恢复平衡
* @param grand 高度最低的那个不平衡节点
*/
public void rebalance(Node<E> grand) {
// 节点g左右子节点中相对较高的节点 --> p
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
// 节点p左右子节点中相对较高的节点 --> n
Node<E> node = ((AVLNode<E>)grand).tallerChild();
// L
if(parent.isLeftChild()) {
// LL
if(node.isLeftChild()) {
// 对节点g右旋
rotateRight(grand);
} else { // LR
// 1. p左旋
rotateLeft(parent);
// 2. g右旋
rotateRight(grand);
}
} else { // R
if(node.isLeftChild()) { // RL
// 1. p右旋
rotateRight(parent);
// 2. g左旋
rotateRight(grand);
} else { // RR
rotateLeft(grand);
}
}
}
/**
* 左旋
*/
private void rotateLeft(Node<E> node) {
}
/**
* 右旋
*/
private void rotateRight(Node<E> node) {
}
10.统一所有旋转操作
注意这里的节点是按照 a,b,c,d,e,f,g 来进行排序的,在进行 LL,RR,LR,RL 这四种操作之后最终得到的平衡树是差不多的,都是 d 作为根节点,d 的左孩子是 b,右孩子是 f,以此类推。因此完全有可能将所有的操作统一。
抽象的关键在于使得 a,b,c,d,e,f,g 为一类节点统一操作。
/**
* 恢复平衡 ---> 统一操作
*
* @param grand 高度最低的那个不平衡节点
*/
public void rebalance(Node<E> grand) {
// 节点g左右子节点中相对较高的节点 --> p
Node<E> parent = ((AVLNode<E>) grand).tallerChild();
// 节点p左右子节点中相对较高的节点 --> n
Node<E> node = ((AVLNode<E>) parent).tallerChild();
// L
if (parent.isLeftChild()) {
// LL
if (node.isLeftChild()) {
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.isLeftChild()) {
r.parent.left = d;
} else if (r.isRightChild()) {
r.parent.right = d;
} else {
root = d;
}
// a-b-c a成为b的left,c成为b的right,同时更新parent
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;
}
updateHeight(f);
// d-b-f
d.left = b;
d.right = f;
b.parent = d;
f.parent = d;
updateHeight(d);
}
11.删除导致的失衡
示例:删除下面这棵子树中的 16
删除节点 16 之后导致节点 15 失衡。
注意:只可能会导致父节点失衡,除父节点以外的其他节点,都不可能失衡
11.1 LL - 左旋转(单旋)
删除之前
删除红色节点之后节点 g 失衡
由于是 LL 的情况:
旋转之后结果如下,节点 n、p、g 都是平衡的。
由于旋转完之后整棵树的高度没有发生变化。
如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡,极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 次调整
怎么理解这个问题呢?试想上图以 g 为根节点的子树是某一个子树 s 的右子树,并且子树 s 的左子树 l 比以 g 为根节点的右子树高度大 1,那么以 g 为根节点的子树在平衡之后高度 减1,那么这就会造成子树 s 失衡。
11.2 LL - 右旋转(单旋)
删除之前
删除红色节点之后节点 g 失衡
如果绿色的节点不存在
11.3 LR-RR左旋转,LL右旋转(双旋)
删除绿色节点之后有可能导致上方失衡
11.4 RL-LL右旋转,RR左旋转(双旋)
删除绿色节点之后有可能导致上方失衡
11.5 解决方案
在删除节点之后做相应的处理
在 BST.java 文件中增加方法:
/**
* 删除node之后的调整
* 子类实现这个方法 比如在AVL树中
* @param node 被删除的节点
*/
protected void afterRemove(Node<E> node) {
}
注意:真正被删除的节点是前驱或后继节点,而不是直接传入的那个节点
node。
/**
* 真正删除节点
*
* @param node
*/
private void remove(Node<E> node) {
if (node == null) return;
size--;
if (node.hasTwoChildren()) {
Node<E> s = successor(node);
node.element = s.element;
node = s;
}
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) {
replacement.parent = node.parent;
if (node.parent == null) {
root = replacement;
}
if (node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
// 删完并且更改完指向之后才执行
// 删除节点之后的处理
afterRemove(node);
} else if (node.parent == null) {
root = null;
// 删除节点之后的处理
afterRemove(node);
} else {
if (node == node.parent.left) {
node.parent.left = null;
} else {
node.parent.right = null;
}
// 删除节点之后的处理
afterRemove(node);
}
}
在 AVLTree.java 中:
@Override
protected void afterRemove(Node<E> node) {
// 找到所有失衡节点中高度最低的一个节点,让它恢复平衡即可
// 被删除节点node的parent属性一直都在
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 如果是平衡的,顺便更新高度
updateHeight(node);
} else {
// 只要遇到不平衡的节点,一直恢复平衡
rebalance(node);
}
}
}
12.总结
添加
- 可能会导致所有祖先节点都失衡
- 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡【仅需 次调整】
删除
- 可能会导致父节点或祖先节点失衡 (只有
1个节点会失衡) - 让父节点恢复平衡后,可能会导致更高层的祖先节点失衡 【最多需要 次调整】
平均时间复杂度
- 搜索:
- 添加:,仅需 次的旋转操作
- 删除:,最多需要 次的旋转操作
13.补充
删除节点 16 的时候会导致节点 15 失衡。虽然父节点失衡了,但是父节点整体的高度是不变的。因此上层的结构肯定也没有失衡,只会导致这一个位置失衡。【删除的是15的比较短的那一边的路径,比较长的那一边并没有发生变化,因此整体的高度并没有发生变化,那么其父节点的平衡因子并没有改变】
删除节点 16 之后,会导致节点 11 (祖父节点) 失衡。
综上所述,得出结论:删除节点
16的时候,可能会导致父节点或祖先节点失衡(只有1个节点会失衡),其他节点,都不可能失衡