List的模拟实现(2)

前言

上一节我们讲解了list的基本功能,那么本节我们就结合底层代码来分析list是怎么实现的,那么废话不多说,我们正式进入今天的学习:)

List的底层结构

我们先来看一下list的底层基本结构:

这里比较奇怪的一点是底层选择了void*来表示链表的指针,其实可以不用这么做

接下来是迭代器部分:

模拟实现链表难就难在实现迭代器,因为链表的物理空间不是连续的,所以不能简单的通过typedef一个指针变量来达到实现迭代器的目的

链表为空的初始化:

链表为空时不是简单的把所有的指针都赋空指针,而是创建一个节点,让这个节点的next和prev指针都指向自己。这就是创造了一个头节点,这里涉及到数据结构阶段双向带头链表的基本知识

get_node是申请节点,调用了内存池,由于我们还没有学习内存池,所以可以简单地将这里理解为malloc

下面是源代码中的头插尾插接口:

通过这些,我们可以知道,单纯实现链表的功能是没有什么难度的,和我们之前实现string和vector差不多,难就难在理解迭代器的实现

既然底层结构已经看的差不多了,那我们现在就来实现一下list吧

List的模拟实现

节点的基本结构

首先要实现链表的基本结构,链表的基本结构是节点

	template<class T>struct list_node{list_node(const T& data = T()):_data(data),_next(nullptr),_prev(nullptr){}T _data;list_node<T>* _next;list_node<T>* _prev;};

链表的借本结构

构造函数 

先来实现一下构造函数,根据底层是双向带头循环链表,写出代码如下:

	template<class T>class list{public:typedef list_node<T> Node;list(){_head = new Node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}private:Node* _head;size_t _size;};

 size和empty函数

实现size和empty函数,由于代码很简单,就不做讲解了:

		size_t size(){return _size;}bool empty(){return _size == 0;}

迭代器

因为不是每一个STL的容器都是存储在连续的物理空间之中,所以并不是每一个容器都支持下标+[]访问,但是所有的容器都有迭代器,都支持范围for,因此实现迭代器是很重要的一个步骤,我们来逐步分析迭代器的实现:

我们知道,对于一个节点而言,*解引用以后找到的不是节点的值,而是节点的地址;因为存储空间不连续,不能通过++来找到下一个节点,所以就不能仅通过typedef来实现迭代器,此时我们就应该考虑封装+运算符重载来实现迭代器,主要内容就是重载 *  ++    != 等,让其满足与其他迭代器相同的作用

	template<class T>struct list_iterator{Node* _node;};
迭代器的构造函数

		list_iterator(Node* node):_node(node){}
重载*
		T& operator*(){return _node->_data;}
重载前置++、--

由于++以后返回的数据类型依旧是迭代器,为了书写简便一点,调用typedef:

		Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}
重载后置++、-- 

因为后置++和--是将当前的值使用以后再执行++或者--,所以我们需要把原本的值拷贝一份,用于完成返回操作:

		Self& operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}
重载!=、==
		bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}
重载-> 

先来分析一下为什么需要重载->这个符号:

接下来先构造一个使用->的情景:

假设有一个这样的结构体:

	struct AA{int _aa1 = 1;int _aa2 = 2;};

此时我们要在链表中存入这个结构体并且打印:

		list<AA> ltaa;ltaa.push_back(AA());ltaa.push_back(AA());ltaa.push_back(AA());ltaa.push_back(AA());list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << *itaa << " ";++itaa;}cout << endl;

此时运行以后发现编译不通过

这里面的itaa通过解引用会得到节点中的data,data的数据类型是T,也就是自定义类型AA。因为没有重载流插入,所以这里的自定义类型无法打印,要想解决这个问题有两种方法:

第一种方法:给AA重载流插入

第二种方法:逐一访问结构体中的元素

		list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << (*itaa)._aa1 << " and " << (*itaa)._aa2 << endl;++itaa;}cout << endl;

此时代码就成功运行了

但是这样写就很麻烦,此时就可以重载->这个运算符,这里先把代码写出来再对其进行分析:

		T* operator->(){return &_node->_data;}
		list<AA>::iterator itaa = ltaa.begin();while (itaa != ltaa.end()){cout << itaa->_aa1 << " and " << itaa->_aa2 << endl;++itaa;}cout << endl;


疑难解答:为什么operator-> 操作符重载函数返回值 T* 而不是 T&

在 list_iterator 这个迭代器类模板中,operator-> 操作符重载函数设计为返回 T* 而不是 T&,这与 operator-> 操作符的特殊语义和 C++ 语言的规定有关:

在 C++ 中,operator-> 操作符有特殊的处理规则。当我们使用 -> 操作符时,编译器会尝试不断应用 -> 操作,直到得到一个非指针类型的对象

简单地说就是:当你使用迭代器 it 调用 it->member 时,编译器会先调用 it.operator->(),如果 operator->() 返回的是一个指针,那么就直接访问该指针指向对象的成员;如果返回的不是指针,编译器会再次对返回值应用 ->

我们可以分两点来说明返回 T* 的合理性:

1. 符合使用习惯

迭代器的 operator-> 操作符通常用于模拟指针的行为,返回 T* 可以让迭代器像指针一样使用

例如,对于一个存储自定义类型 MyClass 的链表,使用迭代器访问 MyClass 的成员时,我们希望能够像使用指针一样直接通过 -> 操作符访问成员,如下所示:

namespace xiaobai
{template<class T>struct list_node{list_node(const T& data = T()): _data(data), _next(nullptr), _prev(nullptr) {}T _data;list_node<T>* _next;list_node<T>* _prev;};template<class T>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node) : _node(node){}T& operator*() {return _node->_data;}Self& operator++() {_node = _node->_next;return *this;}bool operator!=(const Self& s) {return _node != s._node;}T* operator->() {return &_node->_data;}Node* _node;};
}int main() 
{xiaobai::list_node<MyClass> node(MyClass(10));xiaobai::list_iterator<MyClass> it(&node);// 使用迭代器的 -> 操作符访问成员std::cout << it->value << std::endl;return 0;
}

在这个例子中,it->value 能够正常工作,因为 operator->() 返回的是 MyClass* 类型的指针,编译器可以直接通过该指针访问 value 成员

2. 避免额外的复杂度

如果 operator->() 返回 T&,编译器会再次对返回的引用应用 -> 操作,这会导致代码逻辑变得复杂,而且可能不符合用户的预期

例如:若T是一个自定义类型,由于T&不是指针类型,编译器会尝试调用 T 类型的 operator->() 函数,这可能会引发编译错误或意外的行为

所以T* operator->() 是为了让迭代器能够像指针一样使用,符合用户的使用习惯,并且遵循了 C++ 中 operator-> 操作符的特殊语义。而 T& operator->() 会破坏这种语义,导致代码逻辑复杂且不符合预期,因此通常不这样设计


这段代码初步看起来会很奇怪,它这里实际应该是两个箭头,但是为了可读性省略了一个箭头

(具体细节请浏览上面的疑难解答) 

cout << itaa.operator->()->_aa1 << " and " << itaa.operator->()->_aa2 << endl;

第一个箭头是运算符重载,返回的是AA*类型的变量

第二个箭头是原生指针,通过这个箭头就可以访问到list中的数据了

 迭代器代码初步集合

到这里我们就完成了迭代器:

	template<class T>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node):_node(node){}T& operator*(){return _node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self& operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}T* operator->(){return &_node->_data;}Node* _node;};

此时再去链表的类中typedef迭代器,简化书写:

	template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T> iterator;//.......private:Node* _head;size_t _size;};

重载打印容器以及对迭代器的修缮

实现了迭代器,就满足了范围for遍历,此时就可以写一个访问容器的接口,因为所有容器都具有迭代器,所以这个接口也可以访问其他STL容器

	template<class Container>void print_container(const Container& con){for (auto e : con){cout << e << " ";}cout << endl;}

现在测试一下这个打印:

		list<int> lt;ltaa.push_back(1);ltaa.push_back(2);ltaa.push_back(3);ltaa.push_back(4);print_container(lt);

此时运行代码却发现编译报错了,为什么函数外面使用范围for可以打印,但是print_container函数里面使用范围for却打印不了,这是为什么呢?

因为函数里面的参数是const参数,函数外面是普通的参数,我们还没有实现const迭代器,所以这里就会报错

再来分析一下const迭代器的特征:先思考一下const迭代器为什么是const_iterator而不是普通的iterator加上前置的const修饰?

要想想清楚这个问题,我们首先需要明白const迭代器是自身不能修改还是指向的内容不能修改。这里我们结合指针不同位置的const意义来理解:

T* const ptr;
const T* ptr;

第一种:指针本身不允许改变

第二种:指针指向的内容不允许改变

很明显的,const迭代器是想完成第二种功能,如果是const iterator的话,const的作用是确保iterator本身不被修改迭代器本身不能修改的话就无法实现遍历。const迭代器的产生是为了在保证遍历的前提之下保证迭代器指向的内容也不被修改

那么该怎么修改代码来完成这一目的呢?

我们在实现迭代器这一个类时,对数据的修改接口是operator*以及operator->,对于const迭代器而言,返回类型就应该加上const:

		const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}

所以这里就需要拷贝之前的普通迭代器代码,再在*和->的重载中加上const完成const迭代器

	template<class T>struct list_const_iterator{typedef list_node<T> Node;typedef list_iterator<T> Self;list_iterator(Node* node):_node(node){}const T& operator*(){return _node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self& operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}const T* operator->(){return &_node->_data;}Node* _node;};

此时在重载一下begin和end函数,给一个const迭代器版本,并且在list类中typedef const迭代器:

(普通迭代器可以调用const迭代器的begin和end,因为权限可以缩小;反之const迭代器无法调用普通迭代器的begin和end,因为权限不能放大

	template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T> iterator;typedef list_const_iterator<T> const_iterator;iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}//........private:Node* _head;size_t _size;};

**************************************************************************************************************

在 list 类中重载 `begin` 和 `end` 函数(即提供 `const` 和非 `const` 版本)是为了支持对 `const` 对象和非 `const` 对象的迭代操作,下面详细解释原因:

1. 支持非 `const` 对象的迭代

当你有一个非 `const` 的 `list` 对象时,你可能希望能够修改列表中的元素。此时,你需要使用非 `const` 迭代器,因为非 `const` 迭代器允许对其所指向的元素进行修改。非 `const` 版本的 `begin` 和 `end` 函数返回的就是非 `const` 迭代器

2. 支持 `const` 对象的迭代

当你有一个 `const` 的 `list` 对象时,你不应该修改列表中的元素,因为这违反了 `const` 限定的语义。此时,你需要使用 `const` 迭代器,`const` 迭代器只能用于访问元素,而不能修改元素。`const` 版本的 `begin` 和 `end` 函数返回的就是 `const` 迭代器

3. 函数重载的实现

通过函数重载,编译器会根据对象是否为 `const` 来选择合适的 `begin` 和 `end` 函数版本。如果对象是 `const` 的,编译器会调用 `const` 版本的 `begin` 和 `end` 函数,返回 `const` 迭代器;如果对象是非 `const` 的,编译器会调用非 `const` 版本的 `begin` 和 `end` 函数,返回非 `const` 迭代器。

重载 `begin` 和 `end` 函数是为了提供对 `const` 对象和非 `const` 对象的一致迭代接口,同时保证 `const` 对象的元素不被意外修改,遵循了 C++ 的 `const` 正确性原则,使得代码更加安全和灵活。

**************************************************************************************************************

 此时用一个很特别的用例来测试一下代码(这里修改了一下print_container函数):

	template<class Container>void print_container(const Container& con){list<int>::const_iterator it = con.begin();while(it != con.end()){*it += 10;cout << *it << " ";++it;}cout << endl;for (auto e : con){cout << e << " ";}cout << endl;}void test_list01(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;}

可以看到,这里非常奇怪,明明我们故意在打印容器代码里面修改了const迭代器指向的内容,想让它报错,但是代码却成功运行了,这是为什么呢?

这就涉及到我们之前提及的按需实例化,这里的代码存在着编译错误,' *it += 10 ' 中的*it返回的数据类型应该是const T&,对其进行+=10操作本身是应该编译报错的,但是这里非但没有报错反而还运行成功了

之所以产生这样的结果,是因为模板和类模板都会走按需实例化。模板不能被直接调用,模板用于将对应类型的代码实例化出来,实例化出来的代码才能够使用

因为我们在主函数中没有使用print_container函数,编译器在编译的过程中就不会去实例化print_container函数里面的内容,而没有实例化的代码编译器只会对其进行很简单的语法检查,比如:语句结束没加分号,对于细枝末节的操作编译器不会检查。

此时如果使用print_container函数就会报错

        list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;print_container(lt);

对迭代器代码的简化

刚才那种实现方式我们的确能成功的完成任务,但是const迭代器和普通迭代器除了operator*和->的代码有区别以外,其他地方的代码一模一样,这么设计的话在代码长度上就会非常的冗余,我们去底层观察一下是怎么实现迭代器的:

可以看到,它没有将迭代器和const迭代器定义为两个类,而是同一个类模板。而且它除了传入了 T ,还传入了 T& 和 T* 这两个参数

如果是普通迭代器,它的中的参数是 T T& T* ,分别传给了参数 T Ref Ptr ,而 Ref 和 Ptr 分别被重命名为 reference 和 pointer ,而 reference 和 pointer 分别又是 operator* 和 operator-> 的返回值

如果是const迭代器,它的中的参数是 T 、const T&、 const T* ,分别传给了参数 T Ref Ptr , Ref 和 Ptr 分别被重命名为 reference 和 pointer ,而 reference 和 pointer 分别又是 operator* 和 operator-> 的返回值

通过这种形式就控制住了operator*和->的返回值

虽然这两种写法在本质上没有区别,只是之前的写法是自己写了两个类,而这种方法是实现了一个类模板并且传了不同的模板参数给编译器,通过编译器来实例化出两个类。通过这样的方法,我们就可以实现精简代码的需求

那么就根据底层实现的原理来完善一下原来的代码吧:

	template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}//.......Node* _node;};
	template<class T>class list{public:typedef list_node<T> Node;typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*> const_iterator;//.......private:Node* _head;size_t _size;};
 迭代器代码的最终整合
	template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self& operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(){_node = _node->_prev;return *this;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}Ptr operator->(){return &_node->_data;}Node* _node;};

begin、end函数

begin函数用于返回链表第一个元素

		iterator begin(){iterator it(_head->_next);return it;}

也可以用返回匿名对象来简化代码:

			return iterator(_head->_next);

还有一种更加简单的写法:

			return _head->_next;

因为这里返回的是一个节点的指针,节点的指针是可以隐式类型转换成iterator的,单参数的构造函数支持隐式类型的转换


end函数同理:

		iterator end(){return _head->_prev;}

到这里,我们来测试一下:

		list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;

这里迭代器的设计相当完美,假设链表为空,此时就只有一个哨兵位的头节点。由于begin是_head的next,此时它指向的还是自己。end是_head,也同样指向自己,此时it = begin() == end在条件判断的时候就不会进入循环,造成非法访问

我们在实现迭代器的时候没有写拷贝构造函数,这就意味着指针在与指针的拷贝的时候进行的是浅拷贝,那么浅拷贝会不会出现问题呢?

答案是不会,我们就以上面的测试代码为例,我们给it赋了头节点,此时我们期望的就是it也指向原来的头节点,就是需要浅拷贝,而不是开一个新的链表,指向新的链表中的头节点

这里的迭代器只是一个访问和遍历链表的工具,它不像vector、string等容器一样,它所指向的资源并不是属于它自己的,它指向的资源是属于list的。所以它也不要写析构函数,它不能去释放list的空间


insert函数

insert函数用于在pos位置之前插入数据,要想实现这个功能还是比较简单的,仅需要通过简单的修改指针指向即可:

		void insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;}

push_back和push_front函数

可以通过复用insert函数完成这两个函数:

		void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}

erase函数

erase函数用于删除pos位置的节点,这个函数的实现也很简单,也是只需要修改指针指向再释放节点即可:

注意erase不能删除哨兵位的头节点,在这里加上assert断言

		void erase(iterator pos){assert(pos != end());Node* prev = pos._node->_prev;Node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;}

pop_back和pop_front函数

通过复用erase即可实现这两个函数:

		void pop_back(iterator pos){erase(--end());}void pop_front(iterator pos){erase(begin());}

结尾

因为链表很多接口的实现非常简单,这里就没有把所有接口的实现代码一一列举出来了,下一节我们接着分析链表,我们将会分析迭代器失效。那么本节的内容就到此结束了,谢谢您的浏览!!!!!!!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RT-Thread+STM32L475VET6实现红外遥控实验

文章目录 前言一、板载资源介绍二、具体步骤1. 确定红外接收头引脚编号2. 下载infrared软件包3. 配置infrared软件包4. 打开STM32CubeMX进行相关配置4.1 使用外部高速时钟&#xff0c;并修改时钟树4.2 打开定时器16(定时器根据自己需求调整)4.3 打开串口4.4 生成工程 5. 打开HW…

速通HTML

HTML基础 1.快捷键 基于VS Code记录编写过程中常用的快捷键 功能快捷键生成HTML基本骨架!回车保存代码CtrlS在浏览器运行代码AltB注释Ctrl/缩进Tab取消缩进ShiftTab收起侧边栏CtrlB 先保存&#xff0c;再在浏览器运行才能刷新 2.标签 标签作用h1——h6双标签标题标签&#…

WebXR教学 01 基础介绍

什么是WebXR&#xff1f; 定义 XR VR AR Web上使用XR技术的API WebXR 是一组用于在 Web 浏览器中实现虚拟现实&#xff08;VR&#xff09;和增强现实&#xff08;AR&#xff09;应用的技术标准。它由 W3C 的 Immersive Web 工作组开发&#xff0c;旨在提供跨设备的沉浸式体验…

IRI 2016 模型在线版 MATLAB

IRI官网&#xff1a;International Reference Ionosphere IRI-2016在线计算&#xff1a;IRI 2016 | CCMC 官方提供的MATLAB代码需要联网读取IRI网页数据&#xff1a; 下载需要注册账号&#xff0c;没有注册账号的自行注册&#xff0c;下载好后解压是这样的&#xff1a; 下载I…

数据结构系列一:初识集合框架+复杂度

前言 数据结构——是相互之间存在一种或多种特定关系的数据元素的集合。数据结构是计算机专业的基础课程&#xff0c;但也是一门不太容易学好的课&#xff0c;它当中有很多费脑子的东西&#xff0c;之后在学习时&#xff0c;你若碰到了困惑或不解的地方 都是很正常的反应&…

智慧物业平台(springboot小程序论文源码调试讲解)

第4章 系统设计 用户对着浏览器操作&#xff0c;肯定会出现某些不可预料的问题&#xff0c;但是不代表着系统对于用户在浏览器上的操作不进行处理&#xff0c;所以说&#xff0c;要提前考虑可能会出现的问题。 4.1 系统设计思想 系统设计&#xff0c;肯定要把设计的思想进行统…

2024年国赛高教杯数学建模A题板凳龙闹元宵解题全过程文档及程序

2024年国赛高教杯数学建模 A题 板凳龙闹元宵 原题再现 “板凳龙”&#xff0c;又称“盘龙”&#xff0c;是浙闽地区的传统地方民俗文化活动。人们将少则几十条&#xff0c;多则上百条的板凳首尾相连&#xff0c;形成蜿蜒曲折的板凳龙。盘龙时&#xff0c;龙头在前领头&#x…

在PyCharm中集成AI编程助手并嵌入本地部署的DeepSeek-R1模型:打造智能开发新体验

打造智能开发新体验&#xff1a;DeepSeekPycharmollamaCodeGPT 目录 打造智能开发新体验&#xff1a;DeepSeekPycharmollamaCodeGPT前言一、什么是ollama&#xff1f;二、如何使用1.进入ollama官方网站:2.点击下载ollama安装包3.根据默认选项进行安装4.安装成功5.打开命令提示符…

软件测试的基础入门(一)

文章目录 一、什么是软件测试&#xff1f;&#xff08;1&#xff09;生活中的测试案例&#xff08;2&#xff09;代码中的测试示例&#xff08;3&#xff09;软件测试的定义 二、软件测试的重要性三、测试工程师&#xff08;1&#xff09;定义&#xff08;2&#xff09;分类&am…

Linux版本控制器Git【Ubuntu系统】

文章目录 **前言**一、版本控制器二、Git 简史三、安装 Git四、 在 Gitee/Github 创建项目五、三板斧1、git add 命令2、git commit 命令3、git push 命令 六、其他1、git pull 命令2、git log 命令3、git reflog 命令4、git stash 命令 七、.ignore 文件1、为什么使用 .gitign…

20250221 NLP

1.向量和嵌入 https://zhuanlan.zhihu.com/p/634237861 encoder的输入就是向量&#xff0c;提前嵌入为向量 二.多模态文本嵌入向量过程 1.文本预处理 文本tokenizer之前需要预处理吗&#xff1f; 是的&#xff0c;文本tokenizer之前通常需要对文本进行预处理。预处理步骤可…

Spring Boot 3 整合 Spring Cloud Gateway 工程实践

引子 当前微服务架构已成为中大型系统的标配&#xff0c;但在享受拆分带来的敏捷性时&#xff0c;流量治理与安全管控的复杂度也呈指数级上升。因此&#xff0c;我们需要构建微服务网关来为系统“保驾护航”。本文将会通过一个项目&#xff08;核心模块包含 鉴权服务、文件服务…

flutter项目构建常见问题

最近在研究一个验证码转发的app&#xff0c;原理是尝试读取手机中对应应用的验证码进行自动转发。本次尝试用flutter开发&#xff0c;因为之前没有flutter开发的经验&#xff0c;遇到了诸多环境方面的问题&#xff0c;汇总一些常见的问题如下。希望帮助到入门的flutter开发者&a…

Classic Control Theory | 12 Real Poles or Zeros (第12课笔记-中文版)

笔记链接&#xff1a;https://m.tb.cn/h.Tt876SW?tkQaITejKxnFLhttps://m.tb.cn/h.Tt876SW?tkQaITejKxnFL

图解感知机(Perceptron)

目录 1.感知机&#xff08;Perceptron&#xff09;介绍 2.网络结构与工作原理 3.模型工作示例 4.总结 1.感知机&#xff08;Perceptron&#xff09;介绍 感知机&#xff08;Perceptron&#xff09;是最早的人工神经网络模型之一&#xff0c;由弗兰克罗森布拉特&#xff08;…

多旋翼+航模+直升机:多型号无人机飞行表演技术详解

多旋翼、航模、直升机等多种型号的无人机飞行表演技术&#xff0c;是现代科技与艺术的完美结合&#xff0c;它们通过精密的编程、高效的通信、先进的定位与导航技术&#xff0c;以及复杂的编队控制算法&#xff0c;共同呈现出令人震撼的视觉效果。以下是对这些无人机飞行表演技…

deepseek 导出导入模型(docker)

前言 实现导出导入deepseek 模型。deepseek 安装docker下参考 docker 导出模型 实际生产环境建议使用docker-compose.yml进行布局&#xff0c;然后持久化ollama模型数据到本地参考 echo "start ollama" docker start ollama#压缩容器内文件夹&#xff0c;然后拷贝…

【MySQL】表的增删查改(CRUD)(上)

个人主页&#xff1a;♡喜欢做梦 欢迎 &#x1f44d;点赞 ➕关注 ❤️收藏 &#x1f4ac;评论 CRUD&#xff1a;Create&#xff08;新增数据&#xff09;、Retrieve&#xff08;查询数据&#xff09;、Update&#xff08;修改数据&#xff09;、Delete&#xff08;修改数据…

Win11作为宿主机,运行VMware 总没有网络

问题&#xff1a; 移动了VMware到新宿主机上后&#xff0c;虚拟机无法连接网络&#xff0c;其实会显示一个圆圈的图标&#xff0c;这是连接上的图标。 造成这个错误的原因是多种多样的。 用下面的方法来查排查错误。 1.控制面板-> 网络连接 安装好虚拟机后&#xff0c;会…

edge浏览器将书签栏顶部显示

追求效果&#xff0c;感觉有点丑&#xff0c;但总归方便多了 操作路径&#xff1a;设置-外观-显示收藏夹栏-始终