当前位置: 首页 > news >正文

c++编程(15)——list的模拟实现

欢迎来到博主的专栏——c++编程
博主ID:代码小豪


文章目录

    • 前言
    • list的数据结构
    • list的默认构造
    • 尾插与尾删
    • iterator
    • 插入和删除
    • 构造、析构、赋值
      • copy构造
      • initializer_list构造
      • operator =
      • 析构函数

前言

受限于博主当前的技术水平,暂时还不能模拟实现出STL当中用到的allocator,所以在这片博客当中,博主更想表达的是list的数据结构,以及list的各个操作涉及的算法,还有list的迭代器是怎样的工作原理。

博主参考的是SGI版本的STL源码,如果你对源码非常感兴趣,可以私聊博主获取源码。

在《vectoe的模拟实现》这一博客当中,我是按照c++在线文档的接口顺序来模拟实现的,但是list的结构要比vector复杂,所以博主选择跳跃式的方法来模拟实现list。

list的数据结构

list是一个带头双向循环链表的数据结构,其结点具有以下结构

  1. 存储数据的val
  2. 指向上一个节点的指针prev
  3. 指向下一个节点的指针next
    在这里插入图片描述

而链表的结构则需要将表头节点和尾结点链接起来,头结点不存储有效数据,只记录前驱节点和后继节点。
在这里插入图片描述

那么我们需要定义两个类,一个类是节点,另外一个类是管理头结点的链表。链表中的成员只需要一个,那就是指向头结点的指针。

	template<class T>struct ListNode//结点{T _data;//数据ListNode* _next;//指向下一个节点的指针ListNode* _prev;//指向上一个节点的指针};template<class T>class list{typedef ListNode<T> Node;Node* _head;//指向链表头结点};

list的默认构造

我们先来为链表和节点设计一个默认构造吧、还有其他的构造等我们实现了list的插入和删除再来实现。

先来想想怎么默认构造链表,根据c++标准,list的默认构造是实例化出一个空表,由于list是一个带头双向循环链表,因此即使list是一个空表,我们也需要生成这个链表的头结点(带头链表的头结点是不计入有效节点的)。

由于头结点不设置有效数据,那么在构造头结点的时候,我们只需要调用节点的默认构造就行。

list()
{_head = new Node;//_head指向头结点,头结点是默认构造的Node_head->next=_head;_head->prev=_head;
}

在这里插入图片描述

那么问题就来了,list的默认构造会调用Node的默认构造,可是Node的默认构造还没有生成呢!

节点的默认构造则是这样,我们允许节点传入一个T类型的参数val,这个val是传递给data的值。但是有参数的构造不属于默认构造,那么我们就为这个val设置一个缺省值吧,由于ListNode是一个模板类,那么data的值可能是各种各样的类,我们应该为这个val设置什么缺省值呢?

用匿名对象T()作为val的缺省值,如果T实例化成了内置类型,那么val的缺省值就是0,如果T实例化成了类类型,比如string。那么val的缺省值就会调用T类型的默认构造。

ListNode(T val = T())
{date = val;next = nullptr;prev = nullptr;
}

尾插与尾删

实际上push_back和pop_back应该放在插入和删除那一部分的,但如果你查看一下c++手册,你会发现insert和erase都需要用到迭代器,但是如果我们要实现迭代器,又需要插入和删除数据才能验证这些迭代器是无误的,那么这就进入了死循环。

好在push_back和push_pop不需要迭代器,那么我们先来实现这两个操作吧。

void push_back(const T& val);
void push_back();

我们先找到当前链表的尾结点,双向链表的尾结点是非常好找的,_head->prev就是尾结点。尾插的操作如下:

(1)new一个新节点newnode
(2)newnode的next指向头结点
(3)newnode的prev指向尾结点
(4)让尾结点的next指向newnode
(5)头结点的prev指向newnode

在这里插入图片描述

void push_back(const T& val)
{Node* tail = _head->prev;//找到尾结点Node* newnode = new Node(val);newnode->next = _head;newnode->prev = tail;tail->next = newnode;_head->prev = newnode;
}

尾删法的操作如下:
(1)找到尾结点tail的前驱节点Prev。
(2)让Prev的next指向_head
(3)让_head的prev指向Prev
(4)delete掉tail节点
在这里插入图片描述
不过有一点要注意,那就是避免对空表进行尾删操作,这是会导致程序报错的,因此我们可以设置一个异常,也可以直接断言。这里博主为了方便就选择断言了

void push_back()
{Node* tail = _head->prev;//找到tail节点assert(tail != _head);//当尾结点就是头结点时,list为空表Node* prev = tail -> prev;//找到prev节点prev->next = _head;_head->prev = prev;delete tail;
}

这个断言其实不太好,等我们实现了list的迭代器后,我们可以让迭代器完成这项工作,即

assert(this->begin()!=this->!end());

iterator

好吧,最终还是来到了最让我头疼的iterator部分,我第一次尝试实现迭代器的时候是非常痛苦的。不过还是从SGI版本的list的迭代器中学到了精妙的编程方式,也深深的感叹c++封装特性的奇妙之处。

在string和vector的模拟实现当中,博主都是粗暴的将容器中的指针当成迭代器来使用,即

typedef T* iterator;

这是因为string和vector的元素的存储方式都是连续存储。这就导致指针的行为逻辑(++,–,判断)都和迭代器一致。因此我们可以直接把指针当做迭代器。

但是list可不是顺序存储的数据结构,如果我们还是让迭代器具有指针的行为逻辑,那么迭代的结果肯定是飞到姥姥家去了。原因也很简单,迭代器需要执行++操作的吧,而指针++是指向顺序存储的下一个元素空间,但是list不是顺序存储,那么迭代器指向的空间就是未使用的空间,那不成野指针了吗。

那么我们该怎么实现list的迭代器呢?SGI板的STL是这么解决的,list的迭代器的主要问题是由于指针的行为不符合list的迭代要求,那么我们将指针封装起来,成为一个类,那么我们不就可以定义operator++,operator–,使得这个指针符合list迭代的需求。简直是一个奇妙的想法。

博主这里直接将这个类模拟实现出来,源码之下没有秘密,千言万语不如让大家亲身感受。

template<class T,class ref,class ptr>
struct listiterator
{typedef ListNode<T> Node;typedef listiterator self;listiterator(Node* listnode=nullptr){linkptr = listnode;}self operator++()//前置++{linkptr = linkptr->next;return linkptr;}self operator++(int)//后置++{listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;}self operator--()//前置--{linkptr = linkptr->prev;return linkptr;}self operator--(int)//后置--{listiterator tmp = linkptr;linkptr = linkptr->next;return tmp;}ref operator*()//解引用{return linkptr->data;}ptr operator->()//成员访问{return &(linkptr->data);}bool operator!=(listiterator it)//判等{return linkptr != it.linkptr;}bool operator==(listiterator it)//判等{return linkptr == it.linkptr;}Node* linkptr;
};

list的迭代器的设计思路是这样的:由于Node*的指针的行为不符合list对迭代器的需求。那么就把指针封装起来,设计成一个类,这样我们就能在类中重载各种操作符的函数,使这个类符合迭代器的行为。

比如,listiterator的迭代器进行++操作,linkptr并不会指向顺序结构的下一个元素的位置,而是指向节点的后继节点,这就使listiertaor符合list的迭代器行为,然后在list当中将listiterator实例化成iterator。这样就能用迭代器完成迭代操作了。

既然写好了迭代器的类,现在就将迭代器放进list类中吧,顺便再把begin和end实现出来

template<class T>class list{public:typedef ListNode<T> Node;typedef listiterator<T, T&, T*> iterator;typedef listiterator<T, const T&, const T*> const_iterator;iterator begin(){return _head->next;//begin返回第一个有效节点//即_head的下一个节点}const_iterator begin()const{return _head->next;}iterator end() {return _head;//头节点是无效节点,当链表遍历到这里时,即为遍历成功}const_iterator end() const{return _head;}private:Node* _head;//指向链表头结点};
}

begin()指向list中的第一个有效数据,因此我们返回_head->next.

而end()指向list的无效数据(因为STL规定begin到end的区间是左闭右开[begin,end),这意味着end()返回无效的位置),而头结点保存的是无效数据,因此end()返回头结点_head

我们用范围for测试一下,如果成功,就代表迭代器实现完成了。

void TestMylist1()
{list<int> list1;list1.push_back(1);list1.push_back(2);list1.push_back(3);list1.push_back(4);for (auto& e : list1){cout << e << ' ';}
}

插入和删除

我们先来写写单元素的insert和erase。来看看c标准定义的函数原型吧。

iterator insert (iterator position, const value_type& val);
iterator erase (iterator position);

insert需要传入迭代器position,在position位置处插入新的节点。具体操作如下:

(1)构造一个值为val的新节点newnode
(2)令position位置的节点为cur,cur的前驱节点为prev
(3)让newnode的next指向cur
(4)让newnode的prev指向prev。
(5)让prev的next指向newnode
(6)让cur的prev指向newnode

在这里插入图片描述
这个position可以是任意位置,包括头结点和尾结点。我们注意insert是存在返回值的,而且返回值是一个迭代器。我们查看c++文档的叙述。这个返回值是指向新插入的节点的迭代器。

iterator insert(iterator position, const T& val)
{Node* newnode = new Node(val);Node* cur = position.linkptr;Node* prev = cur->prev;newnode->next = cur;newnode -> prev = prev;prev->next = newnode;cur->prev = newnode;return iterator(newnode);
}

erase也是通过迭代器position确定删除的节点位置,具体操作如下:

(1)让position位置的节点为cur
(2)让cur的前驱节点为prev,让position的后继节点为next
(3)让prev的next指向next
(4)让next的prev指向prev
(5)delete掉cur

要注意erase操作不能删除头结点(STL中的list也是这么做的),即pos位置不能是list.end()因此我们在函数的前面加上一句断言。

c++标准当中规定,erase返回被删除节点的下一个节点。因此erase应该实现成这样。

iterator erase(iterator position)
{assert(position != end());Node* cur = position.linkptr;Node* prev = cur->prev;Node* next = cur->next;next->prev = prev;prev->next = next;return iterator(next);
}

insert和erase其实是很万能的,如果我们想要头插,可以调用erase(list.begin(),val),如果想要头删,可以调用erase(list.begin())。包括尾插和尾删,我们都可以通过insert和erase实现操作。因此push_back,push_fornt,pop_back,pop_front,都可以复用insert和erase.

		void push_back(const T& val){insert(end(), val);}void pop_back(){erase(--end());}void push_front(const T&val){insert(begin(), val);}void pop_front(){erase(end());}

构造、析构、赋值

通常我们在写一个类时,都会先写类的构造、析构、赋值重载函数。但是list有点不同,因为list的大部分构造、析构都依赖迭代器的实现。因此在list的末尾,博主才对这些函数进行实现。

copy构造

copy (4)	
list (const list& x);

list的拷贝构造是深拷贝,浅拷贝会导致内存错误,list深拷贝的方法如下:

我们遍历x,将x的元素按照顺序尾插到list容器中。但是光拷贝可不行啊,容器还没有生成头结点(遍历x的时候可不会遍历头结点,因为头结点被视为容器的终点end)。所以我们先创建一个头结点。

void emptynode()//创建头结点
{_head = new Node;//_head指向头结点,头结点是默认构造的Node_head->next = _head;_head->prev = _head;
}

emptynode函数只用于对象构造时创建空节点,所以我们最好把它隐藏起来(protected或者private)。

		list(const list& l1){emptynode();for (const auto& e : l1){push_back(e);}}

initializer_list构造

initializer_list是c++11新增的模板类,关于initializer_list大家可以查看c++手册,或者查看博主的另一篇博客:好用的c++11语言特性

initializer list (6)	
list (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());

initializer_list可以简单的认为是一个存储数据数组,那么构造的方法也很简单,遍历initializer_list的元素,将元素挨个尾插到容器当中。

list<int> list2(list1);
for (const auto& e : list2)
{cout << e << ' ';
}

operator =

赋值操作和拷贝操作的逻辑一致,我直接套用就行。不过这种套用可是单纯的将程序代码复制粘贴过来,我们会利用到一种取巧的方法

const list& operator=(list x)
{std::swap(_head, x._head);return *this;
}

ok,就是这么一个简短的代码,但是其思想可一点都不简短。

首先是函数的形参,没有继续采取常用的pass by const reference(const T&)。而是选择了pass by value的形式, 这就导致调用operator=函数时,形参x会调用拷贝构造构造拷贝实参。
在这里插入图片描述

然后我们将容器的头结点和x的头结点进行交换,使得容器获得实参的内容。

在这里插入图片描述
ok,我们发现了一个有趣的现象,容器现在结构、数据都和实参一致了,这难道不是完成了复制操作吗?

而且这种方法还有一个特别有意思的地方,x是一个临时变量,这就说明x中的空间是没有实际作用的,我们需要将其空间释放掉,但是x是栈帧中的参数,一旦离开了栈帧,x会调用析构函数将资源依次释放。是不是很神奇,简短的三行代码,竟然进行了这么多的操作。而且效率还不低。

析构函数

好吧,讲到最后才完成析构函数,这其实不是很好的习惯,如果你将析构函数放到最后一步,一旦程序通过了测试,你可能就认为已经完成了所有操作,便遗忘掉了析构函数,那么等到内存泄漏的时候可就麻烦了。

list的析构函数应该完成以下操作,释放所有的有效结点,最后释放头结点。额,实际上c++标准定义了list的成员函数clear,clear会释放除头结点外的所有节点。那么为什么博主没有提到呢?因为博主忘了哈哈哈哈。

我们从第一个有效节点开始遍历,每遍历一个节点就释放该节点的空间,直到我们回到头结点为止。

void clear()
{iterator start = begin();while (start != end()){start=erase(start);}
}

erase的返回值是被删除节点的下一个节点,正好拿来用了哈哈哈。

诶,我们的析构函数是做什么来着,先释放所有的有效节点,再释放头结点,OK,clear正好是释放所有的有效节点,那我们就直接拿来复用就行了啊。

~list(){clear();delete _head;_head = nullptr;}

相关文章:

c++编程(15)——list的模拟实现

欢迎来到博主的专栏——c编程 博主ID&#xff1a;代码小豪 文章目录 前言list的数据结构list的默认构造尾插与尾删iterator插入和删除构造、析构、赋值copy构造initializer_list构造operator 析构函数 前言 受限于博主当前的技术水平&#xff0c;暂时还不能模拟实现出STL当中用…...

【深度学习】吸烟行为检测软件系统

往期文章列表&#xff1a; 【YOLO深度学习系列】图像分类、物体检测、实例分割、物体追踪、姿态估计、定向边框检测演示系统【含源码】【深度学习】YOLOV8数据标注及模型训练方法整体流程介绍及演示【深度学习】行人跌倒行为检测软件系统【深度学习】火灾检测软件系统【深度学…...

​你见过哪些不过度设计的优秀APP?​

优联前端https://ufrontend.com/ 提供一站式企业前端解决方案 “每日故宫”是一款以故宫博物院丰富的藏品为基础&#xff0c;结合日历形式展示每日精选藏品的移动应用。通过这款应用&#xff0c;用户可以随时随地欣赏到故宫的珍贵藏品&#xff0c;感受中华五千年文化的魅力。…...

全栈:session用户会话信息,用户浏览记录实例

PHP中的session是一种存储机制&#xff0c;它允许您存储和跟踪用户在访问Web应用程序时的信息。会话通常用于存储用户特定的数据&#xff0c;如用户ID、购物车内容、用户偏好设置等&#xff0c;这些数据需要在多个页面请求之间保持不变。 session详解 1. 会话是如何工作的 会…...

设计模式--》 装饰模式的应用

装饰模式的定义&#xff1a; 装饰模式&#xff08;Decorator Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许你动态地给一个对象添加一些额外的职责。就增加功能来说&#xff0c;装饰模式相比生成子类更为灵活。 何时应用装饰模式&#xff1f; 1.当需要动态地给…...

深入解析Web前端三大主流框架:Angular、React和Vue

Web前端三大主流框架分别是Angular、React和Vue。下面我将为您详细介绍这三大框架的特点和使用指南。 Angular 核心概念: 组件(Components): 组件是Angular应用的构建块,每个组件由一个带有装饰器的类、一个HTML模板、一个CSS样式表组成。组件通过输入(@Input)和输出(…...

ch3运输层--计算机网络期末复习(持续更新中)

运输层位于网络层之上 运输层协议提供的某些服务受到网络层协议的限制。比如,时限和带宽保证。 运输层也提供自己的特殊服务。比如,可靠数据传输服务,安全性服务。 网络层:两个主机之间的逻辑通信 运输层:两个进程之间的逻辑通信 网络地址:主机的标识(IP地址) 传输地址: …...

mysql中的内连接与外连接

在MySQL中&#xff0c;内连接和外连接是用于从多个表中检索数据的两种不同的连接方式。 内连接&#xff08;INNER JOIN&#xff09;&#xff1a; 内连接返回两个表之间匹配的行。它只返回两个表中共同匹配的行&#xff0c;如果在一个表中没有匹配到对应的行&#xff0c;则不会显…...

0基础认识C语言(理论+实操 2)

小伙伴们大家好&#xff0c;今天也要撸起袖子加油干&#xff01;万事开头难&#xff0c;越学到后面越轻松~ 话不多说&#xff0c;开始正题~ 前提回顾&#xff1a; 接上次博客&#xff0c;我们学到了转义字符&#xff0c;最后留下两个转义字符不知道大家有没有动手尝试了一遍&a…...

ChatGPT的基本原理是什么?又该如何提高其准确性?

在深入探索如何提升ChatGPT的准确性之前&#xff0c;让我们先来了解一下它的工作原理吧。ChatGPT是一种基于深度学习的自然语言生成模型&#xff0c;它通过预训练和微调两个关键步骤来学习和理解自然语言。 在预训练阶段&#xff0c;ChatGPT会接触到大规模的文本数据集&#x…...

云计算OpenStack基础

1.什么是虚拟化&#xff1f; •虚拟化是云计算的基础。 •虚拟化是指计算元件在虚拟的而不是真实的硬件基础上运行。 •虚拟化将物理资源转变为具有可管理性的逻辑资源&#xff0c;以消除物理结构之间的隔离&#xff0c;将物理资源融为一个整体。虚拟化是一种简化管理和优化…...

[10] CUDA程序性能的提升 与 流

CUDA程序性能的提升 与 流 1. CUDA程序性能的提升 在本节中,我们会看到用来遵循的基本的一些性能来提升准则,我们会逐一解释它们1.1 使用适当的块数量和线程数量 研究表明,如果块的数量是 GPU 的流多处理器数量的两倍,则会给出最佳性能,不过,块和线程的数量与具体的算法…...

TH方程学习(1)

一、背景介绍 根据CW方程的学习&#xff0c;CW方程的限制条件为圆轨道&#xff0c;不考虑摄动&#xff0c;二者距离相对较小。TH方程则可以将物体间的相对运动推广到椭圆轨道的二体运动模型&#xff0c;本部分将结合STK的仿真功能&#xff0c;联合考察TH方程的有用性&#xff…...

【九十七】【算法分析与设计】图论,迷宫,1207. 大臣的旅费,走出迷宫,石油采集,after与迷宫,逃离迷宫,3205. 最优配餐,路径之谜

1207. 大臣的旅费 - AcWing题库 很久以前&#xff0c;TT 王国空前繁荣。 为了更好地管理国家&#xff0c;王国修建了大量的快速路&#xff0c;用于连接首都和王国内的各大城市。 为节省经费&#xff0c;TT 国的大臣们经过思考&#xff0c;制定了一套优秀的修建方案&#xff0c;…...

【Tools】SpringBoot工程中,对于时间属性从后端返回到前端的格式问题

Catalog 时间属性格式问题一、需求二、怎么使用 时间属性格式问题 一、需求 对于表中时间字段&#xff0c;后端创建对应的实体类的时间属性需要设定格式&#xff08;默认的格式不方便阅读&#xff09;&#xff0c;再返回给前端。 二、怎么使用 导入jackson相关的坐标&#x…...

算法训练营day35

题目1&#xff1a;122. 买卖股票的最佳时机 II - 力扣&#xff08;LeetCode&#xff09; 贪心算法思路很简单&#xff0c;就是把每一天的利润都算出来&#xff0c;然后把整的加起来就是结果 class Solution { public:int maxProfit(vector<int>& prices) {int resu…...

代码随想录-Day23

669. 修剪二叉搜索树 方法一&#xff1a;递归 class Solution {public TreeNode trimBST(TreeNode root, int low, int high) {if (root null) {return null;}if (root.val < low) {return trimBST(root.right, low, high);} else if (root.val > high) {return trimBS…...

基于Visual Studio版本的AI编程助手

Visual Studio 是一个出色的 IDE,可用于构建适用于 Windows、Mac、Linux、iOS 和 Android 的丰富、精美的跨平台应用程序。 使用一系列技术(例如 WinForms、WPF、WinUI、MAUI 或 Xamarin)构建丰富。 1、安装 点击上方工具栏拓展选项,选择管理拓展选项 接着在联机页面中搜索&q…...

04-Vue:ref获取页面节点--很简单

目录 前言在Vue中&#xff0c;通过 ref 属性获取DOM元素使用 ref 属性获取整个子组件&#xff08;父组件调用子组件的方法&#xff09; 前言 我们接着上一篇文章 03-02-Vue组件之间的传值 来讲。 下一篇文章 05-Vue路由 在Vue中&#xff0c;通过 ref 属性获取DOM元素 我们当然…...

CBK-D2-安全与架构工程.md

CBK-D2-安全与架构工程 密码学和对称密钥算法 密码通信的基础知识 明文P-plaintext、加密encrypt、密文C-ciphertext、解密decrypt、密钥Key 多数情况下,密钥无非是一个极大的二进制数 每一种算法都有一个特定密钥控制key space,是一个特定的数值范围 密钥空间由位大小b…...

Windows驱动开发系列文章一

文章目录 环境搭建如何调试实时调试非实时调试 环境搭建 基本上按照官方网站安装 VisualStudio/SDK/WDK 这些软件就可以了 详情请参考这个安装链接 如何调试 Windows 调试分为两种&#xff1a;一种是实时调试&#xff0c;一种是非实时调试 实时调试 这个就需要用到Microso…...

java项目之人事系统源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的人事系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 基于vue的人事系统的主要使用者…...

I/O '24|学习资源焕新,技术灵感升级

2024 年 5 月 15 日凌晨举行的 Google I/O 大会为各地的开发者们带来了新的灵感。面对技术革新&#xff0c;相信各位开发者们都迫不及待想要自己上手试一试。 别急&#xff0c;Google 谷歌今年为中国的开发者们准备了一份特别的学习资源&#xff0c;让开发者们自由探索新知。 G…...

前端应用开发实验:表单控件绑定

目录 实验目的相关知识点实验内容代码实现效果 实验目的 &#xff08;1&#xff09;熟练掌握应用v-model指令实现双向数据绑定的方法&#xff0c;学会使用 v-model指令绑定文本框、复选框、单选按钮、下拉菜单&#xff1b; &#xff08;2&#xff09;学会值绑定&#xff08;将…...

[双指针] --- 快乐数 盛最多水的容器

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Journey 本篇博客我们分享一下双指针算法中的快慢指针以及对撞双指针&#xff0c;下面我们开始今天的学习吧~ &#x1f3e0; 快乐数 &#x1f4d2; 题…...

操作系统 - 输入/输出(I/O)管理

输入/输出(I/O)管理 考纲内容 I/O管理基础 设备&#xff1a;设备的基本概念&#xff0c;设备的分类&#xff0c;I/O接口 I/O控制方式&#xff1a;轮询方式&#xff0c;中断方式&#xff0c;DMA方式 I/O软件层次结构&#xff1a;中断处理程序&#xff0c;驱动程序&#xff0c;…...

代码随想录算法训练营第22天(py)| 二叉树 | 669. 修剪二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树

669. 修剪二叉搜索树 力扣链接 给定一个二叉搜索树&#xff0c;同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[L, R]中 (R>L) 思路 如果当前节点元素小于low&#xff0c;递归右子树&#xff0c;返回符合条件的头节点 如果当前节点元…...

使用C语言实现学生信息管理系统

前言 在我们实现学生信息管理系统的过程中&#xff0c;我们几乎会使用到C语言最常用最重要的知识&#xff0c;对于刚学习完C语言的同学来说是一次很好的巩固机会&#xff0c;其中还牵扯到数据结果中链表的插入和删除内容。 实现学生信息管理系统 文件的创建与使用 对于要实现…...

上下文视觉提示实现zero-shot分割检测及多visual-prompt改造

文章目录 一、Closed-Set VS Open-set二、DINOv2.1 论文和代码2.2 内容2.3 安装部署2.4 使用效果 三、多visual prompt 改造3.1 获取示例图mask3.2 修改函数参数3.3 推理代码3.4 效果的提升&#xff01; 四、总结 本文主要介绍visual prompt模型DINOv&#xff0c;该模型可输入八…...

WebGL学习(一)渲染关系

学习webgl 开发理解渲染关系是必须的&#xff0c;也非常重要&#xff0c;很多人忽视了这个过程。 我这里先简单写一下&#xff0c;后面尽量用通俗易懂的方式&#xff0c;举例讲解。 WebGL&#xff0c;全称Web Graphics Library&#xff0c;是一种在网页上渲染3D图形的技术。它…...