树专题 —— 深入理解经典红黑树

news2024/11/26 9:41:01

大家好,我是 方圆。本篇我们讲红黑树的经典实现,Java中对红黑树的实现便采用的是经典红黑树。前一篇文章我们介绍过左倾红黑树,它相对来说比较简单,需要大家看完上篇再来看这一篇,因为旋转等基础知识不会再本篇文章中赘述。本篇的大部分内容参考 《算法导论》和 Java 实现红黑树的源码,希望大家能够有耐心的看完,如果大家想要找刷题路线的话,可以参考 Github: LeetCode

在正文开始之前我们先看如下问题:

  • 为什么红黑树比AVL树要应用得更广泛呢?

关于红黑树和 AVL 树,大家可能看过“在最坏情况下,AVL 树和红黑树的查找次数都是对数级别的,虽然红黑树的系数更高一些,但是没有本质的区别,是可以容忍的。AVL 树最致命的地方在于删除节点时旋转次数是对数级别的,而红黑树最多只需要 3 次旋转,这导致了红黑树应用相比于 AVL 树要广泛得多”的观点,但实际上这并不是根本原因,根本原因是在以任意序列插入和删除操作混合进行的情况下,红黑树均摊时间复杂度保持在 O(1),而 AVL 树的均摊时间复杂度为 O(logn)

经典红黑树与2-3-4搜索树 同构,它相比于左倾红黑树(2-3树)的实现,在维持红黑树平衡性开销更小。下文中我们会将经典红黑树简称为红黑树,开始吧:

2-3-4 搜索树

2-3-4搜索树是在2-3搜索树中增加了4-节点,在前文中已经介绍过4-节点,我们先来看一下2-3-4搜索树的样子:

2-3-4树.jpg

新节点插入2-节点或3-节点的情况我们就不在这里赘述了,我们重点看一下新节点插入4-节点的情况。当有新节点插入的节点为4-节点时,需要先将4-节点转换成3个2-节点,再在其中的2-节点中执行插入操作,如下所示,其中黄色节点为新插入的节点,对应了插入4-节点中的四种情况:

2-3-4树2.jpg

我们再以情况(4)为例,在2-3-4搜索树中执行插入值为34的节点:

2-3-4树3.jpg

将4-节点转换成3个2-节点并完成插入新节点34后,需要将“根节点25”插入到父节点中,如上图所示。这和我们在前文中在2-3搜索树中讲到的基本类似,需要不断分解临时的5-节点,并将原来4-节点分解成3个2-节点的根节点插入到更高的父节点中,直到遇到2-节点或3-节点,将其转换成不需要继续分解的节点,如果最终插入到根节点后使其为5-节点,同样需要进行分解再插入的操作,完成后树高加一

2-3-4树的插入操作使树本身的改变也是局部的,除了相关的节点和引用之外,不必修改和检查树的其他部分,这些局部变换不会影响到树的全局有序性和平衡性,在插入的过程中,2-3-4树始终是完美平衡二叉树

经典红黑树

经典红黑树与2-3-4搜索树同构,如果我们把2-3-4搜索树样例转换成红黑树的话,会如下图所示(指向红色节点的链接我们同样也染成红色):

经典红黑树.jpg

它满足如下性质:

  • 节点颜色为红色或黑色

  • 根节点是黑色的

  • 叶子节点(null 节点)为黑色(null 节点在图中未画出来)

  • 红色节点的两个子节点为黑色(不能出现连续的红色节点)

  • 任意叶子节点到根节点路径上的黑色节点数量相同,即该树是黑色平衡的

黑色平衡这条性质我们在讲解左倾红黑树时已经讲过,在这里我们不厌其烦地再叙述一遍:2-3-4树始终能保持完美平衡,那么任意叶子节点到达根节点的距离是相等的,红黑树又是一颗2-3-4搜索树,其中的黑链接是2-3-4搜索树中的普通链接,那么红黑树中被黑色链接引用的黑色节点也必然是完美平衡的,所以任意叶子节点到根节点路径上的黑色节点数量必然相同。

下面我们结合图示和Java中 TreeMap 源码来讲解红黑树的插入和删除节点操作:

节点定义
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;

        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
        // ...
    }

我们可以发现节点定义除了有表示颜色信息和左右子节点的引用外,还增加了 parent 针对父节点的引用。

插入节点
插入2-节点

直接将节点插入2-节点的情况非常简单,如下图所示的两种情况,2-节点转换成3-节点:

经典红黑树2.jpg

插入3-节点

插入3-节点我们需要分左斜3-节点和右斜3-节点两种情况讨论:

  1. 插入左斜的3-节点的左节点或右节点,需要将其转换成4-节点,如下图所示:

经典红黑树3.jpg

  1. 插入右斜的3-节点的左节点或右节点,需要将其转换成4-节点,如下图所示:

经典红黑树4.jpg

插入4-节点

以上两种情况都是不会发生“向上合并”,如果插入的4-节点,需要将其分解成3个2-节点,之后将2-节点的“根节点”合并到它的父节点中(如果有的话),我们同样需要分情况讨论:

  1. 插入4-节点的左侧红色节点:

经典红黑树5.jpg

  1. 插入4-节点的右侧红色节点:

经典红黑树6.jpg

目前插入新节点的所有情况已经讨论完了,下面我们看一下 TreeMap 中的源码,大家注意其中的注释即可:

    public V put(K key, V value) {
        Entry<K,V> t = root;
        // 插入第一个节点
        if (t == null) {
            compare(key, key);

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // 根据比较器找到插入节点的位置
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 根据大小关系添加新的节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        // *插入之后修复平衡操作*
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

需要重点关注 fixAfterInsertion 方法:

    private void fixAfterInsertion(Entry<K,V> x) {
        // 新插入的节点指定为红色
        x.color = RED;

        // 如果非空非根节点且有连续的红色节点出现,需要不断地修复平衡
        while (x != null && x != root && x.parent.color == RED) {
            // 插入节点后,出现连续红色的节点的位置在左侧
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                // 插入的是4-节点,对应插入4-节点的情况1
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    // 反色处理
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    // 处理父节点的父节点(因为该节点为红色,可能会发生向上合并的操作)
                    x = parentOf(parentOf(x));
                } else {
                    // 如下步骤对应插入3-节点的情况1
                    // 插入的是3-节点的右节点
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        // 左旋父节点
                        rotateLeft(x);
                    }
                    // 现在转换成了插入位置为3-节点的左节点,父节点染成黑色
                    setColor(parentOf(x), BLACK);
                    // 父节点的父节点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 右旋父节点的父节点,转换成4-节点
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                // 插入节点后,出现连续红色的节点的位置在右侧
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                // 插入的是4-节点,对应插入4-节点的情况2
                if (colorOf(y) == RED) {
                    // 反色处理
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    // 处理父节点的父节点(因为该节点为红色,可能会发生向上合并的操作)
                    x = parentOf(parentOf(x));
                } else {
                    // 如下步骤对应插入3-节点的情况2
                    // 插入的是3-节点的左节点
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        // 右旋父节点
                        rotateRight(x);
                    }
                    // 转换成了插入位置为3-节点的右节点,父节点为黑色
                    setColor(parentOf(x), BLACK);
                    // 父节点的父节点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    // 左旋父节点的父节点,转换成4-节点
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        // 根节点始终为黑色
        root.color = BLACK;
    }
*删除节点

删除节点是红黑树中最复杂的实现,如果删除的是3-节点或4-节点(删除的是红色节点),那么移除该节点不会影响红黑树的平衡;如果删除的是2-节点,需要在删除节点后向上修复红黑树的平衡,这也是其中最复杂的部分,下面我们按照《算法导论》中讨论的情况来分析,理解删除过程一定要对红黑树的性质非常熟悉(尤其是性质5:红黑树始终都是黑色平衡的),书中将2-节点移除后分了四种情况来讨论,记红黑树再平衡开始的节点为 x (x 节点为某2-节点被移除后,拼接到该位置的节点),其兄弟节点为 w,如下:

情况1:x 的兄弟节点 w 是红色的

DELETE.jpg
上图所示的局部红黑树在节点被移除前是黑色平衡的,那么从根节点到 a 子树的黑色节点数量应为 3。当节点被移除后,根节点到 a 子树的黑色节点数量为 2(如图上所示);根节点到 c 子树路径上的黑色节点数量为 2。

经过反色和左旋处理后,原根节点被染红,现在从“新根节点”到 a 子树的黑色节点数量仍然为 2,没有新增到 3,所以还是黑色不平衡;到 c 子树路径上的黑色节点数量为 2,与操作前一致,没有影响到红黑树的性质。

情况1在反色和左旋后会变成情况2、情况3或情况4继续处理,这其实不难理解,结合红黑树的性质5,反色和左旋操作使根节点到某子树路径上的黑色节点数量并没有改变,而我们执行删除操作时已经将该路径上移除一个黑色节点了,所以“根节点”到某子树的路径上依然需要补充一个黑色节点才能满足黑色平衡,关键思想在于:从根节点到每棵子树路径上黑色节点的数量不能被改变,性质 5 在删除节点和变换前后都需要成立

情况2:x 的兄弟节点 w 是黑色的,而且 w 的两个子节点都是黑色的

DELETE2.jpg

注意图中的红黑渐变色表示该节点可能是黑色节点也可能是红色节点。在该情况下,兄弟节点的两个子节点都是黑色的,在某节点被删除前从根节点到 a 子树和 c 子树路径上的根节点数量都是 2(忽略不确定颜色的根节点)。删除节点后根节点到 a 子树路径上的黑色节点数量为 1,如果我们将 w 节点染红,那么根节点到 c 子树路径上的黑色节点数量减少为 1,那么从根节点到图示中所有子树路径上黑色节点数量都一致了,也就是说图示中的局部红黑树满足黑色平衡的条件了。此时需要将 x 指向它的父节点再重复(while)修复的过程。

情况3:x 的兄弟节点 w 是黑色的,w 的左节点是红色的,w 的右节点是黑色的

DELETE3.jpg

这种情况下,根节点到子树 e 路径上黑色节点数量为 2,经过交换 w 和 w.left 的颜色和右旋操作后并不影响红黑树的性质,这样就将情况3转换成了情况4。

情况4:x 的兄弟节点 w 是黑色的,且 w 的右节点是红色的

DELETE4.jpg

在情况4下,根节点到 a 子树路径上的黑色节点数量为 1,想达到黑色平衡需要增加到 2。通过交换 w 节点和 x.parent 节点的颜色,再将 w.right 节点染黑,之后对 x.parent 节点执行左旋操作后,使得根节点到到 a 子树路径上的黑色节点增加到了 2,并且到达其他 c, d, e, f 子树路径上的黑色节点数量不变,不破坏红黑树的性质,操作完毕后将 x 指向根节点,结束循环,因为此时红黑树满足所有性质。

现在我们已经分析完了删除2-节点后的四种不同情况,都需要通过染色和旋转操作让红黑树再次黑色平衡,下面我们看下 TreeMapremove 方法的实现,需要关注其中的注释信息:

    /**
     * 删除节点
     */
    public V remove(Object key) {
        // 找到要删除的节点
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        // 执行删除操作
        deleteEntry(p);
        return oldValue;
    }

接下来我们看一下删除操作的执行逻辑:

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // 如果该节点有左右子节点,则它是一个要被删除的内部节点
        // 那么需要找到该节点的“后继节点”,找到之后删除任务则变为了删除该后继节点
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        }

        // 执行完上述代码块后,被删除节点要么为叶子节点,要么为只有左子树或右子树的节点
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        // 被删除节点有左子树或右子树
        if (replacement != null) {
            // 修正节点的父节点引用关系
            replacement.parent = p.parent;
            // 被删除节点为根节点的关系
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                // 被删除的节点是左节点
                p.parent.left  = replacement;
            else
                // 被删除的节点是右节点
                p.parent.right = replacement;

            // 断开被删除节点的所有引用关系
            p.left = p.right = p.parent = null;

            // 如果被删除节点为2-节点,需要执行再平衡操作
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) {
            // 红黑树只有一个节点的情况
            root = null;
        } else {
            // 删除叶子节点,如果该节点为2-节点那么需要再平衡修复
            if (p.color == BLACK)
                fixAfterDeletion(p);

            // 断开引用关系
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

    // 获取 t 节点的后继节点
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            // 右子树不为空,则右子树中最小的节点为该节点的后继节点
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            // ...
        }
    }

fixAfterDeletion 为删除后再平衡方法,需要大家关注其中的注释信息:

    private void fixAfterDeletion(Entry<K,V> x) {
        // 非根节点和当前节点是黑色的才需要修复平衡
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));

                // 情况 1
                if (colorOf(sib) == RED) {
                    // 反色处理:兄弟节点染黑,父节点染红
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    // 左旋父节点
                    rotateLeft(parentOf(x));
                    // 新的兄弟节点
                    sib = rightOf(parentOf(x));
                }

                // 情况 2
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    // 将兄弟节点染红后,x指向父节点引用,继续修复平衡
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    // 情况 3
                    if (colorOf(rightOf(sib)) == BLACK) {
                        // 交换颜色:兄弟节点左节点染黑,兄弟节点染红
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        // 右旋兄弟节点
                        rotateRight(sib);
                        // 新的兄弟节点
                        sib = rightOf(parentOf(x));
                    }
                    // 情况 4
                    // 交换兄弟节点和父节点的颜色
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    // 将兄弟节点的右节点由红染黑
                    setColor(rightOf(sib), BLACK);
                    // 左旋父节点
                    rotateLeft(parentOf(x));
                    // 满足红黑树的性质,指向根节点循环结束
                    x = root;
                }
            } else {
                // 以下是镜像操作
                Entry<K,V> sib = leftOf(parentOf(x));

                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib = leftOf(parentOf(x));
                }

                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib = leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(sib), BLACK);
                    rotateRight(parentOf(x));
                    x = root;
                }
            }
        }

        // 情况 2,x 为红色节点时跳出循环,需要将 x 节点染黑
        // 因为红黑树中不存在连续的红色节点
        setColor(x, BLACK);
    }

红黑树执行删除方法的时间复杂度是多少呢?含有 n 个节点的红黑树的高度为 logn,不调用fixAfterDeletion 方法时,复杂度为 O(logn),在 fixAfterDeletion 中,情况 1, 3, 4 在各执行常数次的颜色改变和至多 3 次旋转后便终止,只有在情况 2 中才可能重复修复平衡,指针也至多上升 O(logn) 次,且没有任何旋转操作,所以 fixAfterDeletion 复杂度为 O(logn),最多旋转 3 次,因此红黑树删除方法的时间复杂度为 O(logn)。


巨人的肩膀

  • 《算法导论》:第 13 章 红黑树

  • 知乎 - 关于AVL树和红黑树的一点看法

  • LeetCode - 红黑树从入门到看开

  • 博客园 - 红黑树的删除

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

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

相关文章

程序人生15年人生感悟

计算机程序员并不是一件什么高大上的职业。而仅仅是一份普通的工作。就像医生能治病救人&#xff0c;我们能治蓝屏救程序&#xff0c;我们都在为这个世界默默的做出自己的贡献。刻意或无意宣扬某个职业高大上&#xff0c;其实质是对其它行业从业者的不公平。但是有些人却常常这…

防火墙 设置 出站规则

测试需求&#xff1a;禁止10000端口出站&#xff0c;用于测试 搜索栏 &#xff0c;输入防火墙&#xff0c;打开防火墙和网络保护&#xff0c;打开高级设置 新建一个出站规则 新建规则 &#xff0c;自定义、 这样就建好了 同时确保 防火墙&#xff0c;至少有一个打开着&#xf…

java:微服务springcloud入门以及eureka、ribbon、hystrix、feign、gateway的使用

文章目录 微服务架构Spring Cloud微服务或Spring Cloud的工作流程&#xff1a;常见注册中心技术常见的负载均衡技术常见的熔断器技术常见的配置管理技术常见的网关技术常见的消息追踪技术常见的消息总线技术示例&#xff1a;服务注册发现eureka的使用Eureka服务器服务消费者&am…

深入探索Spring Batch:大规模批处理的领航者

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《linux深造日志》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 写在前面参与规则 ✅参与方式&#xff1a;关注博主、点赞、收藏、评论&#xff0c;任意评论&#xff08;每人最多评论…

鸿蒙 Ark ui 实战登录界面请求网络实现教程

团队介绍 作者&#xff1a;徐庆 团队&#xff1a;坚果派 公众号&#xff1a;“大前端之旅” 润开鸿生态技术专家&#xff0c;华为HDE&#xff0c;CSDN博客专家&#xff0c;CSDN超级个体&#xff0c;CSDN特邀嘉宾&#xff0c;InfoQ签约作者&#xff0c;OpenHarmony布道师&…

备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

MySQLhttps://www.mysql.com/ 将下发的ds_db01.sql数据库文件放置mysql中 12、编写Scala代码&#xff0c;使用Spark将MySQL的ds_db01库中表user_info的全量数据抽取到Hive的ods库中表user_info。字段名称、类型不变&#xff0c;同时添加静态分区&#xff0c;分区字段为etl_da…

(1)Linux的 安装与用户的创建

前言 本章正式开始Linux的学习 如果关于Linux环境搭配有问题的朋友 可以阅读此文章:Linux环境搭建 一&#xff0c;浅用一下吧 —— Hello, Linux! 我们现在已经登陆上了&#xff0c;我们当然可以用它来做很多事。 我们来用它写一个 "Hello, Linux!" &#xff0c;来…

Mysql 计算地理位置信息

mysql 处理空间关系 MySQL提供了一系列的函数来帮助我们处理空间对象之间的关系&#xff0c;如 ST_Intersects、ST_Contains 等。这些函数可以帮助我们判断空间对象之间的位置关系&#xff0c;并在此基础上执行相应的查询。 多边形查询 在实际应用中&#xff0c;需要查询某个…

八股文打卡day2——计算机网络(2)

面试题&#xff1a;讲一下三次握手的过程&#xff1f; 我的回答&#xff1a; 1.客户端发送报文段到服务器&#xff0c;主动建立连接。这个报文段中SYN标志位表示&#xff1a;这个报文段是用于连接的&#xff0c;此时SYN标志位设置为1。其中初始序列号字段包含了客户端的初始序…

Vue 项目关于在生产环境下调试

前言 开发项目时&#xff0c;在本地调试没问题&#xff0c;但是部署到生产会遇到一些很奇怪的问题&#xff0c;本地又没法调&#xff0c;就需要在生产环境/域名下进行调试。 在这里介绍一个插件Vue force dev ,浏览器扩展里下载 即便是设置了Vue.config.devtoolsfalse 只要安…

计算机网络:自顶向下第八版学习指南笔记和课后实验--运输层

记录一些学习计算机网络:自顶向下的学习笔记和心得 Github地址&#xff0c;欢迎star ⭐️⭐️⭐️⭐️⭐️ 运输层 TCP&#xff1a; 传输控制协议 报文段 UDP&#xff1a; 用户数据包协议 数据报 将主机间交付扩展到进程间交付被称为运输层的多路复用与多路分解 将运输层…

苹果手机录屏没声音?找出原因,轻松修复!

“有人知道苹果手机录屏为什么没声音吗&#xff1f;今天录屏的时候&#xff0c;发现录制的视频没有声音&#xff0c;反反复复试了好几次&#xff0c;还是一样的情况&#xff0c;真的不知道该怎么办了&#xff0c;有人能帮帮我吗。” 苹果手机的内置录屏功能为用户提供了方便的…

小程序地图检索

<template><view style"background-color: #f5f5f5"><!-- 头部开始 --><viewstyle"position: fixed;left: -5rpx;right: -5rpx;z-index: 99;top: -5rpx;width: 101vw;"><view style"position: relative"><view…

系统移植导学

什么是系统移植&#xff1f; 将操作系统移植到对应的硬件平台 linux系统移植到FS6818开发板 学习系统移植的目的&#xff1f; 1》浅图&#xff1a;为后面的驱动开发学习搭建一个系统环境 2》钱途&#xff1a;就业、工作的需要&#xff08;公司新的硬件平台---》移植linux系统到…

【普中】基于51单片机简易计算器数码管显示设计( proteus仿真+程序+实物演示+讲解视频)

【普中开发板】基于51单片机简易计算器数码管显示设计( proteus仿真程序实物演示讲解视频&#xff09; Proteus 仿真&#xff1a;Proteus 8.16(有低版本) 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;P04 1. 主要功能&#xff1a…

台灯选用什么类型好?精选适合考研党使用的护眼台灯

台灯可以说是我们日常办公、学习必不可少的一盏桌面照明工具&#xff0c;因为他能够给我们带来充足的光线。不过大多数时候&#xff0c;我们只关注到了台灯的亮度是否足够&#xff0c;以为只要光线够亮就不会上眼镜&#xff0c;但实际上我们大多数实力下降的原因就和光线有很大…

大数据技术之 Kettle(PDI)

Kettle 第一章 Kettle概述1.1、ETL简介1.2、Kettle简介1.3、作业 和 转换 概念1.4、核心组件1.5、下载安装 第二章 控件使用2.1、初体验&#xff1a;csv 转换 excel 示例2.2、转换2.2.1、输入控件2.2.1.1、表输入 2.2.2、输出控件2.2.2.1、表输出2.2.2.2、更新&插入/更新2.…

前后端传参:掌握FormData并解决form-data类型传参问题

目录 第一章 解决问题过程 第二章 对form-data的理解 2.1 使用场景 2.2 了解formData对象的创建与使用 2.3 formData常用方法 2.3.1 构造函数 2.3.2 获取数据 2.3.3 添加数据 2.3.4 修改数据 2.3.5 检查是否有该数据 2.3.6 删除数据 2.3.7 遍历formData里的键值对…

极狐GitLab DevSecOps 之容器镜像安全扫描

容器镜像安全 现状 最近某银行遭受供应链攻击的事件传的沸沸扬扬&#xff0c;安全又双叒叕进入了人们的视野。安全确实是一个非常重要&#xff0c;但是又最容易被忽略的话题。但是现在到了一个不得不人人重视安全&#xff0c;人人为安全负责的时代。尤其以现在非常火爆的云原…

Mybatis概述和快速入门

文章目录 Mybatis概述Mybatis概念JDBC 缺点Mybatis 优化 Mybatis快速入门 Mybatis概述 Mybatis概念 &#xff08;1&#xff09;Mybatis是一个半ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;它内部封装了JDBC&#xff0c;开发时只需要关注SQL语句本身&#xff0c…