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

news2024/11/15 18:02:59

树(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的展示如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/738651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[已解决]Running setup.py install for MinkowskiEngine ... error

虚拟环境中安装MinkowskiEngine&#xff1a; pip install -U MinkowskiEngine --install-option"--blasopenblas" -v --no-deps 报错&#xff1a;“Running setup.py install for MinkowskiEngine ... error” 解决办法[链接][参考1]&#xff1a; &#xff08;1&…

Unity 编辑器-创建模板脚本,并自动绑定属性,添加点击事件

当使用框架开发时&#xff0c;Prefab挂载的很多脚本都有固定的格式。从Unity的基础模板创建cs文件&#xff0c;再修改到应有的模板&#xff0c;会浪费一些时间。尤其是有大量的不同界面时&#xff0c;每个都改一遍&#xff0c;浪费时间不说&#xff0c;还有可能遗漏或错改。写个…

查询直播频道发起的签到记录

接口描述 1、通过直播场次id&#xff0c;查询签到发起记录 2、接口支持https协议 接口URL http://api.polyv.net/live/v3/channel/chat/checkin-by-sessionId 请求方式 GET 请求参数描述 参数名必选类型说明appIdtrueString账号appIdtimestamptrueLong当前13位毫秒级时间戳&…

OSPF实验2

OSPF实验2 要求&#xff1a; 1.如图连接&#xff0c;合理规划IP地址&#xff0c;所有路由器各自创建一个loopback接口 2.R1再创建三个接口IP地址为201.1.1.1/24、201.1.2.1/24、201.1.3.1/24 R5再创建三个接口IP地址为202.1.1.1/24、202.1.2.1/24、202.1.3.1/24 R7再创建三…

小红书如何开店,送你一份保姆级开店教程

科思创业汇 大家好&#xff0c;这里是科思创业汇&#xff0c;一个轻资产创业孵化平台。赚钱的方式有很多种&#xff0c;我希望在科思创业汇能够给你带来最快乐的那一种&#xff01; 今天&#xff0c;我们来谈谈小红书的电子商务。这也是今年非常流行的电子商务平台。很多人说…

《设计模式》责任链模式

《设计模式》责任链模式 定义&#xff1a; 责任链模式将链中每一个节点都看成一个对象&#xff0c;并且将这些节点对象连成一条链&#xff0c;请求会沿着这条链进行传递&#xff0c;直到有对象处理它为止&#xff0c;这使得多个对象都有机会接收请求&#xff0c;避免了请求发送…

warp框架教程3-path, method和自定义请求方法

path, method和自定义请求方法 path 是 warp 中的路由系统, 一个 web 框架的灵魂所在, 一个优美的路由系统可以给我们带来非常良好的使用体验, 而 warp 的路由体验本身就是非常 nice 的。在本文中将展示一个 RESTful 风格的 API 设计。下面先来学习一下 path 模块。 path 模块…

从小白到大神之路之学习运维第56天--------shell脚本实例应用2.0之有趣的知识

第三阶段基础 时 间&#xff1a;2023年7月10日 参加人&#xff1a;全班人员 内 容&#xff1a; shell实例 目录 shell脚本应用&#xff1a; 一、if判断 1、if判断的类型 1&#xff09;单分支 2&#xff09;双分支 3&#xff09;多分支 2、单分支if判断 1&#x…

Could not increase number of max_open_files to more than 5000 (request: 65535)

修改MySQL 打开文件数量限制 修改内核限制 ulimit -n //查看系统限制 修改 /etc/security/limits.conf 添加 soft nofile 65530hard nofile 65535 mysql> SHOW VARIABLES LIKE open_files_limit; 通过 MySQL 命令行检查新限制。您可以使用以下查询&#xff0c;确保设置了新…

vue3使用less入门使用案例(webStrom)

文章目录 简介安装less基础代码效果 less进阶代码效果 简介 less&#xff1a;css预处理语言 安装 npm i less3.0.4 -Dless打包解释器 npm i less-loader5.0.0 -Dless基础代码 <template><div class"a"></div> </template><style lang…

Java Web Servlet (1)23.7.7

Servlet 1&#xff0c; Servlet 1.1 简介 Servlet是JavaWeb最为核心的内容&#xff0c;它是Java提供的一门动态web资源开发技术。 使用Servlet就可以实现&#xff0c;根据不同的登录用户在页面上动态显示不同内容。 Servlet是JavaEE规范之一&#xff0c;其实就是一个接口&a…

【日常记录】CentOS7.5 安装tomcat放行8080端口后无法访问

环境 操作系统版本信息&#xff1a; [rootlocalhost conf]# cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core)JAVA版本信息 [rootlocalhost conf]# java -version openjdk version "1.8.0_372" OpenJDK Runtime Environment (build 1.8.0_372-b07) …

闲人闲谈PS之四十四——供应链透明化

惯例闲话&#xff1a; 近期工作和天气一样&#xff0c;如火如荼&#xff0c;前面1个月&#xff0c;拼全力搞了一个新功能&#xff0c;把闲人折腾的够呛&#xff0c;现在回头看看这个过程&#xff0c;倒也回味无穷&#xff0c;如何利用信息化工具&#xff0c;搞点数字化的活&am…

ORA-39168: Object path STATISTICS was not found

上周某客户因异常断电导致lun&#xff0c;某个测试环境无法启动&#xff0c;客户只提供了一个1周前.dmp文件&#xff0c;需要在新环境中导入恢复&#xff0c;解决表空间和临时表空间问题后&#xff0c;导入报错如下 Import: Release 11.2.0.4.0 - Production on Thu Jul 6 07:…

基础篇--Cortex-M系列介绍

视频教程 体系结构&#xff08;ARM架构版本&#xff09;与其对应的ARM处理器内核 Cortex-M系列介绍 ARM公司 ARM公司&#xff1a;只做内核设计和IP授权&#xff0c;不参与芯片设计 ARM架构为什么能风靡全球&#xff1f; Cortex内核分类及特征 Cortex-M3/4/7介绍

几个有趣的Python库,建议收藏~

随着每个 Python 版本的发布&#xff0c;都会添加新模块&#xff0c;并引入新的更好的做事方式&#xff0c;虽然我们都习惯了使用好的旧 Python 库和某些做事方式&#xff0c;但现在也时候升级并利用新的和改进的模块及其特性了。 Pathlib pathlib 绝对是 Python 标准库中最近…

python 第十二章 面向对象

系列文章目录 第一章 初识python 第二章 变量 第三章 基础语句 第四章 字符串str 第五章 列表list [] 第六章 元组tuple ( ) 第七章 字典dict {} 第八章 集合set {} 第九章 常用操作 第十章 函数 第十一章 文件操作 文章目录 系列文章目录12.1面向对象实现方法定义类经典类&am…

《Redis 核心技术与实战》课程学习笔记(五)

数据同步&#xff1a;主从库如何实现数据一致&#xff1f; 那我们总说的 Redis 具有高可靠性&#xff0c;又是什么意思呢&#xff1f; 其实&#xff0c;这里有两层含义&#xff1a;一是数据尽量少丢失&#xff0c;二是服务尽量少中断。AOF 和 RDB 保证了前者&#xff0c;而对于…

【NLP,Huggingface,Colab】使用 Trainer 训练模型,并保存模型参数

【NLP&#xff0c;Huggingface&#xff0c;Colab】使用 Trainer 训练模型&#xff0c;并保存模型参数 前置知识上代码 前置知识 Colab 的使用Huggingface 官网和一些基础API 上代码 首先&#xff0c;建议保存代码到 VSCode&#xff0c;这样双击关键类&#xff0c;F12可以进入…

一个HTTP的流程

1&#xff0c;键入一个URL后浏览器将URL进行解析 2,浏览器解析URL后&#xff0c;需要查询服务器域名对应的IP地址。 流程如下&#xff1a;查询缓存&#xff0d; >客户端发送DNS请求-> 根DNS&#xff0c;根DNS根据 .COM-> 顶级域名服务器&#xff0c;根据baidu->权…