AVL 树的初步认识与基本操作

news2024/11/17 9:25:17

历史

AVL 树是一种自平衡二叉搜索树,由托尔·哈斯特罗姆在 1960 年提出并在 1962 年发表。它的名字来源于发明者的名字:Adelson-Velsky 和 Landis,他们是苏联数学家,于 1962 年发表了一篇论文,详细介绍了 AVL 树的概念和性质。

在二叉搜索树中,如果插入的元素按照特定的顺序排列,可能会导致树变得非常不平衡,从而降低搜索、插入和删除的效率。为了解决这个问题,AVL 树通过在每个节点中维护一个平衡因子来确保树的平衡。平衡因子是左子树的高度减去右子树的高度。如果平衡因子的绝对值大于等于 2,则通过旋转操作来重新平衡树。

AVL 树是用于存储有序数据的一种重要数据结构,它是二叉搜索树的一种改进和扩展。它不仅能够提高搜索、插入和删除操作的效率,而且还能够确保树的深度始终保持在 O(log n) 的水平。随着计算机技术的不断发展,AVL 树已经成为了许多高效算法和系统中必不可少的一种基础数据结构。

什么叫平衡二叉搜索树?什么叫不平衡二叉搜索树?

个人理解:

答:当根节点两边左右孩子都有则为平衡二叉搜索树,如果有一边没有孩子就是不平衡,就像一个人如果缺胳膊少腿肯定就会看起来不平衡。

温馨提示:对于二叉树还是不太认识的,作者推荐:二叉搜索树的初步认识_加瓦不加班的博客-CSDN博客

前面介绍过,如果一棵二叉搜索树长的不平衡,那么查询的效率会受到影响,如下图

比如,我们如果要从上述二叉树,根节点是3,它虽然有左孩子,你有没有发现它右孩子没有,那岂不就是缺胳膊少腿?所以这个就是不平衡二叉搜索树,

那么为什么一棵二叉搜索树长的不平衡会查询的效率会受到影响?

答:比如,你现在要搜索节点为1的情况,你要从根节点开始,从根往左,也就是搜索到最低层节点才能找到,也就是要走2步,如果,此时我将2做为根节点,那么从2开始查询,只需要一步到位查询到1.是不是相对平衡的二叉树搜索起来更加高效?

通过旋转可以让树重新变得平衡,并且不会改变二叉搜索树的性质(即左边仍然小,右边仍然大)

什么叫自平衡二叉搜索树?

就是当二叉树出现不平衡时,我们二叉树能够检测不平衡,通过“旋转”,也就是自动将根节点调换,换成能够变成平衡二叉树的情况。

/**

  • AVL 树

  • <li>二叉搜索树在插入和删除时,节点可能失衡</li>

  • <li>如果在插入和删除时通过旋转, 始终让二叉搜索树保持平衡, 称为自平衡的二叉搜索树</li>

  • <li>AVL 是自平衡二叉搜索树的实现之一</li>

  • </ul> */

如何判断失衡?

如果一个节点的左右孩子,高度差超过 1,则此节点失衡,才需要旋转

处理高度

如何得到节点高度?一种方式之前做过的一道题目:E05. 求二叉树的最大深度(高度)二叉树--二叉树最大深度_加瓦不加班的博客-CSDN博客,但由于求高度是一个非常频繁的操作,因此将高度作为节点的一个属性,将来新增或删除时及时更新,默认为 1(按力扣说法)

static class AVLNode {
    int height = 1;
    int key;
    Object value;
    AVLNode left;
    AVLNode right;
    // ...
}

求高度代码:

这里加入了 height 函数方便求节点为 null 时的高度

private int height(AVLNode node) {
    return node == null ? 0 : node.height;
}

更新高度代码

将来新增、删除、旋转时,高度都可能发生变化,需要更新。下面是更新高度的代码

在这里我们之前其实已经学习了如何获取节点的高度----二叉树最大深度-力扣104二叉树--二叉树最大深度_加瓦不加班的博客-CSDN博客

/*
   思路:
   1. 得到左子树深度, 得到右子树深度, 二者最大者加一, 就是本节点深度
   2. 因为需要先得到左右子树深度, 很显然是后序遍历典型应用
   3. 关于深度的定义:从根出发, 离根最远的节点的总边数,这个总边数指的就是下面的线段数
       注意: 力扣里的深度定义要多一,在你现在看来下面的深度确实是1 2 0  但是力扣官方觉得:在你看来的基础上+1才是正确的深度

 你的视角:      深度:1         深度:2         深度:0
 力扣的视角:      深度:2         深度:3         深度:1
                   1            1            1
                  / \          / \
                 2   3        2   3
                                   \
                                    4
*/
   public int maxDepth(TreeNode node) {
       if (node == null) {
           return 0; // 非力扣题目改为返回 -1
       }
       int d1 = maxDepth(node.left);
       int d2 = maxDepth(node.right);
       return Integer.max(d1, d2) + 1;
   }

举例说明:

当二叉树只有根节点时,高度是1:

当二叉树有根节点跟一边的子节点时,根节点高度是2 子节点是1:(实际上是想告诉你的是,任何一个节点在新增以后的高度都是要变化的):

但是有一种情况是不会变化的,那就是当在同一层加节点,高度不变:

private void updateHeight(AVLNode node) {
    node.height = Integer.max(height(node.left), height(node.right)) + 1;
}

何时触发失衡判断?

定义平衡因子(balance factor)如下

当平衡因子

  • bf = 0,1,-1 时,表示左右平衡

为什么会有0,1,-1的情况?

举例说明:

对于0这个情况:

对于4节点为参考,它的左右孩子高度相减就是0

对于1这个情况:

对于4根节点为参考,它的左孩子高度为2,右孩子高度为1,而我们相减的顺序是(左-右) ,相减就是1

对于-1这个情况:

对于2根节点为参考,它的左孩子高度为1,右孩子高度为2,而我们相减的顺序是(左-右) ,相减就是-1

  • bf > 1 时,表示左边太高

  • bf < -1 时,表示右边太高

对应代码

private int bf(AVLNode node) {
    return height(node.left) - height(node.right);
}

当插入新节点,或删除节点时,引起高度变化时,例如

目前此树平衡,当再插入一个 4 时,节点们的高度都产生了相应的变化,8 节点失衡了

在比如说,下面这棵树一开始也是平衡的

当删除节点 8 时,节点们的高度都产生了相应的变化,6 节点失衡了

失衡的四种情况

LL

  • 失衡节点(图中 8 红色)的 bf > 1,即左边更高

  • 失衡节点的左孩子(图中 6)的 bf >= 0 ,即图中 6的左孩子这边也是左边更高或等高

LR

  • 失衡节点(图中 8)的 bf > 1,即左边更高

  • 失衡节点的左孩子(图中 3 红色)的 bf < 0 ,即左孩子(图中 3 红色)这边是右边孩子更高

对称的还有两种情况

接下来的两个情况和上面两种情况是对称的:

RL

  • 失衡节点(图中 3)的 bf <-1,即右边更高

  • 失衡节点的右孩子(图中 6 红色)的 bf > 0,即右孩子(图中 6 红色)这边左边更高

RR

  • 失衡节点(图中 3)的 bf <-1,即右边更高

  • 失衡节点的右孩子(图中 5 红色)的 bf <= 0,即右孩子(图中 5 红色)这边右边更高或等高

解决失衡

失衡可以通过树的旋转解决。什么是树的旋转呢?它是在不干扰元素顺序的情况下更改结构,通常用来让树的高度变得平衡。

观察下面一棵二叉搜索树,可以看到,旋转后,并未改变树的左小右大特性,但根、父、孩子节点都发生了变化

      4                                          2
     / \             4 right                  / \
    2   5      -------------------->    1   4
   / \         <--------------------      / \
  1   3              2 left               3   5

右旋

  • 红色节点,旧根(失衡节点)

  • 黄色节点,旧根的左孩子,将来作为新根,旧根是它右孩子

  • 绿色节点,新根的右孩子,将来要换爹作为旧根的左孩子

旋转后

代码

//参数:要旋转的节点 也就是失衡节点 
private AVLNode rightRotate(AVLNode red) {
    //黄色节点,旧根的左孩子
    AVLNode yellow = red.left;
    //绿色节点:当黄色节点有右孩子不是null,则要执行下面的red.left = green;  如果黄色节点有右孩子是null,就不需要执行red.left = green; 
    //但是如果黄色节点有右孩子是null 执行red.left = green; 指向Null也没事
    AVLNode green = yellow.right;
    yellow.right = red;
    red.left = green;
    //做完失衡调整以后 记得要做更新高度操作 更新高度的操作不能改变
    updateHeight(red);
    updateHeight(yellow);
    return yellow;
}

左旋

旋转前

  • 红色节点,旧根(失衡节点)

  • 黄色节点,旧根的右孩子,将来作为新根,旧根是它左孩子

  • 绿色节点,新根的左孩子,将来要换爹作为旧根的右孩子

旋转后

代码 :与右旋的代码相对

private AVLNode leftRotate(AVLNode red) {
    AVLNode yellow = red.right;
    AVLNode green = yellow.left;
    yellow.left = red;
    red.right = green;
    //做完失衡调整以后 记得要做更新高度操作 更新高度的操作不能改变
    updateHeight(yellow);
    updateHeight(red);
    return yellow;
}

左右旋

指先左旋左子树,再右旋根节点(失衡),这时一次旋转并不能解决失衡

左子树旋转后

根右旋前

根右旋后

代码

private AVLNode leftRightRotate(AVLNode root) {
    root.left = leftRotate(root.left);
    return rightRotate(root);
}

右左旋

指先右旋右子树,再左旋根节点(失衡)

右子树右旋后

 根左旋前

根左旋后

代码

private AVLNode rightLeftRotate(AVLNode root) {
    root.right = rightRotate(root.right);
    return leftRotate(root);
}

你发现有这四种不平衡情况,其实基本操作就是左旋和右旋这两种。

判断及调整平衡代码

//检查节点是否失衡,重新平衡代码
private AVLNode balance(AVLNode node) {
    if (node == null) {
        return null;
    }
    int bf = bf(node);
    
    if (bf > 1 && bf(node.left) >= 0) { //LL
        return rightRotate(node);
    } else if (bf > 1 && bf(node.left) < 0) { //LR
        return rightLeftRotate(node);
    } else if (bf < -1 && bf(node.right) > 0) { //RL
        return leftRightRotate(node);
    } else if (bf < -1 && bf(node.right) <= 0) {//RR
        return rightRotate(node);
    }
    return node;
}

以上四种旋转代码里,都需要更新高度,需要更新高度的节点只有红色、黄色,而绿色节点与其他无色节点高度是不变的

新增操作

AVLNode root;
public void put(int key, Object value) {
    root = doPut(root, key, value);
}
//传来的根节点
private AVLNode doPut(AVLNode node, int key, Object value) {
    //1.找到空位  创建新节点
    if (node == null) {
        return new AVLNode(key, value);
    }
    //2.Key已存在,则更新
    if (key == node.key) {
        node.value = value;
        return node;
    }
    if (key < node.key) {
        node.left = doPut(node.left, key, value);
    } else {
        node.right = doPut(node.right, key, value);
    }
    updateHeight(node);
    return balance(node);
}

删除操作

public void remove(int key) {
    root = doRemove(root, key);
}

//node:传入的根节点
private AVLNode doRemove(AVLNode node, int key) {
    // 1. node == null
    if (node == null) {
        return null;
    }
    // 2. 没找到 key
    if (key < node.key) {
        node.left = doRemove(node.left, key);
    } else if (node.key < key) {
        node.right = doRemove(node.right, key);
    } else {
        // 3. 找到 key  1) 没有孩子 2) 只有一个孩子 3) 有两个孩子
        if (node.left == null && node.right == null) { //1) 没有孩子
            return null;
        } else if (node.left == null) { //2) 只有一个孩子
            node = node.right;
        } else if (node.right == null) {//2) 只有一个孩子
            node = node.left;
        } else { //3) 有两个孩子
            AVLNode s = node.right; //初始是待删除的右子树  
            //当后继节点与待删除节点不是相邻的
            while (s.left != null) {
                s = s.left;
            }
            //找到后继节点:s
            s.right = doRemove(node.right, s.key);//如果后继节点也有孩子,要把后继节点的孩子处理好
            s.left = node.left;
            //后继节点代替待删除节点
            node = s;
        }
    }
    if (node == null) {
        return null;
    }
    // 4. 更新高度
    updateHeight(node);
    // 5. balance
    return balance(node);
}

完整代码备份

public class AVLTree {
    static class AVLNode {
        int height = 1;
        int key;
        Object value;
        AVLNode left;
        AVLNode right;

        public AVLNode(int key) {
            this.key = key;
        }

        public AVLNode(int key, Object value) {
            this.key = key;
            this.value = value;
        }

        public AVLNode(int key, Object value, AVLNode left, AVLNode right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

    AVLNode root;

    private AVLNode leftRotate(AVLNode p) {
        AVLNode r = p.right;
        AVLNode b = r.left;
        r.left = p;
        p.right = b;
        //做完失衡调整以后 记得要做更新高度操作 更新高度的操作不能改变
        updateHeight(p);
        updateHeight(r);
        return r;
    }

    private void updateHeight(AVLNode node) {
        node.height = Integer.max(height(node.left), height(node.right)) + 1;
    }

    private AVLNode rightRotate(AVLNode r) {
        AVLNode a = r.left;
        AVLNode b = a.right;
        a.right = r;
        r.left = b;
        //做完失衡调整以后 记得要做更新高度操作  更新高度的操作不能改变
        updateHeight(r);
        updateHeight(a);
        return a;
    }

    private AVLNode leftRightRotate(AVLNode p) {
        AVLNode r = p.left;
        p.left = leftRotate(r);
        return rightRotate(p);
    }

    private AVLNode rightLeftRotate(AVLNode p) {
        AVLNode r = p.right;
        p.right = rightRotate(r);
        return leftRotate(p);
    }

    private int height(AVLNode node) {
        return node == null ? 0 : node.height;
    }



    public void remove(int key) {
        root = doRemove(root, key);
    }

    private AVLNode doRemove(AVLNode node, int key) {
        if (node == null) {
            return null;
        }
        if (key < node.key) {
            node.left = doRemove(node.left, key);
        } else if (node.key < key) {
            node.right = doRemove(node.right, key);
        } else {
            if (node.left == null) {
                node = node.right;
            } else if (node.right == null) {
                node = node.left;
            } else {
                AVLNode s = node.right;
                while (s.left != null) {
                    s = s.left;
                }
                s.right = doRemove(node.right, s.key);
                s.left = node.left;
                node = s;
            }
        }
        if (node == null) {
            return null;
        }
        updateHeight(node);
        return balance(node);
    }

    public void put(int key, Object value) {
        root = doPut(root, key, value);
    }

    private AVLNode doPut(AVLNode node, int key, Object value) {
        if (node == null) {
            return new AVLNode(key, value);
        }
        if (key == node.key) {
            node.value = value;
            return node;
        }
        if (key < node.key) {
            node.left = doPut(node.left, key, value);
        } else {
            node.right = doPut(node.right, key, value);
        }
        updateHeight(node);
        return balance(node);
    }

    private int bf(AVLNode node) {
        return height(node.left) - height(node.right);
    }

    private AVLNode balance(AVLNode node) {
        if (node == null) {
            return null;
        }
        int bf = bf(node);
        if (bf > 1 && bf(node.left) >= 0) {
            return rightRotate(node);
        } else if (bf > 1 && bf(node.left) < 0) {
            return rightLeftRotate(node);
        } else if (bf < -1 && bf(node.right) > 0) {
            return leftRightRotate(node);
        } else if (bf < -1 && bf(node.right) <= 0) {
            return rightRotate(node);
        }
        return node;
    }
}

小结

AVL树的优点:

  1. AVL树是一种自平衡树,保证了树的高度平衡,从而保证了树的查询和插入操作的时间复杂度均为O(logn)。

  2. 相比于一般二叉搜索树,AVL树对查询效率的提升更为显著,因为其左右子树高度的差值不会超过1,避免了二叉搜索树退化为链表的情况,使得整棵树的高度更低。

  3. AVL树的删除操作比较简单,只需要像插入一样旋转即可,在旋转过程中树的平衡性可以得到维护。

AVL树的缺点:

  1. AVL树每次插入或删除节点时需要进行旋转操作,这个操作比较耗时,因此在一些应用中不太适用。

  2. 在AVL树进行插入或删除操作时,为保持树的平衡需要不断进行旋转操作,在一些高并发环节和大数据量环境下,这可能会导致多余的写锁导致性能瓶颈。

  3. AVL树的旋转操作相对较多,因此在一些应用中可能会造成较大的空间浪费。

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

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

相关文章

nginx配置https 访问

nginx 解压目录有configure文件 [rootoracledb10 ~]# which nginx1、检查nginx是否包含http_ssl_module 模块 如果出现 --with-http_ssl_module 就是已经安装了[rootoracledb10 sbin]# pwd /usr/local/nginx/sbin [rootoracledb10 sbin]# nginx -V nginx version: nginx/1.23…

技术分享| 二进制部署MySQL

一、介绍 ​MySQL是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#x…

华为手环8全新炫酷表盘上线,这些免费表盘,你绝对不能错过

在现今忙碌的生活节奏中&#xff0c;保持身心健康已逐渐成为大众的共同追求。华为手环8以时尚的设计、轻巧的体积、精确的传感器、丰富的健康模式以及智慧生活的无缝拓展&#xff0c;成为了潮流先锋达人的理想之选。华为手环8还支持多种表盘样式&#xff0c;满足不同用户的个性…

Vue开发中Jwt的使用

&#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Vue》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一定基础的程序员&#xff0c;这个专栏…

ai智能语音机器人必须具备的功能

近年来&#xff0c;大多数互联网公司都进入了智能化领域。 随着人工智能技术的不断升级和突破&#xff0c;智能出境行业涌现出许多新品牌。 这些品牌有的以价格取胜&#xff0c;有的以产品性能取胜&#xff0c;这确实给消费者增加了很多选择。 ​ 然而&#xff0c;智能外呼产品…

【Java每日一题】——第二十八题:编程定义一个学生类汽车类Car(2023.10.12)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

史上最全,最详细SQL基础

SQL基础 一、 建库建表 1.1 创建数据库 创建数据库模板&#xff1a; create database 数据库名称 --创建数据库 on primary (name --数据库的逻辑名称filename --物理存放位置及物理文件名称(Student_info.m…

spark中的shuffle简述 那些会导致shuffle的算子

shuffle操作说白了就是重分区操作 在Apache Spark中&#xff0c;任务之间的依赖关系主要分为两类&#xff1a;宽依赖&#xff08;Wide Dependency&#xff09;和窄依赖&#xff08;Narrow Dependency&#xff09;。这两者之间的主要区别在于它们对任务之间数据的依赖性以及执行…

JNI中调用Java函数

文章目录 一、JNI 注册二、JNI 调用 Java 函数1、实例2、总结3、参考 三、JNI 数据传递四、JNA五、图像传递 一、JNI 注册 JNI 分成静态注册和动态注册 静态注册 cpp 实现 JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *env, jobject…

【TensorFlow2 之012】TF2.0 中的 TF 迁移学习

#012 TensorFlow 2.0 中的 TF 迁移学习 一、说明 在这篇文章中&#xff0c;我们将展示如何在不从头开始构建计算机视觉模型的情况下构建它。迁移学习背后的想法是&#xff0c;在大型数据集上训练的神经网络可以将其知识应用于以前从未见过的数据集。也就是说&#xff0c;为什么…

omnipathr官网教程 mistr

github python版本 omnipath tutorials Issue #17 saezlab/omnipath (github.com) R版本 saezlab/OmnipathR: R client for the OmniPath web service (github.com)https://github.com/saezlab/OmnipathR GitHub - saezlab/OmnipathR: R client for the OmniPath web s…

Prettier插件使用

一、前言 由于之前使用的Beautify格式化插件已经没有在维护了&#xff0c;所以这里再分享一个Formatter插件-Prettier。 二、插件安装 首先在扩展(ctrlShiftX)中搜索关键词Prettier&#xff0c;点击安装。 三、插件使用配置 首先在VSCode编辑器中依次打开菜单文件-首选项-…

Oauth2.0单点登录的解决方案 安当加密

上海安当技术有限公司的ASP身份认证系统提供针对Oauth2.0单点登录的解决方案。该解决方案可以帮助客户实现以下目标&#xff1a; 统一的用户管理&#xff1a;Oauth2.0单点登录可以提供一个统一的用户管理平台&#xff0c;使得用户只需要在一个平台上进行注册和身份认证&#x…

基于Java使用SpringBoot+Vue框架实现的前后端分离的美食分享平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 在当今社会&#xff0…

打造类ChatGPT服务,本地部署大语言模型(LLM),如何远程访问?

ChatGPT的成功&#xff0c;让越来越多的人开始关注大语言模型&#xff08;LLM&#xff09;。如果拥有了属于自己的大语言模型&#xff0c;就可以对其进行一些专属优化。例如&#xff1a;打造属于自己的AI助理&#xff0c;或是满足企业自身的业务及信息安全需求。 所以&#xff…

天猫商品评论数据接口,天猫商品评论API接口,天猫API接口

天猫商品评论内容数据接口的步骤&#xff0c;但是可以提供淘宝商品评论内容数据接口的步骤&#xff1a; 授权获得淘宝开放平台API所需的权限。获取AppKey和AppSecret等认证信息。发送HTTP请求&#xff0c;获取所需评价信息。对获取到的评价信息进行处理和解析。结果处理&#…

行业顶流|AI数字人直播,低成本、高回报的新趋势

去年&#xff0c;元宇宙的概念炒得特别火&#xff0c;落地却寥寥无几&#xff0c;今年的AI数字人是否也只是一时炒作的概念呢&#xff1f; 在这个信息时代&#xff0c;科技的发展总是伴随着各种概念的冒起。元宇宙作为其中之一&#xff0c;的确在一瞬间扑面而来&#xff0c;引…

《机器学习》第5章 神经网络

文章目录 5.1 神经元模型5.2 感知机与多层网络5.3 误差逆传播算法5.4 全局最小与局部最小5.5 其他常见神经网络RBF网络ART网络SOM网络级联相关网络Elman网络Boltzmann机 5.6 深度学习 5.1 神经元模型 神经网络是由具有适应性的简单单元组成的广泛并行互连的网络&#xff0c;它…

数字化转型,河北吉力宝—传统行业的自我救赎新标杆

近年来,国家出台了各项政策支持企业数字化转型,“十四五”计划更是将建设数字经济作为重要发展目标,中国人工智能产业进入爆发式增长阶段,市场潜力巨大。随着数字化时代的到来&#xff0c;加快发展数字经济成为把握新一轮科技革命和产业变革新机遇的战略选择。 健康卫生事件后…

React如何优化减少组件间的重新Render

目前写了不少React的项目&#xff0c;发现React有些特点更灵活和注重细节&#xff0c;很多东西需要有一定的内功才能掌握好&#xff1b;比如在项目中常常遇到的组件重复渲染&#xff0c;有时候组件重复渲染如果内容是纯文本&#xff0c;不打印日志就不容易发现重复渲染了&#…