【C++】list 相关接口的模拟实现

news2024/11/18 1:42:01

list 模拟实现

  • 回顾
  • 准备
  • 构造析构函数的构造
    • 构造方法
    • 析构方法
    • 赋值运算符重载
  • 容量相关接口
  • 元素获取
  • 元素修改相关接口
    • push 、pop
    • insert
    • erase
  • 清空
  • 交换
  • 迭代器 **(重点)
    • 迭代器基本概念
    • 迭代器模拟实现

回顾

在上一篇博客中我们大致了解了 list 相关接口的使用方法并进行了一系列的测试练习,那么这一小节就来模拟实现一些接口吧~

list 是一个带头双向循环的链表:

在这里插入图片描述

在模拟实现这些接口之前我们需要先进行节点信息的创建
在这里插入图片描述

	template<class T>   //采用模板------可以定义不同数据类型的 list 链表
	struct ListNode {
		ListNode(const T&value = T())   //构造方法
			:prev(nullptr), next(nullptr), val(value)
		{}

		ListNode* prev;     //前驱节点
		ListNode* next;    //后继节点
		T val;      //值域
	};

准备

为了与类中 list 进行区分,我们可以自定义一个命名空间,在该命名空间内部来进行接口的模拟实现:

namespace xx {
	template<class T>
	struct  ListNode          //创建节点信息
	{
		ListNode(const T& value = T())
			:prev(nullptr), next(nullptr), val(value)
		{}
		ListNode* prev;
		ListNode* next;
		T val;
	};
}

为了便于测试,我们在自己的命名空间中定义一个打印信息的函数:

template<class T>
	void PrintList(const list<T>& L)
	{
		for (auto e : L)
			cout << e << " ";
		cout << endl;
	}

定义链表信息:

//定义链表信息
	template<class T>
	class List {
		typedef ListNode<T> Node;     //取别名
	public:
		//构造方法
		
		//析构方法

		//接口模拟实现
	private:
		Node* _head;     //头节点
	};

构造析构函数的构造

构造方法

由于 list 链表是一个带头的双向循环链表,并且已知 _head 头节点,因此在创建链表时候要注意 prev 与 next 的指向

创建任何类型的构造方法之前,我们首先要定义出一个带头结点的空链表:

在这里插入图片描述

void CreateList()
		{
			//创建头节点
			_head = new Node();
			_head->next = _head->prev = _head;  //构造循环
		}

(1)构造空链表

创建一个空链表也就是创建一个只有头节点的链表,因此我们可以直接在构造方法内部调用头节点创建的函数即可:

List() {
			CreateList();
		}

(2)构造具有 n 个值为 val 的链表

构造一个具有 n 个相同节点的链表,我们可以复用尾插(或头插都可以)来进行

List(int n, const T& val = T())
		{
			CreateList();  //首先创建头节点信息
			//构造 n 个值为 val 的节点,我们可以采用 n 次尾插来进行
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

注意:

在这里插入图片描述

(3)区间构造

template<class Iterator>
		List(Iterator first, Iterator last)
		{
			CreateList();  //创建头节点
			auto it = first;
			while (it != last) {
				push_back(*it);    //同样采用多次尾插方法来构造,*it 代表获取节点值域
				++it;    //迭代器 it 的自增------表示获取下一个节点的位置
			}
		}

注意:

在进行参数构造时候,我们参数类型定义均为模板类型 Iterator ,因此对于上述构造 n 个值相同的节点信息的链表,倘若我们使用 size_t 类型来传入 n 参数,会使得 编译器在进行类型推演时候默认为两个变量类型不一致导致编译器调用错误,这也就解释了为什么将 n 变量类型定义为 int 的原因。

(4)拷贝构造

用一个已有的链表来创建新的链表信息:

List(const List<T>& L)
		{
			CreateList();  //创建头节点
			for (auto e : L) {
				push_back(e);  //遍历 L 链表同时将遍历到的节点值插入到 新构造的链表中
			}
		}

析构方法

~List()
		{
			clear();  //清空所有节点信息
			delete _head;   //删除头节点
			_head = nullptr;
		}

赋值运算符重载

我们采用现代版的写法,传递的参数为值类型的参数(会调用一次拷贝构造函数来构造出该参数),然后将该参数与 this 指针指向的内容进行交换即可(可以参考深浅拷贝 : 添加链接描述):

List<T>& operator=(const List<T> L)  
 //以值的方式传参会调用一次拷贝构造方法来创建参数(具有自己独立的地址空间),并在函数调用结束自动销毁临时空间
		{
			this->swap(L);   //交换之后,L 中空间改变为原来的 this 空间,并在函数调用结束自动进行了析构销毁
			return *this; 
		}

容量相关接口

由于 list 链表结构为带头双向循环链表,我们只知道 头节点 _head 的信息,因此在进行容量判断时候需要对整个链表进行遍历------注意遍历条件!!!

(1)size 接口

size_t size()const
		{
			//统计节点个数
			int count = 0;
			Node* cur = _head->next;
			while (cur != _head) {          //循环链表的 next 指针域一定是不为空的,因此遍历条件应该是判断是否回到头节点位置
				++count;
				cur = cur->next;
			}
			return count;
		}

(2)判空

循环链表为空时,也就是只有一个头节点存在,故判断条件应为:

bool empty()const
		{
			return _head == _head->next;
		}

(3)resize 修改有效节点的个数

当缩小有效节点个数为 newsize 时,我们需要将 newsize 之后的节点进行删除;
在这里插入图片描述

当扩大有效节点个数为 newsize 时,我们需要在原有的节点尾部插入 newsize-oldsize 个新的节点,且新节点的值为 val ;

在这里插入图片描述

void resize(size_t newsize,const T& val=T())
		{
			size_t oldsize = size();  //统计现有的节点个数
			//当缩小节点
			if (newsize < oldsize) {
				for (int i = newsize; i < oldsize; ++i)
					pop_back();  //进行尾删
			}
			else {
				//增大节点个数
				for (int i = oldsize; i < newsize; ++i)
					push_back(val);  //进行尾插操作
			}
		}

元素获取

(1)获取首节点信息

//非 const 类型表示可以对节点信息进行修改操作
		T& front()
		{
			//获取首节点值
			return _head->next->val;   //_head 为头节点,它所存储的数据不是有效数据
		}

		const T& front()const        //只读
		{
			//获取首节点值
			return _head->next->val;   //_head 为头节点,它所存储的数据不是有效数据
		}

(2)获取尾节点信息

		T& back()
		{
			return _head->prev->val;      //_head 为头节点,它的前驱节点为尾节点
		}

		const T& back()const
		{
			return _head->prev->val;      //_head 为头节点,它的前驱节点为尾节点
		}

元素修改相关接口

push 、pop

由于头部或尾部插入新元素都可以直接复用 insert 任意位置插入方法,因此我们这里直接调用 insert 接口来实现:

		void push_front(const T& val)
		{
			insert(begin(), val);    //begin() 指向首节点,因此进行头插 ,直接在第一个节点前插入新节点 
		}
		void push_back(const T& val)
		{
			insert(end(), val);  //end() 指向头节点,因此进行尾插,直接在 end() 之前插入新节点
		}

由于头部或尾部删除之间可以调用 erase 任意位置删除,所有这里也直接调用erase 接口:

		void pop_front()
		{
			if (empty())
				return;   //链表为空,不能进行删除
 			//头删------删除首节点
			erase(begin());  //删除 begin() 位置节点
		}
		void pop_back()
		{
			if (empty())
				return;   //链表为空,不能进行删除
			//尾删------删除最后一个节点
			erase(end());    
		}

注意看这两段代码是否有问题?

能这么问,当然是有问题啦~

具体什么问题我们来看看:

在这里插入图片描述

仔细观察 begin() 与 end() 的位置,我们发现 begin() 指向的就是第一个节点的位置,因此进行头删时候直接可以进行删除,并且删除之后并不会影响之后元素的访问
而 end() 指向的是头节点的位置,而我们要删除的是最后一个有效节点,也就是 end() 的前一个节点位置,因此此处的尾删函数的实现是不对的,具体修改如下:

	void pop_back()
		{
			if (empty())
				return;   //链表为空,不能进行删除
			//尾删------删除最后一个节点

			auto pos = end();
			--pos;  //迭代器向前移动到尾节点位置
			erase(pos);     //删除 end() 前一个位置节点
		}

list 接口测试中,我们谈到 在插入节点时不会导致迭代器的失效,而在删除元素时候会引发迭代器失效,但是不会导致迭代器位置之后的元素的访问,因此一般在删除节点操作之后我们会接收返回值的信息来防止迭代器失效。
(list 接口使用中我们已经进行了测试,忘记的宝子参考:添加链接描述)

insert

在任意位置进行元素的插入,首先我们需要创建出一个新节点信息,然后对节点的指向进行修改即可:

Iterator insert(Iterator pos, const T& val)
		{
			//在给定的节点位置 pos 之前进行新节点的插入
			Node* newnode = new Node();  //创建新节点
			newnode->val = val;

			newnode->next = pos;
			newnode->prev = pos->prev;

			pos->prev->next = newnode;
			pos->prev = newnode;

			return newnode;  //返回新插入的节点位置
		}

画个图来理解一些吧~

在这里插入图片描述

erase

Iterator erase(Iterator pos)
		{
			//删除给定的 pos 位置的节点
			if (pos == _head)
				return _head;  //头节点不能进行删除

			Node* cur = pos->next;  //记录下一个节点位置
			//修改指向
			cur->prev = pos->prev;
			pos->prev->next = cur;
			delete pos;
			
			return cur;   //返回删除节点的位置-------此时迭代器 pos 已经失效了
		}

删除节点与插入节点过程很类似,注意修改指向的顺序,读者可以自己画画图来理解

清空

clear 是将 list 链表中所有节点进行删除,我们可以采用头删的方法来进行:

	void clear()
		{
			Node* cur = _head->next;   //进行头删
			while (cur != _head) {
				cur->next->prev = _head;
				_head->next = cur->next;
				delete cur;
				cur = _head->next;     //修改要删除的节点位置
			}
			_head->next = _head->prev = _head;       //最后删除头节点
		}

交换

void swap(List<T>& L) {
			std::swap(_head, L._head);      //之间采用全局 swap 函数,交换头节点位置即可
		}

迭代器 **(重点)

迭代器基本概念

string 、vector 还有现在学习的 list 当中,我们都有使用到迭代器,那么迭代器到底是什么呢?

在前边的学习中我们提到 ,迭代器可以看作是原生态的指针类型,在模拟接口中我们发现,定义的迭代器变量我们可以对其进行以下操作:

1)解引用 *
2)自增自减
3)迭代器的比较

例如在遍历时我们使用到的迭代器(vector 容器下的迭代器):
在这里插入图片描述

因此,我们在模拟实现迭代器时候也要能够进行这三种基本操作。

接下来,我们来模拟实现一些迭代器吧~

迭代器模拟实现

在前边模拟实现 string 以及 vector 时,我们将迭代器处理为原生态的指针类型,发现在整个模拟接口测试过程中是没有任何问题的,说明在之前的模拟实现中 ,迭代器就是被当作指针来进行处理的,那么在 list 中我们是否也可以这么处理?

同样将迭代器看作是原生态的指针类型:

typedef Node* Iterator;

当我们进行迭代器的解引用以及自增自减:

		auto it = begin();
		while (it!=end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

在这里插入图片描述
那么为什么在 string 和 vector 当中,迭代器可以被处理成为 原生态的指针并且可以正常使用,在这里就不行了呢?(大家可以思考思考)

解答
回顾我们在学习 string 和 vector 容器中,使用的是顺序结构,也就是说所采用的容器空间是连续的,因此进行自增自减可以直接获取到它相邻的前后元素位置
而在 list 中,我们知道 list 是多个节点构成的链表,而每一个节点都是在使用时才创建(new)出来的,然后将创建的新节点链接到我们的链表当中,由此可见 list 当中的结构并不一定连续的,故不能直接进行迭代器的自增自减操作;

其次,假如将迭代器定义为原生态的指针类型 Node* ,在进行解引用操作时候取到的类型是 Node 而并非是当前迭代器指向位置的值域(val)信息,因此迭代器不能被简单的处理为 原生态的指针;

再者,我们提到迭代器可以进行比较,而原生态指针类型定义出来的迭代器类型都是一致的,类型间如何进行比较?

由此可见,在 list 中模拟实现迭代器时,我们首先需要对迭代器进行封装操作:

正向迭代器的封装

//封装迭代器类-------------要求能够使用指针解引用,并能够进行自增自减操作,并能够进行迭代器的比较
	template<class T,class Ptr,class Ref>
	class ListIterator {
		typedef ListNode<T> Node;

	public:
		typedef Ptr Ptr;          //类型重命名,指明当前 Ptr 是类型而非变量
		typedef Ref Ref;
		typedef ListIterator<T, Ptr, Ref> Self;   //因为节点当中包含值域和指针类型,因此我们设计迭代器时需要能够返回不同的类型的数据信息
	public:
		ListIterator(Node* pNode = nullptr) :_pNode(pNode)
		{}

		// 解引用
		Ref operator*()        //返回值信息应该是节点当中的数据类型
		{
			return _pNode->val;             //解引用也就是获取当前节点中的值域信息
		}
		Ptr operator->()     //返回值信息为当前节点数据域的地址
		{
			return &(_pNode->val);           //在自定义类型中体现很明显
		}

		/// 自增自减
		Self& operator++()          //返回值为迭代器自身类型
		{
			_pNode = _pNode->next;        //自增操作也就是将迭代器的位置向后移动---------获取下一个位置的节点
			return *this;
		}
		Self operator++(int)  //后置++
		{
			Self tmp(*this);           //后置++:先使用,后对其进行修改
			_pNode = _pNode->next;
			return tmp;
		}
		Self& operator--()
		{
			_pNode = _pNode->prev;    //自减操作也就是将迭代器向前移动--------获取前一个位置的节点
			return *this; 
		}
		Self operator--(int)  //后置--
		{
			Self tmp(*this);
			_pNode = _pNode->prev;
			return tmp;
		}


		 迭代器能够比较
		bool operator!=(const Self& s)const
		{
			return _pNode != s._pNode;
		}
		bool operator==(const Self& s)const
		{
			return _pNode == s._pNode;
		}

		Node* _pNode;
	};


反向迭代器的封装

在前边我们介绍到,正向迭代器 begin() 的位置与反向迭代器 rend() 位置相同,正向迭代器 end() 位置与反向迭代器 rbegin() 位置相同,两种迭代器正好相反,因此进行反向迭代器封装我们可以复用正向迭代器的方法:

template<class Iterator>
	struct ListReverseIterator {
		//typename 是为了说明 Ref Ptr 是属于正向迭代器 Iterator 中的类型而不是静态成员变量
		typename typedef Iterator::Ref Ref;
		typename typedef Iterator::Ptr Ptr;
		typedef ListReverseIterator<Iterator> Self;
	public:
		ListReverseIterator(Iterator it) :_it(it)
		{}
		Ref operator*()
		{
			Iterator tmp = _it;    //rbegin() 指向 _head 头节点,不需要进行打印,因此 解引用 获取到的是第一个节点的值域,即需要将 rbegin 向后(++操作)移动-----------即需要将正向迭代器的 end() 向前移动(--操作)
			--tmp;
			return *tmp;      //返回值是节点数据域中的数据类型
		}
		Ptr operator->()     //获取当前节点数据域的地址信息
		{
			return &(_it->pNode->val);
		}

		Self& operator++() {
			--_it;      //反向迭代器的前置++ 等价于 正向迭代器的前置--
			return *this;
		}
		Self operator++(int)
		{
			_it--;     //反向迭代器后置++ 等价于 正向迭代器的后置--
			return *this; 
		}

		Self& operator--() {
			++_it;      //反向迭代器的前置-- 等价于 正向迭代器的前置++
			return *this;
		}
		Self operator--(int)
		{
			_it++;     //反向迭代器后置-- 等价于 正向迭代器的后置++
			return *this;
		}

		///
		bool operator!=(const Self& s)const
		{
			return _it != s._it;
		}
		bool operator==(const Self& s)const
		{
			return _it == s._it;
		}

		Iterator _it;
	};

封装之后我们需要在我们自己定义的 List 类中进行声明:

		typedef ListIterator<T,T*,T&> Iterator;  //迭代器封装之后要能够返回不同的数据类型(节点值域,节点的地址等)
		typedef ListIterator<T, const T*, const T&> const_Iterator;         //const 迭代器-----只读
		
		typedef ListReverseIterator<Iterator> reverse_iterator;
		typedef ListReverseIterator<const_Iterator> const_reverse_iterator;  //const 迭代器-----只读

则迭代器的模拟接口实现如下:

Iterator begin()        //begin() 获取首节点的位置
		{
			return Iterator(_head->next);   //返回值类型的临时对象
		}
		Iterator end()
		{
			return Iterator(_head);
		}

		//反向迭代器
		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}
		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		//const 迭代器
		const_Iterator begin()const
		{
			return const_Iterator(_head->next);
		}
		const_Iterator end()const
		{
			return const_Iterator(_head);
		}

		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}
		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}

由于我们对迭代器进行了封装而并非原始方式定义为原生态的指针类型,因此采用迭代器来进行插入删除元素的函数也要进行相应的修改操作:

Iterator insert(Iterator Itpos, const T& val)
		{
			//在 pos 位置之前进行插入
			Node* pos = Itpos._pNode;     
	//此时的迭代器 Itpos 所指向的并不是当前节点的位置信息(而是包含三种数据结构的信息),因此需要进行取节点操作
	
//后续的代码与前边是相同的
			Node* newnode = new Node();
			newnode->val = val;

			newnode->prev = pos->prev;
			newnode->next = pos;
			pos->prev->next = newnode;
			pos->prev = newnode;

			return newnode;
		}

		Iterator erase(Iterator Itpos)
		{
			//删除 pos 位置的元素
			Node* pos = Itpos._pNode;
				//此时的迭代器 Itpos 所指向的并不是当前节点的位置信息,因此需要进行取节点操作
				
//后续的代码与前边是相同
			if (pos == _head)
				return pos;
			Node* cur = pos->next;
			pos->prev->next = cur;
			cur->prev = pos->prev;
			delete pos;

			return cur;
		}

好了,今天的学习就到这里啦
对于迭代器部分的理解可能比较困难,读者可以自己在代码当中多调试调试看看

在这里插入图片描述

关于本节具体的代码实现请参考(mylist 文件):添加链接描述

有任何问题欢迎评论留言哦!

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

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

相关文章

零基础、非计算机系学Python该如何上手?

首先我觉得要放平心态&#xff0c;不用过多去纠结是不是专业出身这回事。 想学那就认真去学&#xff0c;我们最终目标是掌握Python这门技能。 非计算机专业同时零基础&#xff0c;想自学Python该如何上手&#xff1f;分享我自学Python的几点建议吧。 1、重视基础 Python是一…

《数据库系统概论》学习笔记——第七章 数据库设计

教材为数据库系统概论第五版&#xff08;王珊&#xff09; 这一章概念比较多。最重点就是7.4节。 7.1 数据库设计概述 数据库设计定义&#xff1a; 数据库设计是指对于一个给定的应用环境&#xff0c;构造&#xff08;设计&#xff09;优化的数据库逻辑模式和物理结构&#x…

TIA博途Wincc中自定义配方画面的具体方法示例

TIA博途Wincc中自定义配方画面的具体方法示例 前面和大家分享了通过TIA博途自带的配方视图组态配方功能的具体方法,具体内容可参考以下链接中的内容: TIA PORTAL wincc中配方recipe组态及配方视图的使用方法 但是,使用配方视图的时候感觉不是很方便,同时一部分使用人员也感…

机加行业MES解决方案,助力企业打造数字化透明车间

机械加工行业的主要原材料占整个生产物料成本的95%~99%&#xff0c;以挖掘机为例&#xff0c;原材料有各种规格的钢板、焊丝、焊条、油漆以及各种气体等&#xff0c;其中主要原材料是钢板&#xff0c;占原材料比率的98%以上。 因此机械加工mes的原材料管理是机械加工行业信息化…

【GO】30.grpc拦截器源码分析

一.服务端拦截器server端原理serverOptions配置中的Interceptor&#xff0c;其中unary为一元拦截器&#xff0c;stream为流式拦截器。本文只看一元式拦截器&#xff0c;即最常见的客户端向服务器发送单个请求并返回单个响应。创建一个新的grpc server时&#xff0c;这个方法将拦…

什么?你还不明白什么是ClassLoader?不如试试从JVM来入手ClassLoader是什么玩意吧!

文章目录环境配置篇如何执行一个文件配置JDK环境&#xff08;简述&#xff09;Java文件执行流程编译加载JVM环境准备BootStrapClassLoadersun.misc.laucherAppClassLoader解释执行回收ClassLoader讲解主要的三个ClassLoader双亲委派模型loadClass方法讲解自定义ClassLoaderJVM内…

多芯片设计 Designing For Multiple Die

Why a system-level approach is essential, and why its so challenging作者&#xff1a;Ann MutschlerAnn Mutschler is executive editor at Semiconductor Engineering.将多个裸片或芯粒集成到一个封装中&#xff0c;与将它们放在同一硅片上有着很大的区别。在同一硅片上&a…

断点续传实现

断点续传 1、 什么是断点续传 通常视频文件都比较大&#xff0c;所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制&#xff0c;但是客户的网络环境质量、电脑硬件环境等参差不齐&#xff0c;如果一个大文件快上传完了网断了没有上…

分布式之ZAB协议

写在前面 假定我们现在使用zk执行了如下的指令&#xff1a; [zk: 192.168.0.10:2181(CONNECTED) 0] create /dongshidaddy 123 Created /dongshidaddy [zk: 192.168.0.10:2181(CONNECTED) 1] create /dongshidaddy/mongo 456 Created /dongshidaddy/mongo假定因为节点故障最终…

Python曲线肘部点检测-膝部点自动检测

文章目录一. 术语解释二. 拐点检测肘部法则是经常使用的法则。很多时候&#xff0c;可以凭人工经验去找最优拐点&#xff0c;但有时需要自动寻找拐点。最近解决了一下这个问题&#xff0c;希望对各位有用。一. 术语解释 **肘形曲线(elbow curve)**类似人胳膊状的曲线&#xff…

Echarts 每个柱子一种渐变色的象形柱状图

第023个点击查看专栏目录本示例是解决每个柱状图的每一个柱子都呈现一种渐变色&#xff0c;每个柱子的颜色都不同。这里同时采用了象形的柱状图效果。 文章目录示例效果示例源代码&#xff08;共125行&#xff09;相关资料参考专栏介绍示例效果 示例源代码&#xff08;共125行&…

JavaScript DOM【快速掌握知识点】

目录 DOM简介 获取元素 修改元素 添加和移除元素 事件处理 DOM简介 JavaScript DOM 是指 JavaScript 中的文档对象模型&#xff08;Document Object Model&#xff09;&#xff1b;它允许 JavaScript 与 HTML 页面交互&#xff0c;使开发者可以通过编程方式动态地修改网页…

RocketMQ源码分析

RocketMQ源码深入剖析 1 RocketMQ介绍 RocketMQ 是阿里巴巴集团基于高可用分布式集群技术&#xff0c;自主研发的云正式商用的专业消息中间件&#xff0c;既可为分布式应用系统提供异步解耦和削峰填谷的能力&#xff0c;同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠…

汽轮机胀差及轴向位移(转载的)

这个文章是微信公众号推送看到的。搬运到这里方便以后学习用。 1、轴向位移和胀差的概念 轴位移指的是轴的位移量&#xff0c;而胀差则指的是轴相对于汽缸的相对膨胀量,一般轴向位移变化时其数值较小。轴向位移为正值时&#xff0c;大轴向发电机方向移&#xff0c;若此时汽缸膨…

如何快速了解一个系统?

前言 开发人员经常会面临下面一些场景&#xff1a; 新人入职&#xff0c;需要学习已有系统&#xff0c;作为 landing 的一部分&#xff0c;如何学习&#xff1f;被拉过去参与一个陌生系统的迭代开发或者系统维护&#xff08;bugfix&#xff09;&#xff0c;如何快速上手&…

关键词聚类和凸现分析-实战1——亚急性甲状腺炎的

审稿人问题第8页第26行-请指出#是什么意思&#xff0c;并解释为什么亚急性甲状腺炎在这里被列为#8。我认为在搜索亚急性甲状腺炎相关文章时&#xff0c;关键词共现分析应该提供关键词共现的数据。这些结果的实际用途是什么?亚急性甲状腺炎是一种较为罕见但重要的甲状腺疾病&am…

vue + qiankun 项目搭建

一、cli3构建vue2项目 1、前期工作&#xff1a;查看cli安装情况与安装 npm install -g vue/cli 已安装情况查看&#xff1a;vue -V(大写的V) 2、新建项目 vue create main-project 3、选择自定义配置 配置选择 选择vue版本、babel、router、vuex、css预处理器、lint格式校…

【神经网络】GRU

1.什么是GRU GRU&#xff08;Gate Recurrent Unit&#xff09;门控循环单元&#xff0c;是循环神经网络&#xff08;RNN&#xff09;的变种种&#xff0c;与LSTM类似通过门控单元解决RNN中不能长期记忆和反向传播中的梯度等问题。与LSTM相比&#xff0c;GRU内部的网络架构较为简…

Android 实现菜单拖拽排序

效果图简介本文主角是ItemTouchHelper。它是RecyclerView对于item交互处理的一个「辅助类」&#xff0c;主要用于拖拽以及滑动处理。以接口实现的方式&#xff0c;达到配置简单、逻辑解耦、职责分明的效果&#xff0c;并且支持所有的布局方式。功能拆解功能实现4.1、实现接口自…

【员工管理系统】

员工管理系统前言需求分析系统设计系统框图所需技术系统实现编写代码测试前言 这是一个使用epoll实现TCP并发服务器&#xff0c;并让客户端登录服务器可以进行员工的管理&#xff0c;员工的信息存储在sqlite数据库中&#xff0c;对数据库进行增删改查实现对员工的添加&#xf…