1 树和二叉树的定义
1.1 树
树的基本概念
- 根节点:最顶层的节点 - A
- 树的深度/树的高度:树的层数 - 4
- 节点的度:节点拥有的子树个数,就是有几条边 - A节点的度为3
- 叶子节点:度为0的节点 - K,L,F,G,H,I,J
1.2 二叉树
1)二叉树的定义
每个节点最多只有2个子节点的树叫二叉树,这两个节点的顺序是固定的,称为左节点和右节点
2)二叉树的性质
- 若二叉树的高度从1开始,则在二叉树的第n层至多有2^(n-1)个结点
- 高度为n的二叉树最多有 2^(n) - 1 个结点
- 对任何一棵二叉树,如果其度为0(叶子结点)的节点数为n0,度为2的结点数为n2,总度数为k,则:n0 = n2 + 1
- k = n2*2+n1
- k+1 = n2+n1+n0
3)满二叉树和完全二叉树
- 满二叉树:除了叶子节点外,每个节点都有两个子节点,每一层都被完全填充
- 完全二叉树:除了最后一层外,每一层都被完全填充,并且最后一层所有节点保持向左对齐
2 二叉树的遍历
2.1 先序遍历
根左右:8->5->9->6->2->3->4->1->7
public static <E> void preOrder(TreeNode<E> root, List<TreeNode<E>> list) {
list.add(root);
if (root.getLeft() != null) {
preOrder(root.getLeft(), list);
}
if (root.getRight() != null) {
preOrder(root.getRight(), list);
}
}
2.2 中序遍历
左根右:9->5->2->6->8->4->1->3->7
public static <E> void infixOrder(TreeNode<E> root, List<TreeNode<E>> list) {
if (root.getLeft() != null) {
infixOrder(root.getLeft(), list);
}
list.add(root);
if (root.getRight() != null) {
infixOrder(root.getRight(), list);
}
}
2.3 后序遍历
左右根:9->2->6->5->1->4->7->3->8
public static <E> void postOrder(TreeNode<E> root, List<TreeNode<E>> list) {
if (root.getLeft() != null) {
postOrder(root.getLeft(), list);
}
if (root.getRight() != null) {
postOrder(root.getRight(), list);
}
list.add(root);
}
2.4 层序遍历
逐层遍历:8->5->3->9->6->4->7->2->1
public static <E> void levelOrder(TreeNode<E> root, List<TreeNode<E>> data) {
LinkedList<TreeNode<E>> list = new LinkedList<>();
//入队
list.offer(root);
while (!list.isEmpty()) {
//出队
TreeNode<E> node = list.poll();
data.add(node);
if (node.getLeft() != null) {
list.add(node.getLeft());
}
if (node.getRight() != null) {
list.add(node.getRight());
}
}
}
3 二叉搜索树(BST)
3.1 二分查找法
1)二分查找法定义
二分查找也称折半查找(Binary Search),前提是数据结构必须先排好序。二分查找事实上采用的就是一种分治策略,它充分利用了元素间的次序关系,可在最坏的情况下用O(log n)完成搜索任务
2)二分查找法劣势
- 用数组来实现线性排序的数据虽然简单好用,但是插入新元素的时候性能太低,因为插入一个元素,需要将这个元素之后的所有元素后移一位,所以我们不能用一种线性结构将进行排序。
- 其次,有序的数组在使用二分查找的时候,每次查找都要不断计算中间的位置。
3.2 BST定义
- 没有值相等的节点
- 任意节点左子树如果不为空则左子树中节点的值均小于根节点的值,任意节点右子树如果不为空则右子树中节点的值均大于根节点的值
- 基于BST的特点,正常的情况下,查找的时间复杂度都能在对数范围完成 O(log N)
3.3 验证二叉搜索树
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
输入:
2
/ \
1 3
输出: true
输入:
5
/ \
1 4
/ \
3 6
输出: false
1)先序遍历
用先序遍历的思想,自顶向下进行遍历:对每一个节点先判断它的左右子节点是否符合大小关系,再递归判断该节点的左子树和右子树。在递归时还应该把父节点的值传入到下一次递归,该值即为其左右子树的取值范围。
时间复杂度 :在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)
public static <T extends Comparable<T>> boolean isBst(TreeNode<T> root) {
if (root == null) {
return true;
}
return validator(root.getLeft(), null, root.getData())
&& validator(root.getRight(), root.getData(), null);
}
private static <T extends Comparable<T>> boolean validator(TreeNode<T> root, T lower, T upper) {
if (root == null) {
return true;
}
// 根节点比左节点大
if (upper != null && root.getData().compareTo(upper) >= 0) {
return false;
}
// 根节点比右节点小
if (lower != null && root.getData().compareTo(lower) <= 0) {
return false;
}
//递归判断
return validator(root.getLeft(), lower, root.getData())
&& validator(root.getRight(), root.getData(), upper);
}
2)中序遍历
对于二叉搜索树,左子树的节点的值均小于根节点的值,根节点的值均小于右子树的值。因此如果进行中序遍历,得到的序列一定是升序序列。所以我们的判断其实很简单:进行中序遍历,然后判断是否每个值都大于前一个值就可以了,同样时间复杂度也是O(n)
public static <T extends Comparable<T>> boolean isBst(TreeNode<T> root) {
List<TreeNode<T>> list = new ArrayList<>();
//中序遍历得到升序数组
infixOrder(root, list);
List<T> data = list.stream().map(TreeNode::getData).collect(Collectors.toList());
for (int i = 1; i < data.size(); i++) {
if (data.get(i).compareTo(data.get(i - 1)) <= 0) {
return false;
}
}
return true;
}
3.4 局限性
一个二叉搜索树是由n个节点随机构成,所以,对于某些情况,二叉查找树会退化成一个有n个节点的线性链表,查找时间复杂度变为O(n)
4 平衡二叉树(AVL)
4.1 定义
平衡二叉搜索树:简称平衡二叉树。由前苏联的数学家Adelse-Velskil和Landis在1962年提出的高度平衡的二叉树,根据科学家的英文名也称为AVL树。
- 可以是空树
- 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
4.2 验证二叉平衡树
1)自顶向下
自顶向下(类似先序遍历)递归地判断左右子树是否平衡:先分别计算当前节点左右子树的高度,如果高度差不超过 1,那么再递归地分别判断左右子树
public static <T> boolean isBalanced(TreeNode<T> root) {
if (null == root) {
return true;
}
return Math.abs(height(root.getLeft()) - height(root.getRight())) <= 1
&& isBalanced(root.getLeft())
&& isBalanced(root.getRight());
}
public static <T> int height(TreeNode<T> root) {
if (root == null) {
return 0;
}
return Math.max(height(root.getLeft()), height(root.getRight())) + 1;
}
算法分析
- 时间复杂度:O(nlogn),其中 n 是二叉树中的节点个数
- 对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为O(n^2)。
2)自底向上
另一种优化思路是,可以反过来,自底向上地(类似后序遍历)遍历节点进行判断:计算每个节点的高度时,需要递归地处理左右子树,所以可以先判断左右子树是否平衡,计算出左右子树的高度,再判断当前节点是否平衡。这类似于后序遍历的思路。这样,计算高度的方法 height,对于每个节点就只调用一次了。
public static <T> boolean isBalanced(TreeNode<T> root) {
if (root == null) {
return true;
}
int leftHeight = height(root.getLeft());
int rightHeight = height(root.getRight());
return leftHeight != -1 && rightHeight != -1 && Math.abs(leftHeight - rightHeight) <= 1;
}
public static <T> int height(TreeNode<T> root) {
if (root == null) {
return 0;
}
int leftHeight = height(root.getLeft());
int rightHeight = height(root.getRight());
// 如果子树不平衡,直接返回-1
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
// 如果平衡,高度就是左右子树高度最大值,再加1
return Math.max(leftHeight, rightHeight) + 1;
}
算法分析
- 时间复杂度: O(n),其中 n 是二叉树中的节点个数。
- 使用自底向上的递归,每个节点的计算高度和判断是否平衡,都只需要处理一次。最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。
4.3 旋转
- 二叉树的平衡化有两大基础操作: 左旋和右旋。
- 这种旋转在整个平衡化过程中可能进行一次或多次,这两种操作都是从失去平衡的最小子树根结点开始的(即离插入结点最近且平衡因子超过1的祖结点)
- 需要平衡的四种情况:LL型、RR型、LR型、RL型
1)LL
public TreeNode<E> rightRotate(TreeNode<E> y) {
//1、右旋操作
TreeNode<E> x = y.getLeft();
TreeNode<E> t3 = x.getRight();
x.setRight(y);
y.setLeft(t3);
//2、重新计算高度
y.setHeight(TreeUtil.getHeight(y));
x.setHeight(TreeUtil.getHeight(x));
return x;
}
2)RR
public TreeNode<E> leftRotate(TreeNode<E> y) {
//1、左旋操作
TreeNode<E> x = y.getRight();
TreeNode<E> t2 = x.getLeft();
x.setLeft(y);
y.setRight(t2);
//2、重新计算高度
y.setHeight(TreeUtil.getHeight(y));
x.setHeight(TreeUtil.getHeight(x));
return x;
}
3)LR
public TreeNode<E> leftRightRotate(TreeNode<E> y) {
//1、先左旋变成LL型
TreeNode<E> rotate = leftRotate(y.getLeft());
y.setLeft(rotate);
//2、右旋
return rightRotate(y);
}
4)RL
public TreeNode<E> rightLeftRotate(TreeNode<E> y) {
//1、先右旋变成RR型
TreeNode<E> rotate = rightRotate(y.getRight());
y.setRight(rotate);
//2、左旋
return leftRotate(y);
}
4.4 失衡调整
平衡因子:将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子BF(Balance Factor),对于平衡二叉树,BF的取值范围为[-1,1]。如果发现某个节点的BF值不在此范围,则需要对树进行调整(旋转),对于上图平衡二叉树的节点来说:
- 节点50的左子树高度为3,右子树高度为2,BF= 3-2 = 1
- 节点45的左子树高度为2,右子树高度为1,BF= 2-1 = 1
- 节点46的左子树高度为0,右子树高度为0,BF= 0-0 = 0
- 节点65的左子树高度为0,右子树高度为1,BF= 0-1 = -1
而上图非平衡二叉树结点45左子树高度为2,右子树高度为0,BF= 2-0 = 2,不在BF的取值范围内,因此不是平衡二叉树
4.5 完整代码示例
- 必须是一颗二叉查找树,插入的时候保证顺序
- 每个节点插入完成后重新计算高度及计算节点的平衡因子,若不平衡(左右子树高度差不超过1)就自动触发重平衡操作(旋转)
- 删除转换思维:找到待删除结点右子节点最小值或者左节点最大值替换掉待删除结点,把替换的结点删除
package com.tesia.tree;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.Data;
/**
* AVL树
*
* @author linxh
* @date 2021/12/20 10:27
*/
public class AvlTree<E extends Comparable<E>> {
@Data
static class Node<E> {
private E data;
private Node<E> left;
private Node<E> right;
private int height;
public Node() {
}
public Node(E element) {
this.data = element;
}
}
private Node<E> root;
private int size;
public AvlTree() {
size = 0;
}
@Override
public String toString() {
return JSON.toJSONString(this.root, SerializerFeature.PrettyFormat);
}
//-------------------AVL树基本方法-------------------
public int getSize() {
return this.size;
}
public boolean empty() {
return size == 0;
}
/**
* 计算某个节点高度
*
* @param root 节点
* @return int
*/
private int calculateHeight(Node<E> root) {
if (root == null) {
return 0;
}
return Math.max(
calculateHeight(root.getLeft()),
calculateHeight(root.getRight())
) + 1;
}
/**
* 获取AVL树某个节点最小子节点
*
* @param node 节点
* @return Node
*/
private Node<E> minNode(Node<E> node) {
if (node.getLeft() != null) {
return minNode(node.getLeft());
}
return node;
}
/**
* 计算平衡因子,区间[-1,1]为平衡
*
* @param root 节点
* @return int
*/
private int getBalanceFactory(Node<E> root) {
if (root == null) {
return 0;
} else {
return calculateHeight(root.getLeft()) - calculateHeight(root.getRight());
}
}
/**
* 重平衡操作
*
* @param root 节点
* @return TreeNode
*/
public Node<E> reBalance(Node<E> root) {
int factory = this.getBalanceFactory(root);
// LL
if (factory > 1 && getBalanceFactory(root.getLeft()) > 0) {
return rightRotate(root);
}
// LR
else if (factory > 1 && getBalanceFactory(root.getLeft()) <= 0) {
return leftRightRotate(root);
}
// RR
else if (factory < -1 && getBalanceFactory(root.getRight()) <= 0) {
return leftRotate(root);
}
// RL
else if (factory < -1 && getBalanceFactory(root.getRight()) > 0) {
return rightLeftRotate(root);
}
// factory在[-1,1]之前,平衡
else {
return root;
}
}
//-------------------旋转操作-------------------
/**
* LL
*/
public Node<E> rightRotate(Node<E> y) {
//右旋操作
Node<E> x = y.getLeft();
Node<E> t3 = x.getRight();
x.setRight(y);
y.setLeft(t3);
//重新计算高度
y.setHeight(calculateHeight(y));
x.setHeight(calculateHeight(x));
return x;
}
/**
* RR
*/
public Node<E> leftRotate(Node<E> y) {
Node<E> x = y.getRight();
Node<E> t2 = x.getLeft();
x.setLeft(y);
y.setRight(t2);
y.setHeight(calculateHeight(y));
x.setHeight(calculateHeight(x));
return x;
}
/**
* LR
*/
public Node<E> leftRightRotate(Node<E> y) {
//1、先左旋变成LL型
Node<E> rotate = leftRotate(y.getLeft());
y.setLeft(rotate);
//2、右旋
return rightRotate(y);
}
/**
* RL
*/
public Node<E> rightLeftRotate(Node<E> y) {
//1、先右旋变成RR型
Node<E> rotate = rightRotate(y.getRight());
y.setRight(rotate);
//2、左旋
return leftRotate(y);
}
//-------------------增删改查操作------------------
/**
* 查找节点
*
* @param element 要查找的节点元素
* @return Node 结果
*/
public Node<E> getNode(E element) {
return getNode(this.root, element);
}
private Node<E> getNode(Node<E> node, E element) {
if (node != null) {
int i = node.getData().compareTo(element);
if (i == 0) {
return node;
} else if (i > 0) {
return getNode(node.getLeft(), element);
} else {
return getNode(node.getRight(), element);
}
}
return null;
}
/**
* 新增节点
*
* @param element 节点元素
*/
public void put(E element) {
this.root = put(this.root, element);
}
private Node<E> put(Node<E> node, E element) {
if (node == null) {
this.size++;
Node<E> tmpNode = new Node<>(element);
tmpNode.setHeight(1);
return tmpNode;
}
//插入节点比当前节点小,放左边
if (element.compareTo(node.getData()) < 0) {
node.setLeft(put(node.getLeft(), element));
}
//插入节点比当前节点大,放右边
else if (element.compareTo(node.getData()) > 0) {
node.setRight(put(node.getRight(), element));
}
//相等不作处理返回空
else {
throw new RuntimeException("新增失败:该节点已存在");
}
//更新节点高度
node.setHeight(calculateHeight(node));
//触发重平衡操作
return reBalance(node);
}
/**
* 删除节点
*
* @param element 节点元素
*/
public void remove(E element) {
Node<E> node = getNode(this.root, element);
if (node != null) {
this.root = remove(this.root, element);
}
}
private Node<E> remove(Node<E> root, E element) {
if (root == null) {
return null;
}
Node<E> node;
if (element.compareTo(root.getData()) < 0) {
root.setLeft(remove(root.getLeft(), element));
node = root;
} else if (element.compareTo(root.getData()) > 0) {
root.setRight(remove(root.getRight(), element));
node = root;
} else {
//待删除节点的左子树为空
if (root.getLeft() == null) {
Node<E> rootRight = root.getRight();
root.setRight(null);
size--;
node = rootRight;
}
//待删除节点的右子树为空
else if (root.getRight() == null) {
Node<E> rootLeft = root.getLeft();
root.setLeft(null);
size--;
node = rootLeft;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
else {
Node<E> minRoot = minNode(root.getRight());
minRoot.setRight(remove(root.getRight(), minRoot.getData()));
minRoot.setLeft(root.getLeft());
root.setLeft(null);
root.setRight(null);
node = minRoot;
}
}
if (node == null) {
return null;
}
//更新树的高度
node.setHeight(calculateHeight(node));
return reBalance(node);
}
/**
* 更新节点
*
* @param oldElement 旧元素
* @param newElement 新元素
*/
public void update(E oldElement, E newElement) {
remove(oldElement);
put(newElement);
}
}
package com.tesia;
import com.tesia.tools.Console;
import com.tesia.tree.AvlTree;
import org.junit.jupiter.api.Test;
public class AvlTest {
@Test
public void test() {
AvlTree<Integer> avlTree = new AvlTree<>();
Console.log("---------------新增---------------");
avlTree.put(50);
avlTree.put(45);
avlTree.put(65);
avlTree.put(44);
avlTree.put(70);
avlTree.put(43);
Console.log(avlTree.getSize());
Console.log(avlTree);
Console.log("---------------删除---------------");
avlTree.remove(50);
Console.log(avlTree.getSize());
Console.log(avlTree);
Console.log("---------------更新---------------");
avlTree.update(50, 89);
Console.log(avlTree.getSize());
Console.log(avlTree);
}
}
4.6 使用场景
AVL树适合用于插入删除次数比较少,但查找多的情况
5 红黑树
5.1 五个性质
- 节点是红色或黑色。
- 根是黑色。
- 所有叶子都是黑色(叶子是NIL节点,表示无值,任何变量在没有被赋值之前的值都为nil)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
5.2 插入
在插入一个新节点时,默认将它涂为红色(这样可以不违背上性质5),然后进行旋转着色等操作让新的树符合所有规则
因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的性质需要少量O(log n)的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为O(log n)次。以下情形中可验证红黑树的时间复杂度。
1)情形一
新插入节点A位于树根且没有父结点,这种情况直接让插入节点(即根节点)变色为黑色即可
void insert_case1(Node n){
if(n.parent == null){
n.color = BLACK;
}else{
insert_case2(n);
}
}
2)情形二
新插入节点B的父节点A是黑色,这种情况新插入的节点B为红色并没有打破红黑树的规则,无需调整
void insert_case2(Node b){
if(b.parent.color == BLACK){
return;
}else{
insert_case3(b);
}
}
3)情形三
新插入节点D的父节点B和叔叔节点C都是红色,这种情况导致了B、D两个连续红色节点破坏了红黑树规则,需要调整
- 把节点B变为黑色
- B变成黑色后B路径就会多出一个黑色节点,也打破了红黑树规则,因此把A变为红色
- 这时候,结点A和C又成为了连续的红色结点,我们再让结点C变为黑色
- 祖父节点A的父节点是红色的,这就有可能违反了性质4。为了解决这个问题,我们在祖父节点A上递归地进行情形一的整个过程。
void insert_case3(Node d){
if(uncle(d) != null && uncle(d).color == RED){
d.parent.color = BLACK;//B
uncle(d).color = BLACK;//C
grandParent(d).color = RED; //A
insert_case1(grandParent(n)); //递归A
}else{
insert_case4(d);
}
}
Node grandParent(Node n){
return n.parent.parent;
}
Node uncle(Node n){
if(n.parent == grandParent(n).left){
return grandParent(n).right;
}else{
return grandParent(n).left;
}
}
4)情形四
-
新插入节点D的父节点B是红色,叔叔节点C是黑色或者没有叔叔节点,且新节点是父节点B的右子节点,父节点B是祖父节点A的左孩子
-
我们以节点B为轴,做一次左旋转,使得新节点D成为父结点,原来的父节点B成为D的左孩子
-
然后就进入情形五
-
void insert_case4(Node d){
if(d == d.parent.right && d.parent == grandParent(d).left){
rotateLeft(d);
// 这时候把B传入情形五而不是D
d = d.left;
}
//情形四的镜像
else if(d == d.parent.left && d.parent == grandPaent(d).right){
rotateRight(d);
d = d.right;
}
insert_case5(d);
}
5)情形五
新插入节点D的父节点B是红色,叔叔节点C是黑色或者没有叔叔节点,且新节点D是父节点B的左子节点,父节点B是祖父节点A的左孩子
-
以A为轴右旋一次,使得B变为这棵树的根节点,A节点变为B的右子节点
-
然后让结点B变为黑色,结点A变为红色
void insert_case5(Node d){
d.parent.color = BLACK;
grandParent(d).color = RED;
if(d == d.parent.left && d.parent == grantParent(d).left){
rotateRight(d.parent);
}else{
//d == d.parent.right && d.parent == d.grandParent(d).right
rotateLeft;
}
}
5.3 删除
如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题(这里所指的儿子,为非叶子节点的儿子)。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们要么找到它左子树中的最大元素、要么找到它右子树中的最小元素,并把它的值转移到要删除的节点中。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子的儿子。**因为只是复制了一个值(没有复制颜色),不违反任何性质,这就把问题简化为如何删除最多有一个儿子的节点的问题。**它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
我们只需要讨论删除只有一个儿子的节点(如果它两个儿子都为空,即均为叶子,我们任意将其中一个看作它的儿子)。如果我们删除一个红色节点(此时该节点的儿子将都为叶子节点),它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它,并不会破坏性质3和性质4。通过被删除节点的所有路径只是少了一个红色节点,这样可以继续保证性质5。另一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会破坏性质5,但是如果我们重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持性质5。
需要进一步讨论的是在要删除的节点和它的儿子二者都是黑色的时候,这是一种复杂的情况(这种情况下该节点的两个儿子都是叶子节点,否则若其中一个儿子是黑色非叶子节点,另一个儿子是叶子节点,那么从该节点通过非叶子节点儿子的路径上的黑色节点数最小为2,而从该节点到另一个叶子节点儿子的路径上的黑色节点数为1,违反了性质5)。我们首先把要删除的节点替换为它的儿子。出于方便,称呼这个儿子为N(在新的位置上),称呼它的兄弟(它父亲的另一个儿子)为S。在下面的示意图中,我们还是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。
5.3.1 两种简单情形
如果我们删除一个红色节点(此时该节点的儿子将都为叶子节点),它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它,并不会破坏性质3和性质4。通过被删除节点的所有路径只是少了一个红色节点,这样可以继续保证性质5。
另一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会破坏性质5,但是如果我们重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持性质5
5.3.2 情形一
注意:能进到情形一二三四五六,说明被删除的节点是黑色节点、替换被删除节点的孩子节点也是黑色节点,才会有以下的情形
n是新的根节点,在这种情形下,我们就做完了。我们从所有路径去除了一个黑色节点,而新根是黑色的,所以性质都保持着。
void deleteCase1(Node n){
if(n.parent != null){
deleteCase2(n);
}
}
5.3.4 情形二
N的父亲P、S和S的儿子都是黑色的。在这种情形下,我们简单的重绘S为红色。结果是通过S的所有路径,它们就是以前不通过N的那些路径,都少了一个黑色节点。因为删除N的初始的父亲使通过N的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过P的所有路径现在比不通过P的路径少了一个黑色节点,所以仍然违反性质5。要修正这个问题,我们要从**deleteCase1§**开始,在P上做重新平衡处理
void deleteCase2(Node n){
Node s = brother(n);
if( n.parent.color = BLACK
&& s.color == BLACK
&& s.left.color == BLACK
&& s.right.color = BLACK){
s.color = RED;
deleteCase1(n.parent);
}else{
deleteCase3(n);
}
}
Node brother(Node n){
if(n == n.parent.left){
return n.parent.right;
}else{
return n.parent.left;
}
}
5.3.3 情形三
S是红色。在这种情形下我们在N的父亲上做左旋转,把红色兄弟转换成N的祖父,我们接着对调N的父亲和祖父的颜色。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在N有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色因为它是红色S的一个儿子),所以我们可以接下去按情形四、五、六来处理
void deleteCase3(Node n){
Node s = brother(n);
if(s.color == RED){
n.parent.color = RED;
s.color = BLACK;
if(n == n.parent.left){
rotateLeft(n.parent);
}else{
//镜像
rotateRight(n.parent);
}
}
deleteCase4(n);
}
5.4.5 情形四
S和S的儿子都是黑色(S的儿子们必须是黑色,因为需要把S染为红色),但是N的父亲是红色。在这种情形下,我们简单的交换N的兄弟和父亲的颜色。这不影响不通过N的路径的黑色节点的数目,但是它在通过N的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点。
void deleteCase4(Node n){
Node s = brother(n);
if(n.parent.color == RED
&& s.color == BLACK
&& s.left.color == BLACK
&& s.right.color == BLACK){
s.color = RED;
n.parent.color = BLACK;
}else{
deleteCase5(n);
}
}
5.4.6 情形五
S是黑色,S的左儿子是红色,S的右儿子是黑色,而N是它父亲的左儿子。在这种情形下我们在S上做右旋转,这样S的左儿子成为S的父亲和N的新兄弟。我们接着交换S和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在N有了一个黑色兄弟,他的右儿子是红色的,所以我们进入了情形6。N和它的父亲都不受这个变换的影响
void deleteCase5(Node n){
Node s = brother(n);
if(s.color == BLACK){
if(n == n.parent.left && s.right.color == BLACK && s.left.color == RED){
s.color = RED;
s.left.color = BLACK;
rotateRight(s);
}else if(n == n.parent.right && s.left.color == BLACK && s.right.color ==RED){
s.color = RED;
s.right.color = BLACK;
rotateLeft(s);
}
}
deleteCase6(n);
}
5.4.7 情形六
S是黑色,S的右儿子是红色,而N是它父亲的左儿子。在这种情形下我们在N的父亲上做左旋转,这样S成为N的父亲(P)和S的右儿子的父亲。我们接着交换N的父亲和S的颜色,并使S的右儿子为黑色。子树在它的根上的仍是同样的颜色,所以性质3没有被违反。但是,N现在增加了一个黑色祖先:要么N的父亲变成黑色,要么它是黑色而S被增加为一个黑色祖父。所以,通过N的路径都增加了一个黑色节点。
此时,如果一个路径不通过N,则有两种可能性:
- 它通过N的新兄弟。那么它以前和现在都必定通过S和N的父亲,而它们只是交换了颜色。所以路径保持了同样数目的黑色节点。
- 它通过N的新叔父,S的右儿子。那么它以前通过S、S的父亲和S的右儿子,但是现在只通过S,它被假定为它以前的父亲的颜色,和S的右儿子,它被从红色改变为黑色。合成效果是这个路径通过了同样数目的黑色节点。
在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了性质4。在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SA0LhGg-1648563825697)(.\img\image-20220309155547352.png)]
void deleteCase6(Node n){
Node s = brother(n);
s.color = n.parent.color;
n.parent.color = BLACK;
if(n == n.parent.left){
s.right.color = BLACK;
rotateLeft(n.parent);
}else{
s.left.color = BLACK;
rotateRight(n.parent);
}
}
5.4 代码完整示例
package com.tesia.tree;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ValueFilter;
import lombok.Data;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/**
* 红黑树
*
* @author linxh
* @date 2022/3/10 10:36
*/
@Data
public class RbTree<E extends Comparable<E>> {
private Node<E> root;
private int size;
private static final boolean RED = false;
private static final boolean BLACK = true;
private static ValueFilter valueFilter = (o, s, o1) -> {
if (StringUtils.equals("color", s)) {
boolean flag = (boolean) o1;
return flag ? "black" : "red";
}
return o1;
};
@Override
public String toString() {
return JSON.toJSONString(this.root, valueFilter, SerializerFeature.PrettyFormat);
}
@Getter
static class Node<E> {
private E data;
private boolean color;
private Node<E> left;
private Node<E> right;
private Node<E> parent;
// NIL
public Node() {
this.color = BLACK;
}
public Node(E data) {
this.data = data;
this.color = RED;
}
private Node<E> grandParent() {
if (parent == null) {
return null;
}
return parent.parent;
}
private Node<E> uncle() {
if (grandParent() == null) {
return null;
}
if (parent == grandParent().left) {
return grandParent().right;
} else {
return grandParent().left;
}
}
private Node<E> brother() {
if (parent == null) {
return null;
} else if (parent.left == this) {
return parent.right;
} else {
return parent.left;
}
}
}
private boolean isNil(Node<E> node) {
if (node == null) {
return false;
}
return node.left == null && node.right == null && node.color == BLACK;
}
/**
* 右旋
*/
private void rightRotate(Node<E> p) {
Node<E> gp = p.grandParent();
// 以fa作为轴点旋转
Node<E> fa = p.parent;
Node<E> y = p.right;
fa.left = y;
if (y != null) {
y.parent = fa;
}
p.right = fa;
fa.parent = p;
if (root == fa) {
root = p;
}
p.parent = gp;
if (gp != null) {
if (gp.left == fa) {
gp.left = p;
} else {
gp.right = p;
}
}
}
/**
* 左旋
*/
private void leftRotate(Node<E> p) {
if (p.parent == null) {
root = p;
return;
}
Node<E> gp = p.grandParent();
// 以fa作为轴点旋转
Node<E> fa = p.parent;
Node<E> y = p.left;
fa.right = y;
if (y != null) {
y.parent = fa;
}
p.left = fa;
fa.parent = p;
if (root == fa) {
root = p;
}
p.parent = gp;
if (gp != null) {
if (gp.left == fa) {
gp.left = p;
} else {
gp.right = p;
}
}
}
/**
* 获取该节点右子节点最小节点
*/
private Node<E> getSmallestChild(Node<E> p) {
if (!isNil(p.left)) {
return getSmallestChild(p.getLeft());
}
return p;
}
public void insert(E element) {
//情形一,根节点
if (this.root == null) {
root = new Node<>();
root.color = BLACK;
// root.left = root.right = NIL
root.left = new Node<>();
root.right = new Node<>();
root.data = element;
} else {
insert(this.root, element);
}
this.size++;
}
private void insert(Node<E> node, E element) {
int cmp = node.data.compareTo(element);
if (cmp > 0) {
if (!isNil(node.left)) {
insert(node.left, element);
} else {
Node<E> tmp = new Node<>(element);
// NIL
tmp.left = new Node<>();
tmp.right = new Node<>();
tmp.parent = node;
node.left = tmp;
insertCase(tmp);
}
} else {
if (!isNil(node.right)) {
insert(node.right, element);
} else {
Node<E> tmp = new Node<>(element);
tmp.left = new Node<>();
tmp.right = new Node<>();
tmp.parent = node;
node.right = tmp;
insertCase(tmp);
}
}
}
private void insertCase(Node<E> p) {
if (p.parent == null) {
root = p;
p.color = BLACK;
return;
}
// 情形二,p的父节点是黑色,插入红色节点P没有打破规则,不做处理,直接处理父节点为红色的情况
if (p.parent.color == RED) {
//情形三,p的父节点是红色,叔叔节点是红色
Node<E> uncle = p.uncle();
if (uncle != null && uncle.color == RED) {
p.parent.color = BLACK;
uncle.color = BLACK;
p.grandParent().color = RED;
// 对gp节点进行处理
insertCase(p.grandParent());
} else {
//情形四
if (p.parent.right == p && p.grandParent().left == p.parent) {
leftRotate(p);
p.color = BLACK;
p.parent.color = RED;
rightRotate(p);
} else if (p.parent.left == p && p.grandParent().right == p.parent) {
rightRotate(p);
p.color = BLACK;
p.parent.color = RED;
leftRotate(p);
}
// 情形五
else if (p.parent.left == p && p.grandParent().left == p.parent) {
p.parent.color = BLACK;
p.grandParent().color = RED;
rightRotate(p.parent);
} else if (p.parent.right == p && p.grandParent().right == p.parent) {
p.parent.color = BLACK;
p.grandParent().color = RED;
leftRotate(p.parent);
}
}
}
}
public boolean delete(E element) {
boolean deleteChild = deleteChild(root, element);
if (deleteChild) {
size--;
}
return deleteChild;
}
private boolean deleteChild(Node<E> n, E element) {
int cmp = n.data.compareTo(element);
if (cmp > 0) {
if (isNil(n.left)) {
return false;
}
return deleteChild(n.left, element);
} else if (cmp < 0) {
if (isNil(n.right)) {
return false;
}
return deleteChild(n.right, element);
} else {
// 如果右子树为空,即最多只有一个左子树或没有,这时候直接传入root结点在deleteOneChild方法中用孩子结点直接替换
if (isNil(n.right)) {
deleteOneChild(n);
return true;
}
// 找到右子节点中的最小值并替换要是删除的结点值,转化为删除smallest结点
Node<E> smallest = getSmallestChild(n.getRight());
n.data = smallest.data;
deleteOneChild(smallest);
return true;
}
}
private void deleteOneChild(Node<E> n) {
Node<E> child = isNil(n.left) ? n.right : n.left;
// n叶子节点
if (n.parent == null && isNil(n.left) && isNil(n.right)) {
n = null;
root = n;
return;
}
// n为非叶子结点,仅有左子节点
if (n.parent == null) {
child.parent = null;
root = child;
root.color = BLACK;
return;
}
// 删除n结点
if (n.parent.left == n) {
n.parent.left = child;
} else {
n.parent.right = child;
}
child.parent = n.parent;
//重平衡操作
if (n.color == BLACK) {
//两种简单情形2=》删除的节点是黑色而他的子节点是红色
if (child.color == RED) {
child.color = BLACK;
}
//复杂情况1、2、3、4、5、6
else {
deleteCase(child);
}
}
//两种简单情形1=》删除的节点是红色
// else { 不做处理 }
}
private void deleteCase(Node<E> n) {
//情形一
if (n.parent == null) {
n.color = BLACK;
return;
}
//情形三
if (n.brother().color == RED) {
n.parent.color = RED;
n.brother().color = BLACK;
if (n == n.parent.left) {
// leftRotate(p.parent);
leftRotate(n.brother());
} else {
rightRotate(n.brother());
// rightRotate(p.parent);
}
}
// 情形二
if (n.parent.color == BLACK && n.brother().color == BLACK
&& n.brother().left.color == BLACK && n.brother().right.color == BLACK) {
n.brother().color = RED;
deleteCase(n.parent);
}
// 情形四
else if (n.parent.color == RED && n.brother().color == BLACK
&& n.brother().left.color == BLACK && n.brother().right.color == BLACK) {
n.brother().color = RED;
n.parent.color = BLACK;
} else {
if (n.brother().color == BLACK) {
// 情形五
if (n == n.parent.left && n.brother().left.color == RED && n.brother().right.color == BLACK) {
n.brother().color = RED;
n.brother().left.color = BLACK;
rightRotate(n.brother().left);
} else if (n == n.parent.right && n.brother().left.color == BLACK && n.brother().right.color == RED) {
n.brother().color = RED;
n.brother().right.color = BLACK;
leftRotate(n.brother().right);
}
}
// 情形六
n.brother().color = n.parent.color;
n.parent.color = BLACK;
if (n == n.parent.left) {
n.brother().right.color = BLACK;
leftRotate(n.brother());
} else {
n.brother().left.color = BLACK;
rightRotate(n.brother());
}
}
}
}
package com.tesia;
import com.tesia.tools.Console;
import com.tesia.tree.RbTree;
import org.junit.jupiter.api.Test;
public class RbTreeTest {
@Test
public void test() {
RbTree<Integer> rbTree = new RbTree<>();
rbTree.insert(7);
rbTree.insert(5);
rbTree.insert(3);
rbTree.insert(8);
rbTree.insert(4);
rbTree.insert(9);
rbTree.insert(2);
rbTree.insert(1);
rbTree.insert(0);
rbTree.insert(12);
rbTree.insert(10);
rbTree.insert(-1);
rbTree.insert(15);
rbTree.insert(14);
rbTree.insert(18);
rbTree.insert(20);
Console.log(rbTree.getSize());
Console.log(rbTree);
boolean delete = rbTree.delete(12);
String result = delete ? "删除成功" : "删除失败";
Console.log(result + ",剩下元素:" + rbTree.getSize());
Console.log(rbTree);
}
}
5.5 总结
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。
红黑树多用于搜索、插入、删除操作多的情况下。
5.6 参考文章
维基百科-红黑树,博主红黑树的学习就是借鉴这篇文章的