DAY24:二叉树(十四)二叉搜索树中的插入操作+删除二叉搜索树中的节点(二叉树结构修改难点)

news2024/10/6 8:30:45

文章目录

    • 701.二叉搜索树中的插入操作
      • 思路
      • 递归法
        • 如何保证连接的节点就是空节点的父节点?
      • 迭代法
        • 迭代法注意
        • debug测试
    • 450.删除二叉搜索树中的节点(坑较多,注意复盘)
      • 思路
      • 最开始的写法
      • debug测试
        • 1.使用了释放后的空间ERROR: AddressSanitizer: heap-use-after-free on address
        • 2.if-else if-else的问题
        • 3.c++释放内存的问题
          • 二叉树的节点默认创建在堆上的问题
        • 4.逻辑问题:要找的是右子树最左下角的节点,不仅仅是右子树的左节点
        • 5.奇怪的递归错误
      • 修改后的完整版
        • 连接的问题
      • 普通二叉树的版本
      • 普通二叉树与BST的删除操作,时间复杂度区别

701.二叉搜索树中的插入操作

  • 本题要注意思路,首先,插入节点一定是放在叶子节点上;第二,插入节点是找到空节点之后建立新节点,再把这个新节点返回给上一层;第三,上一层的节点需要把新节点进行连接。
  • 注意递归插入连接节点的时候的逻辑,不管连接节点是不是新建的节点,都需要把返回的节点和上一层连接。对于已经连接的节点,再次连接不会有问题;而对于未连接的新建节点,需要连接到上一层里

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。

在这里插入图片描述
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:

在这里插入图片描述

示例 2:

输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]

示例 3:

输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]

在这里插入图片描述

思路

我们只需要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。

注意,我们插入任意一个节点,其实都可以在叶子节点里找到对应位置!因为BST本身就是有序的,大于和小于都会体现在元素叶子节点里,所以插入的新元素一定是在叶子节点上

画几个例子试一下就会发现这一点了。

新插入节点,在叶子节点插入就可以了
在这里插入图片描述

递归法

  • 当遇到空的时候,说明找到了插入节点的位置!
  • 二叉树插入节点的方式是定义新节点,然后给节点赋值,再把新建立的节点向上一层返回!
  • 向上一层return 在递归的过程中,节点7向左遍历遇到null空节点return了一个新的节点,此刻7就接收到了这个节点
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        //遇到空的时候,说明找到了插入的位置!递归终止
        //只有遇到空的时候,才会有返回值
        if(root==nullptr){
            //建立新节点
            TreeNode* node = new TreeNode(val);
            //新建立的节点向上一层返回
            return node;
        }
        //如果值比较小,插入左子树
        if(root->val > val){
            //接收下层返回的节点
            TreeNode* left = insertIntoBST(root->left,val);
            //把节点连接起来
            root->left = left;
        }
        //值比较大,插入右子树
        if(root->val < val){
            TreeNode* right = insertIntoBST(root->right,val);
            root->right = right;
        }
        //每一层都会返回,连接上层节点
        return root;

    }
};

如何保证连接的节点就是空节点的父节点?

函数 insertIntoBST 总是返回一个 TreeNode 指针。

当我们找到了一个空的节点(也就是找到了插入位置),会创建一个新的节点,并返回这个新节点。但是,在这之前,我们可能已经在二叉树中遍历了很多节点。这些节点在遍历过程中都是被返回的,因为最后return root。也就是说,当调用 insertIntoBST(root->left, val)insertIntoBST(root->right, val) 时,如果子树 root->leftroot->right 不是空的,那么返回的就是这个子树的根节点。这是因为我们没有在这个子树中插入新的节点,所以原来的子树没有发生改变。

也就是说,如果 insertIntoBST(root->left,val)insertIntoBST(root->right,val) 返回了一个节点,那么这个节点要么是新插入的节点,要么就是原来的子树的根节点。在两种情况下,都需要把返回的节点连接到 root 节点上,因为这样可以保证树的结构

  • 这种逻辑能跑通的原因就是,即使root->left已经存在,把root->left和root重新连接也是没有问题的,即执行root->left = left,对于left本身就是root左孩子的情况,这也是不会有问题的。
  • 如果 root->left 已经存在,那么 insertIntoBST(root->left,val) 返回的就是 root->left因为在这个子树中没有插入新的节点。因此,当执行 root->left = left 时,只是把 root->left 指向它原来就指向的节点,这是完全没有问题的。同样的道理也适用于 root->right
  • 二叉树的递归插入算法要确保所有的节点都被正确地连接。对于已经正确连接的节点,再次连接不会产生任何副作用而对于新插入的节点,这个连接操作就会把新节点正确地连接到树中

迭代法

  • 本题的迭代法用的依然是两个指针pre和front的做法,一个指向当前一个指向当前的前一个,再进行连接
  • 迭代法的下层逻辑里面,一定要避免出现root,因为迭代法的root和递归不一样,迭代的root指的就是单纯的根节点
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root==nullptr){
            TreeNode* node = new TreeNode(val);
            return node;
        }
        //需要定义一个指针存放当前节点的前一个节点
        TreeNode* pre = root;
        TreeNode* front = root;
        while(pre!=nullptr){
            front = pre;//存放变化之前的数值
            if(val > pre->val){
                pre = pre->right;
            }
            else{
                //题目里说了新数值不等于任意一个值
                pre = pre->left;
            }
        }
        //当跳出while循环之后,说明遍历到空节点
        TreeNode* node = new TreeNode(val);
        if(front->val < val){
            front->right = node;
        }
        else{
            front->left = node;
        }
        //迭代法直接修改了二叉树,所以返回root就行
        return root;
    }
};

迭代法注意

//迭代法的逻辑里一定要尽量避免root,迭代法的root并不像递归一样指的是每一层的节点,迭代法的root指的就是根节点!			
if(val > pre->val){
    pre = pre->right;
}

这里一开始写成了pre = root->right,导致运行超时。也就是一直在while循环里面走。因为迭代法里root->right是不变的。

迭代法里面需要写成pre = pre->right,这里要特别注意不要和递归写混了。

debug测试

运行超时的错误就是死循环了,需要重点检查循环
在这里插入图片描述
因为迭代法逻辑里面pre写成了root,导致预期输出出现了很奇怪的错误,就是少了一个null。

这种情况的报错光看用例输出是看不出来的,需要重新去看代码的逻辑是不是出了问题,比如迭代法的当前节点是不是想当然地写成递归的root了

450.删除二叉搜索树中的节点(坑较多,注意复盘)

  • 本题注意删除的方法,可参考链表删除操作父节点直接指向其左/右孩子(左右孩子只有一个的情况)
  • 本题需要注意的点很多,删除节点涉及到结构的大改,需要多复盘

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

首先找到需要删除的节点;
如果找到了,删除它。

输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。
在这里插入图片描述
示例 2:

输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

输入: root = [], key = 0
输出: []

在这里插入图片描述

思路

二叉搜索树添加节点的思路比较简单,因为添加节点不需要改二叉树的结构。

但是,删除节点,涉及到结构调整的问题。比如示例1,删除了节点3之后,需要处理节点3的两个左右孩子,把他们其中一个重新变成节点3原来的位置,并且保证还是BST。

分情况讨论

  • 没找到要删除的key值,直接返回原来的root

  • 要删除的节点是叶子节点,那么直接删除,不需要改结构

  • 要删除的节点是左不为空右为空的节点,那么就把左子树的节点直接填补上来即可,也就是父节点直接指向左孩子

  • 要删除的节点是右不为空左为空的节点,那么把右子树的节点直接填补上来就行,也就是父节点直接指向右孩子

  • 要删除的节点是左右都不为空的节点,这是最复杂的情况,此时要判断左右孩子的大小,以及哪个孩子需要来填补空缺的位置。

    示例:假如我们要删除节点7:
    在这里插入图片描述
    7被删除后,7的左右孩子都可以继位,我们选择右孩子9来继位。我们也可以让右孩子的左孩子8来继位,但是右孩子直接继位简单一些,因为右孩子并不是一定都有左孩子,但是待删除节点运行到这一步了,一定有右孩子。

右孩子继位之后,左孩子放在哪里呢?

在这里插入图片描述
由于7的左子树全部都<7,所以我们需要选择一个大于7,但是不能大于继位的右孩子9的元素,也就是右孩子的左子树!我们可以把7的左子树移动到7的右孩子的左子树中。

此处注意,右子树的左孩子找到空的节点,我们就可以直接把7的左子树移动过来!

在这里插入图片描述
注意:为了避免覆盖的问题,必须找到右子树最左下角的左孩子,此时该左孩子必须是叶子节点,否则会发生覆盖

我们没有选择直接用右子树左孩子去继位,就是因为右子树左孩子可能不存在。但是右孩子继位的话,即使其左孩子不存在,也可以直接把左子树移动过来。

最开始的写法

  • cpp删除节点需要手动释放内存
  • 右子树的左孩子即使是空的也可以直接移过来
  • 注意:树中找不到节点的情况,已经被包含在if(root==nullptr)return nullptr;的逻辑里面了!遍历的过程就是这棵树如果都不满足
  • 并不是所有的BST都必须中序遍历,涉及到单调递增才需要,本题可以前序
//bool travelsal(TreeNode* root,int val){
    //注意:不需要单独的遍历函数!
//}

TreeNode* deleteNode(TreeNode* root, int key){
    //先找到节点,第一种情况是找不到,如果下面的情况都不满足,递归到最后就会返回空
    if(root==nullptr){
        return nullptr;
    }
    //此时是找到了
    if(root->val==key){
        //叶子节点,左右都为空
        if(root->left==nullptr&&root->right==nullptr){
            delete root;
            return nullptr;
        }
        //左不为空右为空的节点
        if(root->left!=nullptr&&root->right==nullptr){
            delete root;
            return root->left;//把左孩子返回给父节点进行连接
        }
        //左为空右不为空的节点
        if(root->right!=nullptr&&root->left==nullptr){
            delete root;
            return root->right;//右孩子返回给父节点
        }
        //左右都不为空的节点
        //如果右节点的左孩子存在,就让左子树移动到右节点左孩子下面
        if(root->right->left){
            root->right->left = root->left;
        }
        //右节点左孩子不存在,让左子树移动为右节点的左孩子
        if(!root->right->left){
            root->right->left = root->left;
        }
        //右节点继位
        return root->right;
    }
    //遍历左侧和右侧
    TreeNode* left = deleteNode(root->left, key);
    TreeNode* right = deleteNode(root->right, key);
    if(left){
        root->left = left;
    }
    if(right){
        root->right = right;
    }
    return root;
    
}

debug测试

1.使用了释放后的空间ERROR: AddressSanitizer: heap-use-after-free on address

在这里插入图片描述
内存错误"heap-use-after-free",这是因为在C++中,当使用delete关键字释放对象的内存后,该对象仍然会保留指向已经被释放内存的指针。这个指针称为悬挂指针(Dangling Pointer)。如果我们试图访问已经被释放的内存,就会触发"heap-use-after-free"错误。

错误的写法中,我们检查了 root->leftroot->right 是否为空,然后删除了 root,但是试图返回 root->left,这就使得 root 成为一个悬挂指针,因为它的内存已经被释放,但你仍然试图通过它访问内存

在最开始的写法中,我们把root delete掉了,再调用root->right就会出现内存报错。这是一个非常常见的编程错误,我们需要确保不再使用任何你已经释放的内存。

错误写法:

if(root->left!=nullptr&&root->right==nullptr){
            delete root;
            return root->left;//把左孩子返回给父节点进行连接
        }

修改:

  • 删除之前保存需要返回的和删除节点相关的值
if(root->left!=nullptr&&root->right==nullptr){
    //删除之前保存需要返回的和删除节点相关的值
    		TreeNode* node = root->left;
            delete root;
            return node;//把左孩子返回给父节点进行连接
        }

“悬挂指针”(Dangling Pointer)是一种常见的编程错误,它发生在当一个指针指向的内存已经被释放或者已经超出范围时。在这种情况下,指针仍然存在,但它所指向的内存可能已经被操作系统重新分配给其他地方,或者根本就不能访问。如果试图通过悬挂指针访问这块内存,就可能会导致未定义的行为,比如程序崩溃或者数据损坏。

2.if-else if-else的问题

在删除节点后,继续使用了已删除的节点,这会导致不确定的行为。例如,首先删除 root,然后尝试访问 root->leftroot->right。这在C++中是不允许的。所以这里最好用if-else if-else的结构才能避免操作空节点

但是,我们的写法,全写if也是没有问题的,因为每一个if里面都有return语句,所以满足一个if的时候,直接return出去了,下面的都不执行了。在执行上,是和if-else if-else没有区别的。

3.c++释放内存的问题

delete 操作是在处理 C++ 中的动态内存管理。在 C++ 中,使用 new 来动态创建对象,相应的,当这个对象不再需要的时候,就需要使用 delete 来释放掉它占用的内存。否则,如果只是简单地丢弃了指向它的指针,那么这部分内存将无法再次使用,这就产生了内存泄漏

本题中,当找到了需要删除的节点 root 时,会用一个新的节点 node 来取代它,并返回 node,这个过程就是删除节点。但是,只是这样做的话,被删除的 root 节点实际上并没有被真正地删除,它仍然占用着内存。因此,需要使用 delete root; 来真正地释放 root 所占用的内存

这样做可以确保程序不会因为无法释放内存而出现问题。在处理大量数据或长时间运行的程序中,内存管理尤为重要,因为如果内存泄漏累积到一定程度,会导致程序运行缓慢,甚至崩溃

二叉树的节点默认创建在堆上的问题

我们默认它的输入是一个在堆上动态创建的二叉搜索树的节点。在C++中,二叉树的节点并不一定都在堆上。不过在实际应用中,通常会在堆上创建二叉树的节点,因为二叉树通常会包含大量的节点,如果全部创建在栈上,可能会导致栈溢出。而且,二叉树的节点数量在创建时可能无法确定,所以使用动态内存分配来创建节点会更加灵活

4.逻辑问题:要找的是右子树最左下角的节点,不仅仅是右子树的左节点

错误代码:

  • delete的操作必须提前把元素值存一下
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
    //先找到节点,第一种情况是找不到,如果下面的情况都不满足,递归到最后就会返回空
    if(root==nullptr){
        return nullptr;
    }
    //此时是找到了,注意这里面必须用else if,因为会操作root,如果root已经被删除就没有意义
    if(root->val==key){
        //叶子节点,左右都为空
        if(root->left==nullptr&&root->right==nullptr){
            delete root;
            return nullptr;
        }
        //左不为空右为空的节点
        else if(root->left!=nullptr&&root->right==nullptr){
            TreeNode* node = root->left;
            delete root;
            return node;//把左孩子返回给父节点进行连接
        }
        //左为空右不为空的节点
        else if(root->right!=nullptr&&root->left==nullptr){
            TreeNode* node = root->right;
            delete root;
            return node;//右孩子返回给父节点
        }
        //左右都不为空的节点
        else{
            //如果右节点的左孩子存在,就让左子树移动到右节点左孩子下面
            if(root->right->left){
                root->right->left = root->left;
            }
            //右节点左孩子不存在,让左子树移动为右节点的左孩子
            if(!root->right->left){
                root->right->left = root->left;
            }
            //右节点继位
            return root->right;
        }
        
    }
    //遍历左侧和右侧
    TreeNode* left = deleteNode(root->left, key);
    TreeNode* right = deleteNode(root->right, key);

    //最开始超时报错是因为没有遍历下去
    if(root->val > key){
        root->left = left;
    }
    if(root->val < key){
        root->right = right;
    }
    return root;

    }
};

在这里插入图片描述
这里存在的问题是,我们要找的并不是右子树的左孩子,而是右子树最左下角的孩子,因为右子树可能有很多层,为了防止覆盖掉原有的元素,必须遍历到叶子节点才行!

也就是说,左右孩子都不为空的逻辑,应该改成:

		//左右都不为空的节点
        else{
            //找到右子树最左下角的节点,一直找到空的位置!
            TreeNode* cur = root->right;
            while(cur->left!=nullptr){
                cur = cur->left;
            }
            //当cur->left是空的时候,左子树移动到cur->left
            cur->left = root->left;
            //右节点继位
            TreeNode* node = root->right;
            delete root;
            return node;
        }

5.奇怪的递归错误

在这里插入图片描述
最开始的写法,在每次递归调用后,都更新了 root->leftroot->right,即使它们并没有发生改变。这将导致无限递归,因为总是在原地进行修改,而不是向下递归。

//遍历左侧和右侧,错误写法,没有递归下去
    TreeNode* left = deleteNode(root->left, key);
    TreeNode* right = deleteNode(root->right, key);
    if(left){
        root->left = left;
    }
    if(right){
        root->right = right;
    }

修改成如图
在这里插入图片描述
或者

在这里插入图片描述
这种递归写法很奇怪,错误也很奇怪,现在也没有搞懂为什么会内存报错,建议是一定要避免写这种奇怪的递归!!

修改后的完整版

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
    //如果找不到,递归到最后返回空
    if(root==nullptr){
        return nullptr;
    }
    //找到了,注意这里面必须用else if,因为会操作root,如果root已经被删除就没有意义
    if(root->val==key){
        //叶子节点,左右都为空
        if(root->left==nullptr&&root->right==nullptr){
            delete root;
            return nullptr;
        }
        //左不为空右为空的节点
        else if(root->left!=nullptr&&root->right==nullptr){
            //因为要delete掉来释放内存,所以先用node存储root,再delete root
            TreeNode* node = root->left;
            delete root;
            return node;//把左孩子返回给父节点进行连接
        }
        //左为空右不为空的节点
        else if(root->right!=nullptr&&root->left==nullptr){
            TreeNode* node = root->right;
            delete root;
            return node;//右孩子返回给父节点
        }
        //左右都不为空的节点
        else{
            //找到右子树最左下角的节点,一直找到空的位置!
            TreeNode* cur = root->right;
            while(cur->left!=nullptr){
                cur = cur->left;
            }
            //当cur->left是空的时候,左子树移动到cur->left
            cur->left = root->left;
            //右节点继位
            TreeNode* node = root->right;
            delete root;
            return node;
        }
        
    }
        
    //遍历左右侧,确认key该往哪个方向去找
    if(root->val > key){
        //key在左子树里,接收并连接左子树节点
        root->left = deleteNode(root->left, key);
    }
    if(root->val < key){
        root->right = deleteNode(root->right, key);
    }
    return root;

    }
};

连接的问题

我们直接使用root->left = deleteNode(root->left, key);就可以完成连接,并不需要单独定义left变量来接收返回值。

普通二叉树的版本

普通二叉树和BST的写法区别仅仅在于递归遍历。普通二叉树找key的时候需要遍历整棵树

BST的写法

 	//遍历左右侧,确认key该往哪个方向去找
    if(root->val > key){
        //key在左子树里,接收并连接左子树节点
        root->left = deleteNode(root->left, key);
    }
    if(root->val < key){
        root->right = deleteNode(root->right, key);
    }
    return root;

递归遍历的部分修改成:

//普通的前序遍历+返回的节点连接
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);

即可。
在这里插入图片描述

普通二叉树与BST的删除操作,时间复杂度区别

对于二叉搜索树(BST)和普通二叉树的时间复杂度,区别在于:

  1. 对于 BST,如果我们知道树的高度 h,那么删除操作的最坏情况下时间复杂度为 O(h),因为我们总是沿着树的高度进行搜索。如果树是平衡的(即 AVL 树或红黑树),那么 h = log(n),n 是节点的数量,所以时间复杂度为 O(log(n))。否则,如果树完全不平衡(例如,每个节点都只有一个孩子),那么 h = n,所以时间复杂度为 O(n)
  2. 对于普通二叉树,我们可能需要遍历整个树才能找到要删除的节点,所以最坏情况下的时间复杂度为 O(n),其中 n 是节点的数量。

因此,对于删除操作,BST 通常比普通二叉树更有效率,特别是当树保持较好的平衡时。

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

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

相关文章

通知!2023年湖北住建厅七大员新考和继续教育要求有变化了?启程别

通知&#xff01;2023年湖北住建厅七大员新考和继续教育要求有变化了&#xff1f;启程别 湖北住建厅七大员新考以及继续教育的相关要求都即将有一些变化了目前在征集意见的阶段&#xff0c;具体实施等后续具体通知 对于新考的变化主要是&#xff1a; 1.由原先报名之后只需要完成…

Vue中如何进行颜色选择与调色板

Vue中如何进行颜色选择与调色板 颜色选择和调色板是Web开发中常用的功能&#xff0c;它们可以帮助用户选择或调整颜色。Vue作为一个流行的JavaScript框架&#xff0c;提供了一些工具和库&#xff0c;可以方便地实现颜色选择和调色板功能。本文将介绍如何在Vue中进行颜色选择和…

【aspose-words】Aspose.Words for Java模板语法详细剖析

文章目录 前言&#x1f34a;缘由aspose-words模板语法再了解 &#x1f3af;主要目标实现3大重点 &#x1f381;快速链接&#x1f348;猜你想问如何与狗哥联系进行探讨1.关注公众号【JavaDog程序狗】2.踩踩狗哥博客 &#x1f36f;猜你喜欢文章推荐 正文&#x1f34b;aspose-word…

mfc140.dll丢失的解决方法,解析mfc140.dll这个文件

其实大部分人在使用计算机过程中&#xff0c;您可能会遇到mfc140.dll丢失的错误提示。这个错误会导致一些应用程序无法正常运行&#xff0c;那么要如何解决这个问题呢&#xff1f;今天小编就来给大家详细的解析mfc140.dll这个文件以及教大家 mfc140.dll丢失的解决方法。 目录 …

【算法与数据结构】349、LeetCode两个数组的交集

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;使用了一个哈希数组记录nums1出现的数字&#xff0c;然后遍历nums2&#xff0c;找到hash数组为1的值就…

Vue-Element-Admin项目学习笔记(6)Vuex状态管理

前情回顾&#xff1a; vue-element-admin项目学习笔记&#xff08;1&#xff09;安装、配置、启动项目 vue-element-admin项目学习笔记&#xff08;2&#xff09;main.js 文件分析 vue-element-admin项目学习笔记&#xff08;3&#xff09;路由分析一:静态路由 vue-element-adm…

yoloV5目标识别以及跟踪,功能识别动物(大象,犀牛,水牛,斑马)

yolo目标识别以及跟踪还是很强的嘞&#xff01; 一. YOLO V5我来啦 1. 前期准备 yolo V5项目下载 项目的github地址项目的gitee地址 使用git 克隆下来到项目目录下面就好 环境配置 在yolov5的文件下面有一个 requirements.txt文件,这里就是环境依赖的说明。 这里我以 vs…

DP学习之解码方法

DP学习第二篇之解码方法 91. 解码方法 - 力扣&#xff08;LeetCode&#xff09; 一. 题目解析 二. 题解 算法原理及代码 状态表示 tips: 经验题目要求。以i位置为结尾&#xff0c;。。。 dp[i]: 以i位置为结尾时&#xff0c;解码方法的总数 状态转移方程 tips: 用之前或…

5.3.2 因特网的路由协议(二)基于距离向量算法的RIP协议

5.3.2 因特网的路由协议&#xff08;二&#xff09;基于距离向量算法的RIP协议 一、RIP协议概念 RIP是Routing Information Protocol缩写&#xff0c;又称为路由信息协议&#xff0c;是最先得到应用的内部网关协议&#xff0c;RIP作为一个常在小型互联网中使用的路由信息协议…

【mmcls】mmdet中使用mmcls的网络及预训练模型

mmcls现在叫mmpretrain&#xff0c;以前叫mmclassification&#xff0c;这里为了统一称为mmcls。在基于MM框架的下游任务&#xff0c;例如检测(mmdetection)中可以使用mmcls中的backbone进行特征提取&#xff0c;但这就需要知道网络的参数以及输出特征的维度。本文简单介绍了在…

CDD诊断数据库的简单介绍

1. 什么是数据库? 数据库是以结构化方式组织的一个数据集合。 比如DBC数据库: Network nodes Display Rx Messages EngineState(0x123) 通过结构化的方式把网络节点Display里Rx报文EngineState(0x123)层层展开。这种方 式的好处是:层次清晰,结构分明,易于查找。 2. 什么…

ERROR: AddressSanitizer: heap-use-after-free on address

内存错误"heap-use-after-free"&#xff0c;这是因为在C中&#xff0c;当使用delete关键字释放对象的内存后&#xff0c;该对象仍然会保留指向已经被释放内存的指针。这个指针称为悬挂指针&#xff08;Dangling Pointer&#xff09;。如果我们试图访问已经被释放的内…

【Linux】15. 文件系统与软硬链接

1. 文件系统的引出 在之前的学习过程当中&#xff0c;我们知道当文件被打开后需要加载进内存&#xff0c;第一步为其创建struct file结构体描述其结构(操作系统需要管理被打开的文件&#xff1a;先描述再组织)&#xff0c;在通过进程当中的文件描述符指针指向文件描述符表&…

《机器学习算法竞赛实战》-chapter2问题建模

《机器学习算法竞赛实战》学习笔记&#xff0c;记录一下自己的学习过程&#xff0c;详细的内容请大家购买作者的书籍查阅。 问题建模 当参赛者拿到竞赛题目时&#xff0c;首先应该考虑的事情就是问题建模&#xff0c;同时完成基线(baseline)模型的pipeline搭建&#xff0c;从…

芯片工程师平均薪酬排第一,入行就学这几个热门专业>>>

高考已经结束&#xff0c;对于广大考生来说&#xff0c;考一个理想的分数固然重要&#xff0c;但高考志愿的填报同样事关重大&#xff0c;它决定未来几年考生的学习走向&#xff0c;也会影响到考生未来职业生涯的长远发展。目前&#xff0c;北京、江苏、河南、湖南、海南、甘肃…

Spring Boot进阶(45): Spring Boot 如何返回统一结果包装?一文教会你 | 超级详细,建议收藏

1. 前言&#x1f525; 现如今是前后端分离的时代&#xff0c;如果没有统一的返回格式&#xff0c;给前端的结果各式各样&#xff0c;估计前端小伙伴就要骂街了。我们想对自定义异常抛出指定的状态码排查错误&#xff0c;对系统的不可预知的异常抛出友好一点的异常信息。我们想让…

Java基础重点概要(部分)

为工信部第六届全国计算机信息大赛准备 &#xff0c;主要复习以下内容。 Java基础及环境&#xff1a;JDK发展历史&#xff0c;不同版本的进阶内容。Java程序的编写、编译、调试。 Java程序设计基础&#xff1a;常量和变量的概念&#xff0c;声明方式和作用域。基本数据类型的定…

基于Java学校运动会管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

C语言之指针详解(4)

目录 本章重点 1. 字符指针 2. 数组指针 3. 指针数组 4. 数组传参和指针传参 5. 函数指针 6. 函数指针数组 7. 指向函数指针数组的指针 8. 回调函数 9. 指针和数组面试题的解析 函数指针 数组指针—指向数组的指针 函数指针—指向函数的指针 函数指针 我们来看代码 #…

基于人工智能,现代数据基础架构的新兴架构

作者 Matt Bornstein、Jennifer Li和Martin Casado 摘要 现代机器学习基础设施2.0新架构&#xff1a; http://bit.ly/3AVBpV6 这个图概括了机器学习基础设施2.0的主要组成部分。它涵盖了从数据转换到模型集成的全过程。每个阶段的具体工具和技术也在括号中列出。 结构解读 …