一、引入
1.1什么是BST树(二叉搜索树)
- 若左子树不为空,左子树所有节点值小于根节点。
- 若右子树不为空,右子树所有节点值大于根节点。
- 左右子树也是BST树
作用:可以快速查找到需要的值,常用在数据的查找上。
效率:最好O(logn),数据本来有序的时候O(n)
缺点:当插入的数据本来的就是有序的时候就退化成了链表。
例如:
为了解决这个问题出现了二叉平衡树(AVL树)
二、AVL树
- 具有BST树的所有特性。
- 每个节点的左子树和右子树的高度差最多为1。
图一是AVL树。
图二由于高度差大于1,不是AVL树。
2.1左旋和右旋?
在图一的基础上插入节点3,正常情况下会插入到4的左边,这样又破化了AVL的特性,此时就引入了左旋和右旋。
左-左型树通过右旋恢复平衡
即:顺时针旋转两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子
节点4和9高度相差大于1。由于是左孩子的高度较高,此时是左-左型,进行右旋。
右-右型树通过左旋恢复平衡
3和6的子树高度差大于1
即:逆时针旋转两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子
2.2AVL树的形成?
每次插入节点时按照BST树的规则进行(小的在左,大的在右)并检测每个节点的左右子树高度差,如果高度差大于1就进行左旋/右旋使其形成AVL树,后面继续插入节点时继续判断。
特殊情况:
右-左型: 先对10右旋变为右右型,然后再进行左旋。
左-右型同理
2.3总结
1、左-左型:做右旋。
2、右-右型:做左旋转。
3、左-右型:先做左旋,后做右旋。
4、右-左型:先做右旋,再做左旋。
不理解的看看代码思考原理
java代码实现:
class AvlNode{
int data;
AvlNode l_child;
AvlNode r_child;
// 高度从底往上算
int height;
}
public class AvlTree {
/**
* 返回树的高度
* @param node
* @return
*/
static int height(AvlNode node){
if (node == null){
return 0;
} else {
return node.height;
}
}
/**
* 右旋,返回中间节点
* @param node 中间节点
*/
static AvlNode R_Rotate(AvlNode node){
AvlNode k;
// 旋转
k = node.l_child;
node.l_child = k.r_child;
k.r_child = node;
// 重新计算节点高度
node.height = Math.max(height(node.l_child),height(node.r_child)) + 1;
k.height = Math.max(height(k.l_child),height(k.r_child)) + 1;
return k;
}
/**
* 左旋
* @param node
*/
static AvlNode L_Rotate(AvlNode node){
AvlNode k;
k = node.r_child;
node.r_child = k.l_child;
k.l_child = node;
node.height = Math.max(height(node.l_child),height(node.r_child)) + 1;
k.height = Math.max(height(k.l_child),height(k.r_child)) + 1;
return k;
}
/**
* 先右旋再左旋(右左型)
* @param node
*/
static AvlNode R_L_Rotate(AvlNode node){
// 先对其孩子进行右旋
node.r_child = R_Rotate(node.r_child);
// 再对自己进行左旋
return L_Rotate(node);
}
/**
* 先左旋再右旋(左右型)
* @param node
*/
static AvlNode L_R_Rotate(AvlNode node){
node.l_child = L_Rotate(node.l_child);
return R_Rotate(node);
}
/**
* 插入节点(存在节点的情况下)
* @param node
* @return 返回根节点
* 如果节点不存在则创建一个节点并返回,存在则一直递归直至不存在
*/
static AvlNode insert(int data,AvlNode node){
if (node == null){
node = new AvlNode();
node.data = data;
node.l_child = node.r_child = null;
} else if(data < node.data){
// 向左孩子递归插入
node.l_child = insert(data,node.l_child);
// 调整,如果左孩子比右孩子高度大2
if ((height(node.l_child) - height(node.r_child)) == 2){
if (data < node.l_child.data){
// 左左型,直接右旋
node = R_Rotate(node);
} else {
// 左右型
node = L_R_Rotate(node);
}
}
} else if (data > node.data){
// 向右孩子递归插入
node.r_child = insert(data,node.r_child);
// 调整
if ((height(node.r_child) - height(node.l_child)) == 2){
// 右子树高度比左子树高2
if (data > node.r_child.data){
// 右右型,直接左旋
node = L_Rotate(node);
} else {
// 右左型
node = R_L_Rotate(node);
}
}
}
node.height = Math.max(height(node.l_child),height(node.r_child)) + 1;
return node;
}
/**
* 中序遍历
* @param node
*/
static void inorder(AvlNode node){
if (node == null){
return;
}
inorder(node.l_child);
System.out.println(node.data);
inorder(node.r_child);
}
public static void main(String[] args) {
AvlNode root = null;
root = insert(3,root);
root = insert(1,root);
root = insert(2,root);
inorder(root);
}
}
三、红黑树
3.1红黑树的性质和定义?
红黑树相较于前两种树理解起来就比较麻烦了
红黑树是一种含有红黑结点并能自平衡的二叉查找树。性质:
- 根节点是黑色。
- 每个叶子节点都是空节点且都是黑色。
- 每个红色节点的两个子节点一定都是黑色。
- 任一节点到每个叶子节点的路径都包含数量相同的黑节点。
在进行插入和删除等可能会破坏树的平衡的操作时,红黑树可以重新自处理达到平衡状态。
由第三条可以推出如果一个节点存在黑子节点,那么该节点肯定存在两个子节点。
黑色完美平衡: 任一节点到每个叶子节点的路径都包含有相同个数的黑节点。
红黑树的节点定义?
Entry就是TreeMap的内部红黑树实现类,节点由key和value组成。
3.2红黑树自平衡的条件?
三种操作:
- 左旋: 以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图 3。
- 右旋: 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如图 4。
- 变色: 结点的颜色由红变黑或由黑变红。
左旋只改变旋转节点和右子树的结构(父节点以上的结构不变),右旋只改变旋转节点和左子树的结构(父节点以上的结构不变)
3.3红黑树的查找
数据的查找与BST树的查找相同,且不会改变树的结构。
3.4红黑树的插入
插入操作节点的叫法约定:
插入节点应该是什么颜色?
红色!红色在父节点(如果存在)为黑色节点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。
情景一:红黑树为空树 |
情景二:插入节点的key已经存在 |
情景三:插入节点的父节点是黑色 |
情景四:插入节点的父节点是红色 |
-
4.1叔叔节点存在且为红色
祖父节点一定是黑色,因为不可能存在两个相连的红节点。
处理方式:黑红红——》红黑红
如果PP节点的父节点是黑色就不用做任何操作。但如果是红色就需要把PP节点当作新插入节点进行平衡处理。
当PP节点为根节点时,需重新把PP设为黑色,此时就成了黑黑红。 -
4.2叔叔节点不存在或为黑节点,且插入结点的父亲结点是祖父结点的左子结点。
从插入前来看,没有I节点时,如果叔叔节点非红,则叔叔节点必是叶子节点(Nil)(走右子树路径的黑节点比走左子树路径的黑节点多),这种情况下插入节点就需要自平衡了。 -
4.2.1插入节点是其父节点的左子节点
将P设置为黑色,PP设置为红色,右旋。
-
4.2.2插入节点是其父节点的右子节点
-
4.3叔叔节点不存在或为黑节点,且插入结点的父亲结点是祖父结点的右子结点。
-
4.3.1插入节点是其父节点的右子节点
P设为黑色,PP设为红色,直接左旋。 -
4.3.1插入节点是其父节点的左子节点
到此插入的所有情况已经罗列出来。
3.5红黑树的删除
- 查找目标节点
- 删除后自平衡
二叉树删除结点找替代结点有 3 种情景:
- 若删除结点无子结点,直接删除。
- 若删除结点只有一个子结点,用子结点替换删除结点。
- 若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点。
情景一: |
情景二: |
情景三: |
前继节点:删除节点左子树的最右节点
删除节点的前后节点就是前/后继节点
重要思路:
重要思路:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!
情景一二三都可以按照情景三的思路来,比如如果P节点没有右子节点,那么K就是替代节点(情景二),然后把K看作要删除的节点,这时候就找到了M(情景三),M没有左右子节点,此时M就是被删除的节点(情景一)(P的前继节点)
删除前先找到替换节点,然后把替换节点移到删除节点的位置进行覆盖,替换节点处成了空。
删除情景一:替换结点是红色结点。 |
删除情景二:替换结点是黑色结点。 |
黑色节点被删除了得进行自平衡处理。还得考虑替换节点是父节点的左子节点还是右子节点。
删除情景2.1:替换结点是父结点的左子节点。
删除情景2.1.1:替换结点的兄弟节点是红节点。
兄弟结点是红结点,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景。先变色再左旋。旋转后的节点颜色如图。
删除情景2.1.2:替换结点的兄弟节点是黑节点。
此时其父节点和兄弟节点的子节点颜色也无法确认。
删除情景2.1.2.1:替换结点的兄弟节点的右子节点是红节点,左子节点任意颜色
先改变颜色再左旋。
删除情景2.1.2.2:替换结点的兄弟节点的右子节点是黑节点,左子节点为红色
删除情景2.1.2.3:替换结点的兄弟节点的子节点都是黑色
把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。
删除情景2.2:替换结点是父结点的右子节点。
删除情景2.2.1:替换结点的兄弟节点是红节点
删除情景2.2.2:替换结点的兄弟节点是黑节点
删除情景2.2.2.1:替换结点的兄弟节点的左子节点是红节点,右子节点任意颜色
删除情景2.2.2.2:替换结点的兄弟节点的左子节点是黑节点,右子节点为红节点
删除情景2.2.2.3:替换结点的兄弟节点的子节点都是黑节点
将 S 设为红色,把 P 作为新的替换结点,重新进行删除结点情景处理。
综上,红黑树删除后自平衡的处理可以总结为:
- 自己能搞定的自消化(情景 1)
- 自己不能搞定的叫兄弟帮忙(除了情景 1、情景 2.1.2.3 和情景 2.2.2.3)
- 兄弟都帮忙不了的,通过父母,找远方亲戚(情景2.1.2.3和情景2.2.2.3)