Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

Re:从零开始的 C++ STL篇(七)二叉搜索树增删查操作系统讲解(含代码)+key/key-value场景联合分析

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰

0.1概要&序論

这里是「此方」,好久不见。 今天我们要学习的是二叉搜索树。它是在普通二叉树的基础上加入特定约束,从而具备了高效的搜索能力。虽然这种结构能够支持高效的插入、删除与查找操作,但其性能背后也隐藏着潜在的 效率风险 。同时,在 key 与 key-value 两种不同的应用场景 下,二叉搜索树的设计与实现方式也会产生不同的变化。这里是「此方」。让我们现在开始吧!

前情提要,没有系统学习过一般二叉树的小伙伴直接看这篇文章可能会有些吃力,此方在这里留一个传送门:Re:从零开始的链式二叉树:建树、遍历、计数、查找、判全、销毁全链路实现与底层剖析

一,二叉搜索树的概念

1.1二叉搜索树的概念和性质

二叉搜索树(英文名:Binary Search Tree, BST),又称二叉排序树。它或者是一棵空树,或者是具有以下性质的二叉树:

左子树性质:若它的左子树不为空,则左子树上所有结点的值都小于(或小于等于)根结点的值。
右子树性质:若它的右子树不为空,则右子树上所有结点的值都大于(或大于等于)根结点的值。
递归性质:它的左右子树也分别为二叉搜索树。

光讲概念肯定不好理解,我们画两个图看看:

在这里插入图片描述


如上图就是搜索二叉树的模型,根8左边子树上的数都比8小(左子树性质),右边子树上的数都比8大(右子树性质)。并且这种性质在3为根的左子树,6为根的左子树等等都成立(递归性质)。

如右图,搜索二叉树除了左边这种每一个键值不重复的情况,还有右边这颗树一样的重复键值情况。这个我们会在接下来讲到:

1.2关于相等值的处理

二叉搜索树中是否支持插入相等的值,取决于具体的实现和使用场景:
1.不允许重复
:某些实现(如 C++ STL 中的 set 和 map)不允许插入与已有节点值相等的节点。
2.允许重复:另一些实现(如 C++ STL 中的 multiset 和 multimap)则支持插入相等的值,这些值通常会被放置在同一子树(通常是右子树)上。

set,map,multiset 和 multimap这些STL我们会在接下来的文章中详细介绍。他们的底层是平衡二叉树中的红黑树。

二,二叉搜索树的性能分析

2.1二叉搜索树的性能与其结构形态密切相关

最优情况:当二叉搜索树为一棵完全二叉树(或高度平衡的树)时,其高度为 log₂N。此时,查找、插入、删除操作的时间复杂度均为 O(log N)。
最差情况
:当二叉搜索树退化为一个链表(例如,插入的数据本身就是有序的),其高度为 N。此时,查找、插入、删除操作的时间复杂度均退化为 O(N)。

还是用图片解释概念,如下左图,二叉树接近完全二叉树,增删查的时间复杂度为logN。如右图,二叉树接近一个链表,增删查的时间复杂度均退化为 O(N)。
恐怕有小伙伴不知道这个链表怎么来的,这里还是补充一下为妙
:有序序列[10,9,8,7,6,5,4,3,2,1]构建二叉树:根为10,9<10插在10的左边,8<9插在9的左边,以此类推序列后面的数只能插在父亲结点的左边,最后导致整个搜索二叉树呈现出一条链表状态。

在这里插入图片描述

综合来看,二叉搜索树各种操作的平均时间复杂度为 O(N)。这样的效率在最坏情况下无法满足实际需求。

因此,在后续的学习中,我们需要了解二叉搜索树的改进版本——平衡二叉搜索树,如 AVL 树和红黑树。这些结构通过特定的规则确保树的高度始终保持在 O(log N) 的范围内,从而保证了高效的增删查改性能。

2.2二叉搜索树对二分查找的优势

平衡二叉搜索树的优势可以通过与二分查找的对比来理解。虽然二分查找也能达到 O(log N) 的查找效率,但它存在两个主要缺陷

存储限制:必须将数据存储在支持下标随机访问的结构(如数组)中,并且要求数据是有序的(或者有一定规律可言)
更新效率低(最难受的点):在支持随机访问的结构中插入和删除元素,通常需要移动大量数据,导致效率低下。

而平衡二叉搜索树恰好弥补了这两个缺陷,它无需连续存储空间,且能高效地完成动态数据的插入、删除和查找操作。

——以下正式开始讲二叉搜索树的增删查操作实现原理——

三,实现框架结构

先搭个框架方便接下来好写代码。

#pragmaonce#include<iostream>usingnamespace std;namespace key{template<classK>classSBTnode{public: K _key; SBTnode<K>* left; SBTnode<K>* right;SBTnode(const K& key):_key(key),left(nullptr),right(nullptr){}};template<classK>classSBTree{using Node = SBTnode<K>;public:///………………protected: Node* _root =nullptr;};}

准备两个模板类,一个是搜索二叉树的结点模板类,一个是搜索二叉树的功能实现模板类。

3.1二叉搜索树节点模板类SBTnode

类模板参数 K 表示节点中存储的数据类型,使该节点结构可以适用于任意类型的数据。
节点内部包含三个成员:_key 用于存储节点的关键字(数据),left 和 right 分别指向当前节点的左子节点和右子节点。
构造函数 SBTnode(const K& key) 接收一个键值 key,用于初始化节点的数据成员 _key,同时将左右孩子指针 left 和 right 初始化为空,表示新创建的节点在初始状态下没有子节点。

3.2二叉树功能实现类SBTree

  1. 重命名一个结点为Node,可以用typedef也可以using(C++11新增)
  2. 组合进来一个根结点指针 并添加缺省值Node* _root = nullptr;

四,二叉搜索树的查找

4.1基本思想与代码实现

从根节点开始比较,查找值 x:

若 x 大于根节点的值,则向右子树查找;若 x 小于根节点的值,则向左子树查找。最多查找树的高度次,若遍历到空节点仍未找到目标值,则说明该值不存在。
在这里插入图片描述

代码实现如下: (非常简单,不细讲了。)

Node*Find(const K& key){ Node* cur = _root;while(cur){if(cur->_key < key) cur = cur->right;elseif(cur->_key > key) cur = cur->left;elsereturn cur;}returnnullptr;}

4.2特别情况分析

如果不支持插入相等的值(即不允许重复),则在找到第一个等于 x 的节点时即可返回。
如果支持插入相等的值(即允许重复),则可能有多个值为 x 的节点。此时通常要求查找中序遍历下的第一个 x。
(例如:查找值为 3,需要找到在中序序列中首次出现的 3,即某个节点(如值为 1 的节点)的右孩子为 3 的那个节点并返回。)

关于重复值查找的规则C++并没有做出规定,属于未指定行为,像multimap,multiset这些容器在这方面的实现取决于编译器。

五,二叉搜索树的插入

5.1具体过程图文解释

插入的具体过程如下:

  1. 树为空,则直接新增结点,赋值给root指针。
  2. 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  3. 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走

我们用一个例子解释一下原理:

在这里插入图片描述

如上有一颗二叉搜索树,我们这个时候要插入一个数16。如下图:按照二叉树的性质不断向下查找到指定位置并插入。

在这里插入图片描述

如下图,当我们要插入一个重复的数字的时候,可以规定他在原有的重复数字的左子树或者是右子树。 然后用二叉树的规则同样进行查找并插入。

在这里插入图片描述

5.2代码实现详细解析

稍微有点复杂,我们分块儿讲。具体可以将代码分成三部分:

1.查找插入位置,2.实现插入,3.特殊情况处理

5.2.1查找插入位置

从根节点开始,根据 key 与当前节点 cur->_key 的大小关系不断向左或向右查找,同时记录父节点 parent。当 cur 走到 nullptr 时,说明找到插入位置;若遇到相同 key,则返回 false 表示不允许重复插入。

boolInsert(const K& key){ Node* cur = _root; Node* parent =nullptr;while(cur){if(cur->_key < key){ parent = cur; cur = cur->right;}elseif(cur->_key > key){ parent = cur; cur = cur->left;}elsereturnfalse;}}

5.2.2实现插入

当查找结束后,parent 指向待插入位置的父节点,此时创建新节点 new Node(key)。随后再次比较 key 与 parent->_key 的大小关系:若 key 小于父节点,则插入为左孩子;若大于父节点,则插入为右孩子。这样即可保持二叉搜索树左子树小于根、右子树大于根 的有序结构。

cur =newNode(key);if(parent->_key > key) parent->left = cur;elseif(parent->_key < key) parent->right = cur;returntrue;

5.2.3特殊情况处理

如果整棵树为空(即 _root == nullptr),说明当前插入的是第一个节点。此时无需进行查找过程,直接创建新节点并将 _root 指向该节点即可。这一步完成了二叉搜索树的初始化。

if(_root ==nullptr){ _root =newNode(key, value);returntrue;}

六,二叉搜索树的删除

<二叉搜索树最麻烦的点来了。>

6.1执行删除操作时共有多少种情况

首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。如果查找元素存在,则分以下四种情况分别处理:(假设要删除的结点为 N)

情况1. 要删除结点 N 左右孩子均为空
情况2. 要删除的结点 N 左孩子为空,右孩子结点不为空
情况3. 要删除的结点 N 右孩子为空,左孩子结点不为空
情况4. 要删除的结点 N 左右孩子结点均不为空

6.2所有情况的应对策略思想总结

对应以上四种情况的解决方案我总结出了三种:

直接删除法,托孤法,替换删除法。

6.2.1直接删除法

把 N 结点的父亲对应孩子指针指向空,直接删除 N 结点(情况 1 可以当成 2 或者 3 处理,效果是一样的

在这里插入图片描述

6.2.2托孤法

情况二,把 N 结点的父亲对应孩子指针指向 N 的右孩子,直接删除 N 结点。
情况三,把 N 结点的父亲对应孩子指针指向 N 的左孩子,直接删除 N 结点。

如图所示,当前需要删除的结点为 14。该结点只有一个孩子 13,按照托孤法的处理方式,只需让 14 的父结点 10 的右指针直接指向 13,再删除结点 14 即可。
这样既完成了结点删除,又保证了二叉搜索树“左小右大”的结构性质不被破坏
。如果 13 结点下方还存在子树,也会随着 13 一同接入原来的位置。

在这里插入图片描述

6.2.3替换删除法

先看图,假设我们要删除3结点或者是8结点这类有两个子节点的结点。

在这里插入图片描述

无法直接删除 N 结点,因为 N 的两个孩子无处安放,(为什么?如果还是用托孤法,被删除结点有两个孩子,被删除结点的父亲只有一个空出来的指针,指向其中一个孩子另外一个孩子怎么办?)所以删除有两个孩子的结点,我们必须另寻他法。
回想我们曾今在堆删除堆顶元素的时候采用的方法:从堆的底部取一个数和堆顶元素交换,然后删除堆底元素并对对顶做向下调整算法。
我们也可不可以将一个数和被删除的数交换来实现替换式的删除?因为搜索二叉树的规则严谨性,这个数不能随便取得。于是我们设计出以下规则来取得这个替换数。

找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 R(最左结点)替代 N,因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。

(替代 N 的意思就是 N 和 R 的两个结点的值交换,转而变成删除 R 结点,R 结点符合情况 2 或情况 3,可以直接删除。)

注意,这里有错误(是我忽略了这个问题):一句话先说结论:只能让替代结点R去覆盖N,而不能让替代结点R去交换N,原理如下:

在这里插入图片描述


如上图,假设我们要去删除3,并且找到了右子树的最小结点4,这个时候交换4和3:如下图:

在这里插入图片描述


如果在这个时候继续选择从头遍历去查找3并删除的话,我们是找不到的,因为3的位置不符合二叉搜索树的规则。就是这个细节我们下面的代码要注意一下。 ok来看看代码怎么写:

6.3代码实现详细解析

6.3.0先给出完整代码

boolErase(const K& key){ Node* parent =nullptr; Node* cur = _root;while(cur){if(cur->_key < key){ parent = cur; cur = cur->right;}elseif(cur->_key > key){ parent = cur; cur = cur->left;}else{if(cur->left ==nullptr){if(cur == _root){ _root = cur->right;}else{if(parent->left == cur){ parent->left = cur->right;}elseif(parent->right == cur){ parent->right = cur->right;}}}elseif(cur->right ==nullptr){if(cur == _root){ _root = cur->left;}else{if(parent->left == cur){ parent->left = cur->left;}elseif(parent->right == cur){ parent->right = cur->left;}}}else{ Node* replaceparent = cur; Node* replace = cur->right;while(replace->left){ replaceparent = replace; replace = replace->left;} cur->_key = replace->_key;if(replaceparent->left == replace){ replaceparent->left = replace->right;}elseif(replaceparent->right == replace){ replaceparent->right = replace->right;} cur = replace;}delete(cur);returntrue;}}returnfalse;}

6.3.1查找待删除结点

Node* parent =nullptr; Node* cur = _root;while(cur){if(cur->_key < key){ parent = cur; cur = cur->right;}elseif(cur->_key > key){ parent = cur; cur = cur->left;}

这一段代码的作用是 在二叉搜索树中查找要删除的结点。

cur:当前遍历到的结点,parent:记录 cur 的父结点

查找过程完全遵循 二叉搜索树的查找规则,因此代码不断向下移动,同时更新 parent,这样在真正删除节点时,就可以修改父节点的指针。 如果循环结束 cur == nullptr,说明 树中不存在该元素,函数最终返回 false。

6.3.2 情况一、二、三:当前结点没有左/右孩子

if(cur->left ==nullptr){if(cur == _root) _root = cur->right;else{if(parent->left == cur) parent->left = cur->right;elseif(parent->right == cur) parent->right = cur->right;}}

这一部分对应前面原理中的:情况1:左右孩子都为空,情况2/3:左/右孩子为空,右/左孩子不为空。
这三种情况在代码中可以 统一处理。原因是:如果一个结点没有左/右孩子,那么只需要让父节点直接指向它的右/左孩子,这就是前面所说的托孤法。
代码中特别处理了 删除根节点的情况:_root = cur->right; 因为根节点没有父节点,只能直接修改 _root 指针。

6.3.4 情况四:左右子树都存在

else{ Node* replaceparent = cur; Node* replace = cur->right;while(replace->left){ replaceparent = replace; replace = replace->left;}

当待删除结点左右子树都存在时,不能直接删除。因为如果直接删除该结点,它的左右子树将无法同时接入父结点,因此必须使用替换删除法

这里采用的方法是:在当前结点的右子树中寻找最小结点。实现方式是先进入右子树,然后不断向左寻找,直到找到最左侧的结点,这个结点就是当前结点的中序后继

 cur->_key = replace->_key;

找到替换结点后,将 replace 的值赋给当前结点 cur。这样在逻辑上就相当于:用 replace 替换了 cur 的位置。
此时真正需要删除的结点,就从 cur 变成了 replace。

if(replaceparent->left == replace){ replaceparent->left = replace->right;}elseif(replaceparent->right == replace){ replaceparent->right = replace->right;}

由于 replace 是右子树中的最小结点,它一定没有左孩子,最多只可能存在一个右孩子。因此删除它时可以按照托孤法处理。
只需要让 replaceparent 指向 replace->right,即可把 replace 从树结构中摘除。

 cur = replace;}

最后将 cur 指向 replace,在后续统一执行 delete(cur),完成结点释放。

七,二叉搜索树的其他接口代码

这里不必细讲了,大家应该也能看得懂

7.1交换函数

voidswap(SBTree& other){ std::swap(_root, other._root);}

7.2中序遍历

voidInorder(){inorder(_root); cout << endl;}voidinorder(Node* root){if(root ==nullptr){return;}inorder(root->left); cout << root->_key <<" "<< root->_value << endl;inorder(root->right);}

7.3强制构造和拷贝构造

SBTree()=default;SBTree(const SBTree& tree){ _root =Copy(tree._root);} Node*Copy(Node* root){if(root ==nullptr){returnnullptr;} Node* newnode =newNode(root->_key, root->_value); newnode->left =Copy(root->left); newnode->right =Copy(root->right);return newnode;}

7.4赋值重载

SBTree&operator=(const SBTree tree){swap(tree);return*this;}

7.5析构函数

~SBTree(){destory(_root); _root =nullptr;}voiddestory(Node* root){if(root ==nullptr){return;}destory(root->left);destory(root->right);delete(root);}

八,key搜索场景/key-value搜索场景

8.1 key搜索场景

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。

场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景2:检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。

8.2 key/value搜索场景

每一个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value。

场景1:简单中英互译字典,树的结构中(结点)存储key(英文)和vlauе(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景2:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用, 缴费后抬杆,车辆离场。
场景3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1),单词存在,则++单词对应的次数。

8.3 key/value搜索场景代码

我之前带着大家写的是key搜索场景的代码,key/value搜索场景的代码只需要在次基础上稍作修改即可,以下呈现给大家。

namespace key_value{template<classK,classV>classSBTnode{public: K _key;V _value; SBTnode<K, V>* left; SBTnode<K, V>* right;SBTnode(const K& key,const V& value):_key(key),_value(value),left(nullptr),right(nullptr){}};template<classK,classV>classSBTree{using Node = SBTnode<K, V>;public:voidswap(SBTree& other){std::swap(_root, other._root);}voidInorder(){inorder(_root); cout << endl;}voidinorder(Node* root){if(root ==nullptr)return;inorder(root->left); cout << root->_key <<" "<< root->_value << endl;inorder(root->right);}SBTree()=default;SBTree(const SBTree& tree){_root =Copy(tree._root);} Node*Copy(Node* root){if(root ==nullptr)returnnullptr; Node* newnode =newNode(root->_key, root->_value); newnode->left =Copy(root->left); newnode->right =Copy(root->right);return newnode;} SBTree&operator=(const SBTree tree){swap(tree);return*this;}~SBTree(){destory(_root); _root =nullptr;}voiddestory(Node* root){if(root ==nullptr)return;destory(root->left);destory(root->right);delete(root);} Node*Find(const K& key){ Node* cur = _root;while(cur){if(cur->_key < key) cur = cur->right;elseif(cur->_key > key) cur = cur->left;elsereturn cur;}returnnullptr;}boolInsert(const K& key,const V& value){ Node* cur = _root; Node* parent =nullptr;if(_root ==nullptr){ _root =newNode(key, value);returntrue;}while(cur){if(cur->_key < key){ parent = cur; cur = cur->right;}elseif(cur->_key > key){ parent = cur; cur = cur->left;}elsereturnfalse;} cur =newNode(key, value);if(parent->_key > key) parent->left = cur;elseif(parent->_key < key) parent->right = cur;returntrue;}boolErase(const K& key){ Node* parent =nullptr; Node* cur = _root;while(cur){if(cur->_key < key){ parent = cur; cur = cur->right;}elseif(cur->_key > key){ parent = cur; cur = cur->left;}else{if(cur->left ==nullptr){if(cur == _root) _root = cur->right;else{if(parent->left == cur) parent->left = cur->right;elseif(parent->right == cur) parent->right = cur->right;}}elseif(cur->right ==nullptr){if(cur == _root) _root = cur->left;else{if(parent->left == cur) parent->left = cur->left;elseif(parent->right == cur) parent->right = cur->left;}}else{ Node* replaceparent = cur; Node* replace = cur->right;while(replace->left){ replaceparent = replace; replace = replace->left;} cur->_key = replace->_key;if(replaceparent->left == replace) replaceparent->left = replace->right;elseif(replaceparent->right == replace) replaceparent->right = replace->right; cur = replace;}delete(cur);returntrue;}}returnfalse;}protected: Node* _root=nullptr;};}

九,后语和未来连载规划

从本文开始,本人将开始连载C++中面试经常出现的高效查找容器和方法。

在这里插入图片描述

最初我们学习了二分查找法,但由于需要移动数据和排序的限制,其效率并不总是理想,使用场景也相对有限。
于是出现了二叉搜索树(由二叉树演变而来)和哈希表。二叉搜索树自身存在一些缺陷,因此诞生了平衡二叉搜索树的代表——AVL树。随着算法的发展,又出现了另一种平衡搜索树——红黑树。在此基础上,C++提供了set和map两个容器,分别适用于不同的搜索需求。而哈希表则沿着另一条赛道成为高效查找的新星

好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!

Read more

《算法题讲解指南:优选算法-二分查找》--21.山峰数组的的峰顶索引,22.寻找峰值

《算法题讲解指南:优选算法-二分查找》--21.山峰数组的的峰顶索引,22.寻找峰值

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 21. 山峰数组的的峰顶索引 题目链接: 题目描述: 题目示例: 解法(二分查找): 算法思路: C++算法代码: 算法总结及流程解析: 22. 寻找峰值 题目链接: 题目描述: 题目示例: 解法(二分查找): 算法思路: C++算法代码: 算法总结及流程解析: 结束语 21. 山峰数组的的峰顶索引 题目链接: 852. 山脉数组的峰顶索引 - 力扣(LeetCode) 题目描述: 题目示例: 解法(

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 collection 为鸿蒙端处理海量业务数据提供算法级的集合操作支持(数据处理瑞士军刀)

Flutter for OpenHarmony: Flutter 三方库 collection 为鸿蒙端处理海量业务数据提供算法级的集合操作支持(数据处理瑞士军刀)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 的复杂业务逻辑开发时,我们经常需要处理各种 Lists、Sets 和 Maps: 1. 数据分组:如何将成百上千条鸿蒙日志按日期自动归类(GroupBy)? 2. 集合对比:如何判断两个鸿蒙节点的状态列表是否内容一致(无视顺序)? 3. 优先级队列:如何在鸿蒙任务调度中自动让高优先级的任务插队排在第一位? collection 软件包是 Dart 官方团队维护的“集合增强包”。它补齐了原生态集合操作在算法层面的短板,为鸿蒙开发者提供了一套工业级、高性能的数据处理函数库。 一、高级数据处理模型 collection 在基础 List/Map 之上增加了丰富的算法维度。 鸿蒙原始迭代器 (Iterable) 分组与聚合 (GroupBy) 特殊数据结构 (Queue/Heap) 业务最终态 深层对比 (Equality)

By Ne0inhk

为什么多人解析总失败?M2FP的拼图算法是关键突破

为什么多人解析总失败?M2FP的拼图算法是关键突破 🧩 M2FP 多人人体解析服务:从模型到可视化的完整闭环 在当前计算机视觉领域,人体解析(Human Parsing) 已成为智能服装推荐、虚拟试衣、动作识别和AR/VR交互等应用的核心前置技术。然而,当场景中出现多人重叠、遮挡或远距离小目标时,传统语义分割方案往往表现不佳——要么边界模糊,要么部位错配,甚至将多个个体误判为一个整体。 这正是 M2FP(Mask2Former-Parsing) 模型脱颖而出的关键所在。作为ModelScope平台上专为人体解析任务优化的先进架构,M2FP不仅继承了Mask2Former强大的像素级分类能力,更针对多人复杂场景进行了结构化改进。其核心优势在于:通过引入分层注意力机制与实例感知解码器,实现了对多个人体区域的精准定位与语义解耦。 但真正让M2FP走出实验室、落地为可用服务的,是其背后一套完整的工程化设计——尤其是可视化拼图算法的集成。许多开发者在部署类似模型时发现:“模型能输出mask,但结果无法直观展示”,“多个mask叠加混乱,颜色不统一”……这些问题本质上源于后处理环节的缺

By Ne0inhk
解锁动态规划的奥秘:从零到精通的创新思维解析(10)

解锁动态规划的奥秘:从零到精通的创新思维解析(10)

前言:         前几天,我写了一篇关于动态规划的文章,今天继续为大家带来一些动态规划相关的习题解析。本次分享的两道题依然围绕“股票”问题展开,不过相比之前的题目,难度有所提升。希望能为大家的学习提供帮助! 1.买卖股票的最佳时机 1.1.题目来源         本题目来源于力扣,下面小编给出它的链接:121. 买卖股票的最佳时机 - 力扣(LeetCode) 1.2.题目解析         本题是小编之前讲解的股票问题的升级版。它实际上是一个经典的股票问题,因为在这一版本中,既没有交易手续费,也没有冷却期。问题的状态很直观:分为买入和卖出两种状态。不过,与之前的版本不同的是,本题对交易次数有限制——我们只能进行一次交易。也就是说,我们需要找到最佳的两天进行买入和卖出操作,从而获得最大的利润。         本题的难点在于如何高效地找到理论上的最大利润。接下来,小编将详细讲解本题的解题思路。 1.3.思路解析 1.状态表示         对于动态规划类型的题目,我们通常都需要设置好dp表来帮助我们进行状态的分析,本题小编将会使用两个二维的dp表来表示

By Ne0inhk