30张图带你弄懂 二叉树、AVL、红黑树,他们之间有什么联系,AVL树和红黑树如何平衡

树(Tree)是若干个结点组成的有限集合,其中必须有一个结点是根结点,其余结点划分为若干个互不相交的集合,每一个集合还是一棵树,但被称为根的子树。注意,当树的结点个数为0时,我们称这棵树为空树,记为Φ。

二叉树是树的其中一种。二叉树(Binary Tree)是一种每结点最多拥有2个子树的树结构,其中第1个子树被称为左子树,第2个子树被称为右子树。注意,当二叉树的结点个数为0时,我们称这个二叉树为空二叉树,记为Φ。

二叉树是有序的,即若将其左、右子树颠倒,就成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树,还是右子树。因此二叉树具有五种基本形态,如下图:

二叉树具有以下特点:
(1)每个节点的度最大为2(最多拥有2棵子树)
(2)左子树和右子树是顺序的.
(3)即使某节点只有一棵树,也要区分左右子树.

二叉树的存储分为顺序存储结构和链式存储结构。

(1)顺序存储结构
二叉树的顺序存储是用一组连续的存储单元存放二叉树中的结点。一般是按照二叉树结点从上至下、从左到右的顺序存储。完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。例如下图的二叉树的顺序存储
在这里插入图片描述
data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或null表示)。

三叉链表存储每个结点由四个域组成,如下图所示:

在这里插入图片描述

data、lchild以及rchild三个域的意义同二叉链表结构,parent域为指向该结点双亲结点的指针。这种存储结构既便于查找孩子结点,又便于查找双亲结点,但是,相对于二叉链表存储结构而言,它增加了空间开销。

(2)链式存储结构
二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常有两种形式:二叉链表存储和三叉链表存储

二叉链表中每个结点由。三个域组成,除了数据域外,还有两个指针域,分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。
在这里插入图片描述

二叉搜索树(Binary Search Tree ,BST)

二叉查找树(BST)是二叉树的一种,是应用非常广泛的一种二叉树,简称BST.又被称为二叉查找树或二叉排序树。二叉查找树只要不是空二叉树,则具有如下特征:
(1)若它的左子树不空,则左子树上所有结点的值均小于根结点的值。
(2)若它的右子树不空,则右子树上所有结点的值均大于根结点的值。
(3)它的左、右子树也分别是二叉排序树。

二叉查找树的数据元素可采用键值对(key-value)的形式存储,每个数据元素构成一个二叉查找树的结点。每个结点由键、值value、左子结点和右子结点等组成,

二叉搜索树基本操作

添加结点

插入新结点的步骤如下:
(1)判断插入的元素是否为null ,若为null就抛出异常并结束插入;
(2)判断是否有根结点,若没有根结点,把待插入的元素创建一个新结点,并设置为根结点;同时树结点个数加1; 插入成功后返回;
(3)若已有根结点,就通过循环查找待插入元素的结点的父节点。 通过结点的值与待插入值进行比较, 由于二叉树的右子结点的值比根结点值大,左子节点的值比根结点值小的特点,判断出父结点应该在哪里。如果待插入元素的值已经与树中某一结点的值相同,就直接覆盖。
(4)找出父结点后,然后通过比较判断待插入结点的值比父结点值大还是小。如果比父结点值小,就作为父结点的左子结点插入,如果比父结点值大,就作为父结点的右子节点插入。同时树结点个数加1. 插入成功并返回。

插入逻辑如下图所示:
在这里插入图片描述

插入代码如下:

public void add(E element) {
        //非空检测
        if (element == null) {
            throw new IllegalArgumentException("element must not be null");
        }
        //添加第一个结点
        if (root == null) {
            root = createNode(element,null);
            size++;
            return;
        }
        //添加的不是第一个结点
        
        //用来标记移动的结点
        Node<E> node = root;
        //保存当前结点的父结点,默认根结点就是父结点
        Node<E> parent = root;
        //根据比较规则找到待添加元素的位置
        int cmp = 0;
        do {
            //比较值
            cmp = compare(element, node.element);
            //保存当前结点的父结点
            parent = node;
            if (cmp > 0) {
                node = ((Node<E>) node).right;
            } else if (cmp < 0) {
                node = ((Node<E>) node).left;
            } else {
                //若遇到值相等的元素建议覆盖旧的值
                ((Node<E>) node).element = element;
                return;
            }
        } while (node != null);
        //创建新节点.并判断是插入到哪里
        Node<E> newNode =createNode(element,parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        
    }

删除结点

删除结点的步骤如下:
(1) 判断需要删除的节点是否为null ,若为null抛出异常并返回;
(2)判断需要删除的节点度是否为2, 如果度为2就需要找到后继结点。寻找后继结点具体操作是先找到待删除结点的右结点,如果右结点存在,然后循环查找右结点的左结点,直到无左结点,然后返回无左结点的结点。如果右结点不存在的话,通过循环判断待删除的节点不为空并且待删除节点为父节点的右结点,就返回待删除节点的祖父节点(node.parent.parent),把后继节点放在待删除节点的位置。
(3) 判断后继结点或者待删除结点(结点度为1或者0的情况)是否有左右子树,如果有左结点,替换待删除结点的结点就取左结点,否则就取右结点。
(4)如果替换待删除结点的结点度为1,就把替换待删除结点的结点的父结点为待删除结点或后继结点的父节点。如果为待删除结点或后继结点的父节点是根结点,就把替换待删除结点的结点设置为根结点,如果待删除结点为父节点的左结点,就设置待删除结点的父结点的左结点为替换结点。如果待删除结点为父节点的右结点,就设置待删除结点的父结点的右结点为替换结点。 (5)如果替换待删除结点的结点是叶子结点(度为0)并且是根结点,设置根结点为null。
(6)如替换待删除结点的结仅是叶子结点(度为0),如果后继结点是父结点的左结点,设置父结点的左结点为null;如果后继结点是父结点的右结点,就设置父结点右结点为null;
(7)二叉树的结点减少1,并返回删除成功。

删除节点的流程图如下:

在这里插入图片描述

代码实现:

private void remove(Node<E> node) {
        if (node == null) return;
        size--;
        if (node.hasTwoChildren()) { // 度为2的节点
            // 找到后继节点
            Node<E> s = successor(node);
            // 用后继节点的值覆盖度为2的节点的值
            node.element = s.element;
            // 删除后继节点
            node = s;
        }
        // 删除node节点(node的度必然是1或者0)
        Node<E> replacement = node.left != null ? node.left : node.right;

        if (replacement != null) {
            // node是度为1的节点
            // 更改parent
            replacement.parent = node.parent;
            // 更改parent的left、right的指向
            if (node.parent == null) { // node是度为1的节点并且是根节点
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else { // node == node.parent.right
                node.parent.right = replacement;
            }

           
        } else if (node.parent == null) { // node是叶子节点并且是根节点
            root = null;

            // 删除节点之后的处理
            afterRemove(node);
        } else { // node是叶子节点,但不是根节点
            if (node == node.parent.left) {
                node.parent.left = null;
            } else { // node == node.parent.right
                node.parent.right = null;
            }
            
        }
    }
    /**
     * 获取后继结点
     * @param node
     * @return
     */    
    protected Node<E> successor(Node<E> node) {
        if (node == null) return null;

        // 前驱节点在左子树当中(right.left.left.left....)
        Node<E> p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }
        // 从父节点、祖父节点中寻找前驱节点
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }

        return node.parent;
    }

遍历
遍历是数据结构中的常见操作,就是把所有元素都访问一遍。二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,是每个结点被访问一次且仅被访问一次。

根据节点访问顺序的不同,二叉树的常见遍历方式有4钟:

  • 前序遍历
    访问顺序:根节点、左子树、右子树
    在这里插入图片描述

    采用递归方式遍历的代码如下:

 public  void preorderTraversal(Node<E> node){
        if(node == null ){
            return;
        }
        System.out.print(element + " ");
        preorderTraversal(node.left);
        preorderTraversal(node.right);
 }

采用非递归方式(栈)遍历代码操作步骤如下:
(1)先将根节点入栈,然后弹出(访问根节点),
(2)将其右结点入栈,再将其左结点入栈(栈是先进后出,我们需要先访问左结点,所以先将右结点入栈),
(3)弹出左结点,对左结点也进行同样的操作(右结点先入栈,左结点入栈),直至栈为空并且访问完了所有结点。

具体实现如下:

  public void preorderTraversalByStack(Node<E> popNode) {
        if (popNode == null ) {
            return;
        }
        SingleLinkedList<E> linkedList = new SingleLinkedList<E>();
        Stack<Node<E>> stack = new Stack<>();

        Node<E> node = popNode;
        stack.push(popNode);
        while (!stack.isEmpty()) {
            node = stack.pop();
            linkedList.add(node.element);
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }
        linkedList.toString();
    }

  • 中序遍历
    访问顺序: 左子树、根结点、右子树

在这里插入图片描述

递归方式遍历实现代码:

      public  void inorderTraversal(Node<E> node){
            if(node == null){
                return;
            }
            inorderTraversal(node.left,visitor);
            System.out.print(element + " ");
            inorderTraversal(node.right);
        }

非递归实现步骤:从根节点依次将左结点入栈,当没有左结点的时候,弹出栈顶元素,将其写入list,并将其右结点入栈。重复上述操作。与常规方法相比,省去了查看左子树是否被访问过的步骤,对每个结点,都是先遍历其左子树,所以当访问到该结点的时候,可以确保其左子树已经被访问过了,只需要访问其本身和其右结点即可。

具体实现如下:

       public  void inorderTraversalByStack(Node<E>  popNode){
            if(popNode == null ){
                return;
            }
            Stack<Node<E>> stack = new Stack<>();
           Node<E> node = popNode;
           SingleLinkedList<E> linkedList = new SingleLinkedList<>();
           // stack.push(node);
            while(node !=null || !stack.isEmpty() ){
              if (node != null){
                  stack.push(node);
                  node = node.left;
              }else{
                  node = stack.pop();
                  linkedList.add(node.element);
                  node = node.right;
              }
            }
            linkedList.toString();
        }
  • 后续遍历
    遍历顺序:左子树、右子树、根结点
    在这里插入图片描述

    采用递归方式的后续遍历代码实现如下:

public  void postorderTraversal(Node<E> node){
	if(node == null ){
		return;
	}
	postorderTraversal(node.left,visitor);
	postorderTraversal(node.right,visitor);
	System.out.print(element + " ");
   
}

非递归实现步骤:将根节点左子树入栈,当栈顶结点是叶子结点或者栈顶结点的右结点是上一次pop的结点,出栈。如果栈顶结点的左结点是上一次pop的结点,如果栈顶结点的右结点存在的情况,就把栈顶结点的右结点添加到栈中,如果如果栈顶结点的右结点不存在的情况,即栈顶结点出站。 如果栈顶结点的左结点不是上一次pop的结点,就把栈顶结点的左结点添加到栈顶。具体代码实现如下:

public  void postorderTraversalByStack(Node<E> popNode){
	if(popNode == null){
		return;
	}
	Stack<Node<E>> stack = new Stack<>();
	SingleLinkedList<E> linkedList = new SingleLinkedList<>();
	Node<E> node = popNode;

	// 栈顶结点
	Node<E> peek;
	//上次访问的结点
	Node<E> prev = null;
	stack.push(node);

	while( !stack.isEmpty()){
		//获取栈顶结点
		peek = stack.peek();
		//如果栈顶结点是叶子结点或者栈顶结点的右结点是上一次pop的节点,就pop 出栈顶元素
		if (peek.isLeaf() || peek.right == prev) {
			node = stack.pop();
			linkedList.add(node.element);
			prev = node;

		}else  {
			//如果栈顶结点的左结点 是上一次pop的结点,如果栈顶结点的右结点存在,就push,如果不存在就pop 出栈顶结点
			if (peek.left == prev){
				if (peek.right!=null){
					stack.push(peek.right);
				}else{
					node = stack.pop();
					linkedList.add(node.element);
					prev = node;
				}
			}else {
				//如果栈顶结点的左结点不是上一次pop的结点,即把栈顶结点的左结点push入栈
				stack.push(peek.left);
				node = peek.left;
			}
		}
	}

	linkedList.toString();
}

  • 层序遍历

实现步骤:将二叉树按层输出,借助队列实现 将根结点入队, 然后循环到队列为空结束。 在循环代码中,先将队头结点出队,然后将队头结点的左结点和右结点分别按顺序放入队列中,
在这里插入图片描述

具体代码实现如下:

public  void levelorderTraversal(){
	if( root == null) {
		throw new IllegalArgumentException("Visitor不能为空");
	}
	Queue<Node<E>> linkedList = new LinkedList<>();
	SingleLinkedList singleLinkedList = new SingleLinkedList();
	//将根结点入队
	linkedList.offer(root);
	while (!linkedList.isEmpty()){
		//队头元素出队
		Node<E> node = linkedList.poll();
		singleLinkedList.add(node.element);
		//回调,将处理遍历数据的业务交给调用者,如果返回true停止遍历
		if(node.left !=null) {
			linkedList.offer(node.left);
		}
		if(node.right !=null) {
			linkedList.offer(node.right);
		}
	}
	singleLinkedList.toString();
}

平衡二叉查找树(Balanced Binary Search Tree ,BBST)

二叉查找树在不断插入的时候,有可能出现这样一种情况:很容易“退化”成链表,如果bst 树的节点正好从大到小的插入,此时树的结构也类似于链表结构,这时候的查询或写入耗时与链表相同。

退化成链表的二叉查找树如下图所示:
在这里插入图片描述

为了避免这种退化情况发生,引入了平衡二叉树,平衡二叉树又叫自平衡的二叉搜索树;经典常见的平衡二叉树有AVL和红黑树。AVL 树在Windows NT 内核中广泛应用; 红黑树在java的TreeMap、TreeSet、HashMap、HashSet、Linux的进程调度、Nginx 的timer管理中应用。

AVL和红黑树都是通过本身的建树原则来控制树的层数和节点位置。他们直接的关系如下:
在这里插入图片描述

在使用平衡二叉树解决二叉查找树退化问题之前,我们先来看看什么是平衡;

平衡就是当结点数量固定时,左右子树的高度余越接近,这棵二叉树就越平衡,如下图所示:
在这里插入图片描述

最理想的平衡就是像完全二叉树、满二叉树那样,高度是最小的。

AVL

AVL树是最早发明的自平衡二叉树之一,AVL取名与两位发名字的名字G.M.Adelson-Velsky 和E.M.Landis。

AVL树本质上还是一颗二叉查找树,它有以下特性:
(1)对于任何一颗子树的root根结点而言,它的左子树任何节点的key一定比root小,而右子树任何节点的key 一定比root大;
(2)对于AVL树而言,其中任何子树仍然是AVL树;
(3)每个节点的左右子节点的高度之差的绝对值最多为1;

在插入、删除树节点的时候,如果破坏了以上的原则,AVL树会自动进行调整使得以上三条原则仍然成立。

AVL树通过结点的旋转进行平衡的过程可以分为左旋和右旋。在旋转之前,首先确定旋转支点(pivot),旋转支点指的是失去平衡部分的树,是自平衡之后的根节点。平衡的调整过程,需要根据pivot它来进行旋转。事实上,AVL树的旋转有规律可循的,因为只要聚焦到失衡子树,然后进行左旋、右旋即可。左旋就是逆时针转,右旋是顺时针转。

AVL子树失衡处理

新增节点和删除节点会导致AVL树失衡,需要再平衡。导致AVL失衡的场景有以下4个:
(1)左左结构失衡(LL型失衡)
(2)右右结构失衡(RR型失衡)
(3)左右结构失衡(LR型失衡)
(4)右左结构失衡(RL型失衡)

场景1:LL型失衡-左左结构失衡(右旋)

在平衡二叉查找树的左子树的左叶子结点中插入新结点,导致root左子树不平衡。此时,以root的左儿为支点,也就是,左侧的不平衡元素为pivot(支点), 进行右旋。右旋过程中,如果pivot有右子树,则作为 原root的 左子树, 保障AVL的特性1如下图:
在这里插入图片描述

场景2:RR型失衡:右右结构失衡(左旋)
在平衡二叉查找树的右子树的右叶子结点中插入新结点,导致root右子树树不平衡。此时,以root的右儿为支点,也就是,右侧的不平衡元素 为 pivot(支点), 进行左旋。左旋过程中,如果pivot有左子树,则作为 原root的 右子树,
在这里插入图片描述

场景3:LR型失衡:左右结构失衡(左旋+右旋)
在平衡二叉查找树的左子树的右叶子结点中插入新结点,导致root左子树不平衡。

在这里插入图片描述

场景4:RL失衡: 右左结构 (右旋+左旋)
在平衡二叉查找树的右子树的左叶子结点中插入新结点,导致root右子树树不平衡
在这里插入图片描述

AVL树 在添加结点和删除的时候,先判断AVL树是否平衡,若不平衡则需要进行自动平衡操作,具体代码如下:

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 = root;
        //根据比较规则找到待添加元素的位置
        int cmp = 0;
        do {
            //比较值
            cmp = compare(element, node.element);
            //保存当前结点的父结点
            parent = node;
            if (cmp > 0) {
                node = ((Node<E>) node).right;
            } else if (cmp < 0) {
                node = ((Node<E>) node).left;
            } else {
                //若遇到值相等的元素建议覆盖旧的值
                ((Node<E>) node).element = element;
                return;
            }
        } while (node != null);

        //创建新节点.并判断是插入到哪里
        Node<E> newNode =createNode(element,parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        // 新添加节点的处理
        afterAdd(newNode);
    }
  protected void afterAdd(Node<E> node) {
        while ((node = node.parent) != null) {
            if (isBalanced(node)) {
                // 更新高度
                updateHeight(node);
            } else {
                // 恢复平衡
                rebalance(node);
                // 整棵树恢复平衡
                break;
            }
        }
    }
     private void rebalance(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
                rotateRight(grand);
            } else { // LR
                rotateLeft(parent);
                rotateRight(grand);
            }
        } else { // R
            if (node.isLeftChild()) { // RL
                rotateRight(parent);
                rotateLeft(grand);
            } else { // RR
                rotateLeft(grand);
            }
        }
    }
    

删除结点与插入结点类似,可自行实现代码。

红黑树(RBTree)

红黑树也是一种自平衡二叉查找树,它与AVL树类似,都在添加和删除的时候通过旋转操作保持二叉树的平衡,以求更高效的查询性能。与AVL树相比,红黑树牺牲了部分平衡性,以换取插入、删除操作时较少的旋转操作,整体来说性能要优于AVL树。

虽然RBTree是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是最高效的。

红黑树是实际应用中最常用的平衡二叉查找树,但它不严格的具有平衡属性,但是平均的使用性能非常良好。

在红黑树中,结点被标记为红色和黑色两种颜色,红黑树的特性有以下几点:
(1)结点非黑即红;(颜色属性)
(2)根结点一定是黑色;(根属性)
(3)叶子结点(NIL)一定是黑色;(叶子属性)
(4)每个红色结点的两个子结点都为黑色。即从每个叶子到根的所有路径上不能有两个连续的红色结点;(红色属性)
(5)从任一结点到其每个叶子的所有路径,都包含相同数目的黑色结点。(黑色属性,平很属性)

红色结点的孩子一定是黑色,但是,RBTree黑色结点的孩子可以是红色或者黑色。如下图所示:

在这里插入图片描述

基于上面的原则,我们一般在插入红黑树结点的时候,会将这个结点设置为红色。因为红色破坏原则的可能性最小,如果是黑色,很可能导致这条支路的黑色结点比其他支路的要多1,破坏了平衡。

RBTree 有点属于空间换时间类型的优化, 在AVL数的节点上,增加了颜色属性的数据,相当于增加了空间的消耗,通过颜色属性的增加换取后面平衡操作的次数减少。

根据红黑树的特性5,红黑树的平衡又称为完美黑色平衡,红黑树的恢复平衡的过程有三个操作:
(1)变色
结点的颜色由红变黑或者由黑变红。

(2)左旋
以某个结点作为支点(pivot),其父节点(子树的root)旋转为自己的左子树(左旋),pivot的原左子树变成 原root节点的 右子树,pivot的原右子树保持不变。

(3)右旋
以某个结点作为支点(pivot),其父节点(子树的root)旋转为自己的右子树(右旋),pivot的原右子树变成 原root节点的 左子树,pivot的原左子树保持不变。

红黑树子树失衡处理

红黑树插入新节点时,首先是找到一个合适的插入点,就是找到插入节点的父节点,由于红黑树 它又满足BST二叉查找树的 有序特性,这个找父节点的操作和二叉查找树是完全一致的。二叉查找树,左子节点小于当前节点,右子节点大于当前节点,然后每一次向下查找一层就可以排除掉一半的数据,查找的效率在log(N)。
最终查找到nil节点或者 key一样的节点。如果最终查找到 key一样的节点,进行更新操作。这个TreeNode.key 与当前 put.key 完全一致。这就
不需要插入,替换value就可以了,父节点就是当前节点的父节点。如果最终查找到nil节点,进行插入操作。nil节点的父节点,就是当前节点的父节点,把插入的节点替换nil节点。然后进行红黑树的 平衡处理。

由于父节点为黑色的概率比较大,插入新节点会设置为红色,可以避免颜色冲突。

插入一个红色的新节点后,会出现一下几种情况。

场景1:红黑树为空
直接把插入的新节点设置为根结点,然后根据红黑树性质2(根节点是黑色)把插入节点设置为黑色。
在这里插入图片描述

场景2:插入节点的Key已经存在
新插入节点的key已经存在,更新已存在的节点值为新插入的节点值,保持原节点的颜色。

在这里插入图片描述

情景3:插入节点的父节点为黑色
由于新插入节点是红色的,当插入节点的父节点是黑色时,不会影响红黑树的平衡。所以直接插入无需平衡。
在这里插入图片描述

情景4:插入节点的父节点为红色
如果新插入节点的父节点为红色节,点根据性质2(根节点是黑色),那么该父节点不可能为根节点,所以新插入节点总是存在祖父节点(grand)(三代关系)。

根据特性4(每个红色节点的两个子节点一定是黑色的,不能有两个红色节点相连),此时会出现两种状态,分别是父节点(parent)和叔父(uncle)节点为红色 、父节点(parent)为红色,叔父(uncle)节点为黑色。

情景4.1:父亲和叔叔为红色节点
根据特性4,两个红色节点不能相连 可以推断出祖父节点(grand)肯定为黑色节点。 父节点为红色,那么此时该插入子树的红黑树层数的情况是:黑红红,因为不可能同时存在两个相连的红色节点,需要对新插入的节点进行变色操作, 处理方式是 黑红红==>红黑红.

具体操作是:
(1)先把新插入节点的父(parent)节点和叔父(uncle)节点变成红色。
(2)再把新插入节点的祖父(grand)节点变成红色,并设置为当前节点。

在这里插入图片描述

将将新插入节点的祖父节点设置为红色后,然后祖父节点的父节点是黑色的,就无需做平衡处理, 若祖父节点的父节点是红色的, 违反了红黑树特性4(每个红色节点的两个子节点一定是黑色的,不能有两个红色节点相连),把祖父节点设置为当前节点,然后接着做平衡处理,直到整体平衡位置。

细分场景1:LL型失衡
新插入节点,为其父节点的左子节点(LL红色情况), 插入后 就是LL 型失衡。自平衡处理步骤如下:
(1)变颜色:将将新插入节点的节点设置为黑色,将祖父节点设置为红色;
(2)对插入后节点的父节点进行右旋,
在这里插入图片描述

细分场景2: LR型失衡
新插入节点,为其父节点的右子节点(LR红色情况), 插入后 就是LR 型失衡. 自平衡处理步骤如下 :
(1) 对新插入节点的父节点F进行左旋;并设置F为当前节点,得到LL红色情况。
(2)按照LL红色情况进行处理。
在这里插入图片描述

情景4.3:叔叔为黑节点,父亲为红色,并且父亲节点是祖父节点的右子节点

细分场景1:RR型失衡

新插入节点,为其父节点的右子节点(RR红色情况),自平衡操作步骤:
(1)变色:将新插入节点的父节点设置为黑色,将祖父节点设置为红色;
(2)对将新插入节点的祖父节点进行左旋;
在这里插入图片描述

细分场景2:RL型失衡
新插入节点,为其父节点的左子节点(RL红色情况)。自平衡操作步骤如下:
(1)对新插入节点的父节点F进行右旋;并把选择后的F设置为当前节点,得到RR红色情况
(2)按照得到RR红色情况处理(变色、左旋)

在这里插入图片描述

红黑树插入新节点后或删除节点需要做平衡操作,具体代码实现如下:

 /**
     * 添加结点后的处理
     * @param node 新添加的节点
     */
    @Override
    protected void afterAdd(Node<E> node) {
        Node<E> parent = node.parent;

        // 添加的是根节点 或者 上溢到达了根节点
        if (parent == null) {
            black(node);
            return;
        }

        // 如果父节点是黑色,直接返回
        if (isBlack(parent)) return;

        // 叔父节点
        Node<E> uncle = parent.sibling();
        // 祖父节点
        Node<E> grand = red(parent.parent);
        if (isRed(uncle)) { // 叔父节点是红色【B树节点上溢】
            black(parent);
            black(uncle);
            // 把祖父节点当做是新添加的节点
            afterAdd(grand);
            return;
        }

        // 叔父节点不是红色
        if (parent.isLeftChild()) { // L
            if (node.isLeftChild()) { // LL
                black(parent);
            } else { // LR
                black(node);
                rotateLeft(parent);
            }
            rotateRight(grand);
        } else { // R
            if (node.isLeftChild()) { // RL
                black(node);
                rotateRight(parent);
            } else { // RR
                black(parent);
            }
            rotateLeft(grand);
        }
    }
    
     @Override
    protected void afterRemove(Node<E> node) {
        // 如果删除的节点是红色
        // 或者 用以取代删除节点的子节点是红色
        if (isRed(node)) {
            black(node);
            return;
        }

        Node<E> parent = node.parent;
        // 删除的是根节点
        if (parent == null) return;

        // 删除的是黑色叶子节点【下溢】
        // 判断被删除的node是左还是右
        boolean left = parent.left == null || node.isLeftChild();
        Node<E> sibling = left ? parent.right : parent.left;
        if (left) { // 被删除的节点在左边,兄弟节点在右边
            if (isRed(sibling)) { // 兄弟节点是红色
                black(sibling);
                red(parent);
                rotateLeft(parent);
                // 更换兄弟
                sibling = parent.right;
            }

            // 兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                // 兄弟节点没有1个红色子节点,父节点要向下跟兄弟节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    afterRemove(parent);
                }
            } else { // 兄弟节点至少有1个红色子节点,向兄弟节点借元素
                // 兄弟节点的左边是黑色,兄弟要先旋转
                if (isBlack(sibling.right)) {
                    rotateRight(sibling);
                    sibling = parent.right;
                }

                color(sibling, colorOf(parent));
                black(sibling.right);
                black(parent);
                rotateLeft(parent);
            }
        } else { // 被删除的节点在右边,兄弟节点在左边
            if (isRed(sibling)) { // 兄弟节点是红色
                black(sibling);
                red(parent);
                rotateRight(parent);
                // 更换兄弟
                sibling = parent.left;
            }

            // 兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                // 兄弟节点没有1个红色子节点,父节点要向下跟兄弟节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    afterRemove(parent);
                }
            } else { // 兄弟节点至少有1个红色子节点,向兄弟节点借元素
                // 兄弟节点的左边是黑色,兄弟要先旋转
                if (isBlack(sibling.left)) {
                    rotateLeft(sibling);
                    sibling = parent.left;
                }

                color(sibling, colorOf(parent));
                black(sibling.left);
                black(parent);
                rotateRight(parent);
            }
        }
    }


红黑树与AVL树的区别

1、调整平衡的实现机制不同
红黑树根据路径上黑色节点数目一致,来确定是否失衡,如果失衡,就通过变色和旋转来恢复;

AVL根据树的平衡因子(所有节点的左右子树高度差的绝对值不超过1),来确定是否失衡,如果失衡,就通过旋转来恢复

2、红黑树的插入效率更高
红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,红黑树并不追求“完全平衡”,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能;

AVL是严格平衡树(高度平衡的二叉搜索树),因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高。

3、红黑树统计性能比AVL树更高
红黑树能够以O(log n) 的时间复杂度进行查询、插入、删除操作。

AVL树查找、插入和删除在平均和最坏情况下都是O(log n)。

红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,

4、适用性:AVL查找效率高
如果你的应用中,查询的次数远远大于插入和删除,那么选择AVL树,如果查询和插入删除次数几乎差不多,应选择红黑树。

即,有时仅为了排序(建立-遍历-删除),不查找或查找次数很少,R-B树合算一些。

节点【10, 35, 47, 11, 5, 57, 39, 14, 27, 26, 84, 75, 63, 41, 37, 24, 96】;节点的BST、AVL、RBTree的展示如下

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Docker是一种流行的容器化技术,通过轻量级、隔离性强的容器来运行应用程序。下面我将通过十张图你深入理解Docker容器和镜像。 1. 第一张图展示了Docker容器和镜像的关系。镜像是Docker的基础组件,它是一个只读的模板,包含了运行应用程序所需的所有文件和配置。容器是从镜像创建的实例,它具有自己的文件系统、网络和进程空间。 2. 第二张图展示了Docker容器的隔离性。每个容器都有自己的文件系统,这意味着容器之间的文件互不干扰。此外,每个容器还有自己的网络和进程空间,使得容器之间的网络和进程相互隔离。 3. 第三张图展示了Docker镜像和容器的可移植性。镜像可以在不同的主机上运行,只需在目标主机上安装Docker引擎即可。容器也可以很容易地在不同的主机上迁移,只需将镜像传输到目标主机并在其上创建容器。 4. 第四张图展示了Docker容器的快速启动。由于Docker容器与主机共享操作系统内核,启动容器只需几秒钟的时间。这使得快速部署和扩展应用程序成为可能。 5. 第五张图展示了Docker容器的可重复性。通过使用Dockerfile定义镜像构建规则,可以确保每次构建的镜像都是相同的。这样,可以消除由于环境差异导致的应用程序运行问题。 6. 第六张图展示了Docker容器的资源隔离性。Docker引擎可以为每个容器分配一定数量的CPU、内存和磁盘空间,确保容器之间的资源不会互相干扰。 7. 第七张图展示了Docker容器的可扩展性。通过使用Docker Swarm或Kubernetes等容器编排工具,可以在多个主机上运行和管理大规模的容器群集。 8. 第八张图展示了Docker镜像的分层结构。镜像由多个只读层组成,每个层都包含一个或多个文件。这种分层结构使得镜像的存储和传输变得高效。 9. 第九张图展示了Docker容器的生命周期。容器可以通过创建、启动、停止和销毁等命令来管理。这使得容器的维护和管理变得简单。 10. 第十张图展示了Docker容器的应用场景。Docker容器广泛应用于开发、测试、部署和运维等领域。它可以提供一致的开发和运行环境,简化了应用程序的管理和交付过程。 通过这十张图,希望能让大家更深入地理解Docker容器和镜像的概念、特性和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弯_弯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值