C++ 栈-队列-优先级队列

目录

1 栈

2 队列

3 deque 介绍

4 优先级队列

5 反向迭代器


栈也是我们在C语言就模拟实现过的一种数据结构,在C++中,栈其实和我们前面模拟实现过的string、vector等容器有一点区别,站起是不是容器,而是一种容器适配器,我们可以打开C++官方文档找到stack的介绍来看一下

同时我们打开队列的文档就会发现,队列也是一种容器适配器,

那么适配器到底是什么东西呢?适配器其实是一种设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

在前面的容器的模拟实现中,困扰我们颇多的迭代器也是一种设计模式。而适配器是设计模式中结构模式中的一种。 适配器本质上是进行转换的,使用现有的东西封装转换成你想要的东西。

1 栈

我们说栈是一种容器适配器,而适配器的本质是进行转换。

我们发现栈的结构很少,因为他的结构限制了他的接口,而在之前C语言我们实现栈使用一个动态顺序表来实现的,因为栈的接口其实就相当于顺序表的尾插尾删,使用数组完成效率很高,这其实就是一种转换,我们将顺序表通过封装接口转换出我们想要的栈。而在C++中,我们也能够使用前面模拟实现过的 vector 和 list 来作为栈的适配容器,具体使用哪个其实都行,在栈的实现上,它们各有千秋,而我们看到的库里面的栈的默认适配容器则是一个双端队列 deque

这个容器我们还没学过,在栈和队列后面会讲一下。

目前我们实现栈就使用vector来作默认容器,因为其实 vector 实现栈的效率其实跟deuqe差不多的。栈的基本的结构如下,就是一个vector,然后封装vector的接口来实现栈的接口

栈的构造函数只有一个,而这个构造函数我们直接理解为无参的构造就行了,这里的缺省值的意思其实就是可以使用我们的适配器容器的对象来初始化,但是我们一般实现栈是不需要这样初始化的。

既然是一个无参构造,那么我们需要自己写吗?不需要,因为编译器自动生成的就够用了,栈对象中只有一个我们实现过的vector的对象,而我们实现过的vector是有默认构造的,那么编译器自动生成构造函数就会去调用vector的默认构造去对st进行初始化。那么析构也是一样的,我们也不需要自己去实现。

那么剩下的几个接口 size 、empty 、top 、 push 、pop就很简单了。

        

		bool empty()const
		{
			return st.empty();
		}

		size_t size()const
		{
			return st.size();
		}

		T& top()
		{
			assert(!empty());
			return st.back();
		}

		const T& top()const
		{
			assert(!empty());
			return st.back();
		}

		void push(const T& val)
		{
			st.push_back(val);
		}

		void pop()
		{
			assert(!empty());
			st.pop_back();
		}

这样一来,一个简单的栈就实现了。栈是不需要实现迭代器的,因为不需要,对于栈而言,遍历整个栈就是需要用户不断地取top,然后pop,而不是像容器一样通过迭代器遍历。

2 队列

队列是先入先出的结构,也就是头插和头删,尾插头删vector的效率就不如list了,所以我们实现queue使用list作为默认容器。

其他的接口也基本上和stack差不多

	bool empty()const
		{
			return q.empty();
		}

		size_t size()const
		{
			return q.size();
		}

		const T& front()const
		{
			assert(!empty());
			return q.front();
		}

		const T& back()const
		{
			assert(!empty());
			return q.back();
		}

		void push(const T& val)
		{
			q.push_back(val);
		}

		void pop()
		{
			assert(!empty());
			q.pop_front();
		}

3 deque 介绍

为什么stack和queue的默认适配容器都是 deque 双端队列呢?deque到底是何方神圣?

在介绍 deque之前,我们先回想一下vector和list都有什么缺点。vector的缺点就是扩容的开销大,因为他的扩容需要挪动数据,同时他的头插头删的开销也很大,不适合作为队列的适配容器。 而list的缺点则是不支持随机访问,同时由于空间不连续,他的CPU高速缓存命中率低。

那么能不能设计一个容器,能够完美融合vector和list的优点,同时能够规避他们的缺点呢?从结果上来说,肯定是没有的,如果有容器能够完美替代vector和list,如今我们也不会学习这两个容器了,早就被湮没在历史中。双端队列就是一个兼具 vector 和 list 的优点的一个容器 ,比如他的头插头删效率高,cpu命中率高等,但是他的这些性能都比不上单独拎出来的 list 和vector,相当于不上不下的水平,list 和vector在各自的领域是极致效率的,而deque的效率最多只能用拔尖来描述,比不上list 和vector。

deque是怎么做到既适合头插头删,同时cpu命中率还高的呢?这就要从它的结构说起了。

deque其实是由一个一个的数组来构成的,同时 deque 内部存储了一个指针数组,来保存些数组的地址。

当我们第一次插入数据时,首先申请一个数组buffer,buffer的大小是由我们自己来指定的静态数组。 第一个申请的数组的指针保存在 中控数组的中间位置上

而当我们不断尾插数据时,数组的尾部进行插入,如果数组满了,再次尾插,则会再开辟一个同样大小的数组,将数组的地址保存在中控数组的后一个位置

依次类推,当尾插到一定数量时,中控数组的后半部分都已经被填满了,这时候就会被中控数组进行二倍扩容

扩容之后会将原中控数组保存的指针全部拷贝到新的数组中,同时不会释放原来的 buffer ,再创建一个新的buffer来存储新插入的数据。 所以说,deque其实也是有扩容的消耗的,只是他的消耗相比于vector来说要小得多,它只需要挪动指针数组就行了。

而如果要头插的话,则需要在中控数组的中间位置的前面填充新创建的buffer的指针,然后将新的数组从专门用于头插的 buffer 的尾部开始插入,不断往前插入,过程基本类似于尾插。

而deque支持的随机访问则需要在 [ ]重载的时候进行一定的运算转换来找到对用下标的数据,所以他的随机访问的效率其实也是不如vector的。

同时,我们也能发现一个问题,就是deque在进行中间位置的插入删除的话会很复杂,如果要插入或者删除的数据个数刚好是 buffer 大小的整数倍还好说,如果不是整数倍,则要比 vector 和list 麻烦的多,所以其实deque的任意位置插入删除的效率也是比不上 list 的 

但是这其实也是它的优势所在,他进行除了中间位置插入删除之外的其他的操作的操作都还不错,我们知道 queue 和stack是不需要中间位置的插入删除的,所以由 deque 作为 stack 和 queue 的默认适配容器刚刚好,很通用。 

如果要模拟实现 deque ,最复杂的其实还不是他的下标转换以及他的中间位置的插入删除,而是他的迭代器的实现,他的迭代器的实现需要四个指针来实现,具体的实现大家可以在网上查阅相关资料,总之十分的复杂,简直折磨人 。我们只需要知道她大概的实现就OK了,最重要的就是中控数组也就是一个指针数组就行了。

4 优先级队列

优先级队列其实就是把优先级最高的或者优先级最低的数据放在第一个,那么之前我们有没有学过类似的结构呢? 学过,就是我们的堆。 大堆就是把最大的数放在堆顶,也就是优先级最高的数据放在最前面,而小堆就是把优先级最小的数据放在最前面。而我们以前实现了堆,他的物理结构其实是一个数组,也就是一个vector,而他的逻辑结构我们看成是一棵完全二叉树。

那么优先级队列的默认适配容器我们就能够使用vector来实现。同时,虽然他叫做优先级队列,但是他的本质上还是一个堆,同时他也只能够读取头部数据以及头删,他的插入是需要进行调整来保证堆结构的。

我们可以看一下库里面的优先级队列的模板参数,我们发现模板参数除了我们意料之中的数据类型,适配容器,还有一个 less<...> ,这个less< >相信我们在leetcode刷题的时候或者我们使用sort的时候也经常使用。

我们先不管这个less 仿函数,假设我们就用 vector 实现一个大堆,实现完之后我们再将仿函数加上。

他的接口中,默认的构造和析构编译器自动生成的就够用,不需要我们自己写。而 empty ,size 和top接口实现起来也十分简单,

	public:
		bool empty()const
		{
			return pq.empty();
		}

		size_t size()const
		{
			return pq.size();
		}

		const T& top()const
		{
			assert(!empty());
			return pq[0];
		}

他的重点在插入和删除上,对于插入数据,以前我们使用堆的时候插入数据首先是插入在数组的最后,也就是尾插,然后通过向上调整来维持堆结构。我们可以写一下push和adjustup的实现

		//push 就是尾插
		void push(const T& val)
		{
			pq.push_back(val);
			//然后向上调整
			adjust(size()-1); //传新插入的数据的的下标
		}

		void adjust(size_t child)
		{
			size_t parent = (child - 1) / 2; //父节点的下标
			while (child) //child 为 0 时退出循环
			{
				if (pq[child] >pq[parent])  //假设是建大堆、
				{
					swap(pq[child],pq[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

然后实现 pop ,pop 就是删除 栈顶元素,但是删除栈顶元素之后我们还需要通过向下调整来保证栈结构。

		void pop()
		{
			swap(pq.front(),pq.back());
			pq.pop_back();
			adjustdown(0); //开始调整下标 
		}

		//向下调整
		void adjustdown(size_t parent)
		{
			size_t maxchild = parent*2+1;
			
			while (maxchild<size())
			{
				if (maxchild + 1 < pq.size() && pq[maxchild + 1] > pq[maxchild]) //假设是大堆
					maxchild++;
				if (pq[parent] < pq[maxchild])
				{
					swap(pq[parent],pq[maxchild]);
					parent = maxchild;
					maxchild = parent*2 + 1;
				}
				else
					break;
			}
		}

这样一来就写好了弹出栈顶元素和向下调整的方法了。

最后再实现一个迭代器区间的构造函数,

这个函数还是要用到仿函数,但是我们在这里其实也可以不使用仿函数,因为我们可以直接复用adjustdown来进行向下调整建大堆,一会讲完了仿函数之后我们也只需要修改向下调整和向上调整的逻辑就行了,而不用修改这里的构造函数。

向下调整建大堆我们只需要找到第一个非叶子节点,然后往前遍历,让每一棵子树都是大堆就行了。至于为什么不用向上调整建大堆 可以 看一下我的数据结构专栏的堆的实现,是由于向下调整建堆的效率比向上调整建堆的效率是要高得多的,效率差距主要体现在叶子节点的向上调整中。

	//默认构造
		priority_queue()
		{}

		//迭代器区间构造
		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:pq(first,last)
		{
			size_t end = (size() - 1 - 1) / 2;
			while (end!=0)
			{
				adjustdown(end);
				end--;
			}

			adjustdown(end);
		}

这里要注意的一点就是,如果我们写了迭代器区间的构造,那么编译器就不会自动生成默认构造函数了,我们需要自己实现一个默认构造,而这里的priority_queue的默认构造我们也什么都不用写,编译器会再初始化列表自动调用vector的默认构造来进行初始化,析构函数也是一样的,编译器自动生成的就能够完成析构了。

仿函数

实现完一个大堆,我们就要开始该清楚仿函数是什么东西,只有搞清楚了仿函数我们才能够实现一个完成的优先级队列。

仿函数其实就是一个类,这个类实例化出来的对象就叫仿函数,类里面重载了( ) 这个函数调用操作符。叫做仿函数的原因是这个类的对象可以像函数调用一样实现按我们的逻辑。

比如:

template<typename T>
class myless
{
public:
	bool operator()(const T& x, const T& y)const 
	{
		return x < y;
	}
};

这样一个类能够干什么呢?

我们使用 sort 的时候是不是有这样的用法  

sort ( container :: iterator first  , container :: iterator last , less<T> ( ) )

这里面的 less<T>() 就是一个仿函数,为什么这么说呢?参考我们上面实现的仿函数类, <T>表示数据类型是T,编译器在实例化出这个匿名对象的时候就会使用具体的 T 去实例化,然后用这个匿名对象的 ( ) 也就是 operator() 来传参,也就是将这个成员函数传给了 sort 。

		vector<int> v;
		v.push_back(5);
		v.push_back(7);
		v.push_back(4);
		v.push_back(3);
		v.push_back(9);
		v.push_back(2);

		vector<int>v1 = v;

		sort(v.begin(),v.end(),myless<int>());

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		sort(v1.begin(), v1.end(), less<int>());
		for (auto e : v)
		{
			cout << e << " ";
		}

		cout << endl;

我们可以看一下我们自己实现的的 myless 和库里面的 less 是不是一样的

其实仿函数的传参就有点类似于我们以前C语言传递函数指针,但是函数指针写起来很复杂,尤其是对于一些复杂的函数指针,同时函数指针也不支持泛型编程,每一个类型的比较都需要写一个函数来实现,但是仿函数他就是一个类实例化出来的对象,我们传参穿的也是这个对象,我们只需要显式传递数据类型,就能够实例化出我们想要的数据类型的比较函数。

上面的传参我们也可以使用一个有名对象来传参

		myless<int> mless;
		sort(v.begin(),v.end(),mless);

我们以前的泛型都是类型的泛型,而这里的仿函数的泛型则是逻辑的泛型。

那么我们就可以用仿函数来完善我们的优先级队列了。

	template<typename T ,typename container_type=vector<T>,typename CMP=less<T>>
void adjustup(size_t child)
		{
			CMP cmp;
			size_t parent = (child - 1) / 2; //父节点的下标
			while (child) //child 为 0 时退出循环
			{
				//if (pq[child] >pq[parent])  //假设是建大堆
				if(cmp(pq[parent],pq[child]))
				{
					swap(pq[child],pq[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		//向下调整
		void adjustdown(size_t parent)
		{
			CMP cmp;
			size_t maxchild = parent*2+1;
			
			while (maxchild<size())
			{
				//if (maxchild + 1 < pq.size() && pq[maxchild + 1] > pq[maxchild]) //假设是大堆
				if (maxchild + 1 < pq.size() && cmp(pq[maxchild],pq[maxchild+1])) //假设是大堆
					maxchild++;
				//if (pq[parent] < pq[maxchild])
				if (cmp(pq[parent] , pq[maxchild]))
				{
					swap(pq[parent],pq[maxchild]);
					parent = maxchild;
					maxchild = parent*2 + 1;
				}
				else
					break;
			}
		}

这还只是仿函数的基础用法,仿函数还有一些高级用法,比如我们要实现两个对象的大小的比较

class A 
{
public:
	A(int n = 0)
		:_n(n)
	{}

	bool operator<(const A& a)const
	{
		return _n < a._n;
	}
private:
	int _n;
};

我们首先要在该类中重载 < 运算符,然后创建两个对象进行比较


int main()
{
	A a1(10);
	A a2(20);

	int ret = myless<A>()(a1,a2);
	cout << ret << endl;

	return 0;
}

目前来看没什么问题,但是当我们的对象是使用 new 申请的呢?这时候我们一般使用的就是对象的指针了,如果我们想要直接传这两个指针进行两个指针所指向的对象比较,目前我们实现的仿函数类是做不到的,

	A* p1 = new A(20);
	A* p2 = new A(10);
	int ret = myless<A*>()(p1,p2);
	cout << ret << endl;

 我们只能这么用,而且这样比较出来的结果还不一定是对的,因为这样比较的话,比较的是两个指针的大小,而不是对象的大小了,但是我们就是想要比较类对象的大小要怎么做呢? 

很简单,我们只需要在仿函数类中再重载一个( )函数就行了.

	bool operator<(const A& a)const
	{
		return _n < a._n;
	}

这样一来,如果我们传的是A类型的指针,我们也可以直接使用 myless<A>的仿函数进行比较

	A* p1 = new A(20);
	A* p2 = new A(10);
	int ret = myless<A>()(p1,p2);
	cout << ret << endl;

我们这里举例的例子其实用的不是很合适,更恰当的例子是类似于一个排序算法的场景,既要支持对象本身的比较,还要支持参数是对象指针时对指针所指向的对象的比较。

5 反向迭代器

为什么我们前面没实现反向迭代器,因为反向迭代器其实就是一种适配器,为什么这么说呢?我们知道,反向迭代器和正向迭代器唯一的区别就是 ++ 和 -- 的方向,正向迭代器 ++  是从前往后走 ,而反向迭代器的 ++ 是从后往前走,所以要实现反向迭代器我们只需要封装和复用正向迭代器的代码,就能够很轻松的实现。

拿 list 的反向迭代器来举例,反向迭代器的 rbegin() 和 rend() 的位置是哪呢?

我们可以向正向迭代器一样,让 rbegin 指向最后一个节点,也就是在迭代器中维护 phead->prev ,然后rend 指向第一个节点的前一个节点,也就是 phead ,这样是一个标准的反向迭代器,但是这样却没有最大程度的复用正向迭代器。 让 rbegin 直接复用 正向迭代器的 end ,让rend 直接复用 begin ,但是这样设计有一个要注意的地方就是,解引用的时候 ,要返回的不是当前指针指向的节点的数据,而是当前节点的建一个节点的数据,因为我们相当于 将区间整体往右移了一个节点。 还有就是要实现 反向迭代器的 ++ ,我们就需要正向迭代器中重载了 - - ,要实现 - - ,则需要复用正向迭代器的 ++ 。 

那么反向迭代器的实现就很简单了,下面是一个针对 list 的反向迭代器的简单的实现,可能会有一些问题,但是大体的框架就是这样了。

template<typename T, typename ref, typename ptr>
	class reverse_list_iterator 
	{
	public:
		typedef _list_iterator<T> _list_iterator;

		//迭代器的构造
		reverse_list_iterator(_list_iterator it)
			:_it(it)
		{
		}
		reverse_list_iterator& operator++()
		{
			return --_it;
		}
		reverse_list_iterator& operator++(int) //后置++
		{
			return _it--;
		}
		bool operator!=(reverse_list_iterator& it1)const
		{
			return _it!=it1._it;
		}
		ref operator*()
		{
			list_iterator tmp=_it;
			--tmp;
			return *tmp;
		}
		const T& operator*()const
		{
			list_iterator tmp = _it;
			--tmp;
			return *tmp;
		}
		ptr operator->()
		{
			return _it.operator->();
		}

		listnode* pnode;


	private:
		_list_iterator _it;

	};

但是,其实反向迭代器不是针对某一个容器来设计的,他的重点在于复用,能够用不同类型的正向迭代器,来转换出相应的反向迭代器。

	template<typename Iterator>

但是这里面需要用到一些牛逼的技术来实现 从 正向迭代器中提取出 ref 和 ptr 用于 * 和 -> 的返回类型。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Floyd判圈算法——寻找重复数(C++)

287. 寻找重复数 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定一个包含 n 1 个整数的数组 nums &#xff0c;其数字都在 [1, n] 范围内&#xff08;包括 1 和 n&#xff09;&#xff0c;可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 &#xff0c;返…

python基础语法笔记(有C语言基础之后)

input()用于输入&#xff0c;其有返回值&#xff08;即用户输入的值&#xff09;&#xff0c;默认返回字符串。括号里可放提示语句 一行代码若想分为多行来写&#xff0c;需要在每一行的末尾加上“\” 单个“/”表示数学中的除法&#xff0c;不会取整。“//”才会向下取整。 …

无人机之飞行规划与管理篇

无人机飞行规划与管理是确保无人机安全、高效且符合法规的运行的关键步骤。这一过程包括了对飞行任务的详细安排、航线的设定以及风险的评估和管理。下面简述这一过程的主要环节&#xff1a; 一、飞行目的和任务确定 在规划之初&#xff0c;必须明确无人机的飞行目的&#xf…

HTTPS理解

一个完整的HTTP连接 TCP三次握手接受窗口发送数据关闭连接 接受窗口是用来做什么呢&#xff1f; 它根据自身网络情况设置不同大小的值用来控制对方发送速度&#xff0c;避免对方发送太快&#xff0c;导致网络拥塞。 为什么TCP握手要三次&#xff1f; 1&#xff09;确认双方的…

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…

JDK11中zgc垃圾回收器的探索

背景 垃圾回收器主要做的事情 自动跟踪和管理程序中创建的对象&#xff0c;确定哪些对象仍在使用&#xff0c;哪些对象已经不再使用。回收那些不再使用的对象所占用的内存空间&#xff0c;使得这部分内存可以被重新使用。 1.1 传统垃圾回收器 垃圾回收器简述优缺点应用场景…

typora 两边太宽,设置宽度

步骤&#xff1a; 查看目前使用主题类型 文件 —> 偏好设置 —> 外观 —> 打开主题文件夹 修改对应的主题&#xff1a;max-width

在Linux下使用Docker部署chirpstack

目录 一、前言 二、chirpstack 1、chirpstack是什么 2、chirpstack组件 3、为什么选择Docker部署 三、Linux下部署过程 四、web界面部署过程 一、前言 本篇文章我是在Linux下使用 Docker 进行部署chirpstack&#xff0c;chirpstack采用的是v4 版本&#xff0c;v4 版本 与…

实时数仓搭建

项目概述 本项目针对实时数仓中的dim层&#xff0c;使用flik获取维度数据以及维度表结构把处理过的数据和维度表同步到habse中&#xff0c;同步采用的是雪花模型&#xff0c;遵循三范式&#xff0c;对维度数据进行实时的增删改查。 对维度表进行动态拆分功能。 动态拆分功能…

centos安装数据库同步工具sqoop并导入数据,导出数据,添加定时任务

目录 1.安装jdk 1.1上传jdk安装包到/opt目录下并解压 1.2解压 1.3配置环境变量 2.安装hadoop 2.1.下载hadoop 2.2.解压hadoop 2.3配置环境变量 3.安装sqoop 3.1下载 3.2解压 3.3下载依赖包并复制到指定位置 3.3.1下载commons-lang-2.6-bin.tar.gz 3.3.2将mysql-c…

【postgresql初级使用】用户与角色的关系,搭建数据库安全体系中的分权管理

用户角色管理 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 文章目录 用户角色管…

Nature Renderer 2022(植被渲染工具插件)

渲染大量详细的植被。 自然渲染器通过替换Unity的默认地形细节和树系统来提高植被渲染的质量。一切都适用于现有数据:使用相同的草地、植被和树木,并保留现有地形。我们只是升级您的渲染器。 Unity验证的解决方案 Nature Renderer受到25000多名开发人员的信任,是Unity验证的…

基于Make的c工程No compilation commands found报错

由于安装gcc时只安装了build-essential&#xff0c;没有将其添加到环境变量中&#xff0c;因此打开Make工程时&#xff0c;CLion会产生如下错误&#xff1a; 要解决这个问题&#xff0c;一个方法是将GCC添加到环境变量中&#xff0c;但是这个方法需要修改至少两个配置文件&…

请编写函数,删除字符串中指定位置下的字符,删除成功函数返回被删字符,否则返回空值

char arr_del(char* p, int pos) {if (pos> strlen(p) || pos<0){printf("这是一个无效下标\n");exit(1);}//到这里就是有效下标char ch p[pos];//把要删除的下标存储for (int i pos; p[i] ! \0; i){p[i] p[i 1];}return ch; } int main() {char arr[100];…

PFC电路中MOS管的选取3

MOS管的驱动波形 一个 MOS管在开通或者关断的时候&#xff0c;必定会经历一个线性区。这个线性区域在 Vgs波形上表现出一个平台&#xff0c;在这个平台的时候电流和电压的变化率是很大的&#xff0c;有很大的 dv/dt&#xff0c;di/dt &#xff0c;由于 di/dt变化非常大&#xf…

Transformer模型解析:走进自然语言处理的新时代

UPDATED&#xff1a;2023 年 1 月 27 日&#xff0c;本文登上 ATA 头条。&#xff08;注&#xff1a;ATA 全称 Alibaba Technology Associate&#xff0c;是阿里集团最大的技术社区&#xff09;UPDATED&#xff1a;2023 年 2 月 2 日&#xff0c;本文在 ATA 获得鲁肃点赞。&…

使用lv虚拟卷扩展磁盘

使用centos演示。 首先创建centos虚拟机。链接&#xff1a;VMWARE安装Centos8,并且使用ssh连接虚拟机-CSDN博客 1. 增加磁盘。 选中要扩容的虚拟机&#xff0c;右键选择设置&#xff0c;然后点击磁盘&#xff0c;选择添加。 这里选择NVM的磁盘。选择这种磁盘是为了保持与之前…

【Java】零散知识--感觉每条都有知识在进入脑子唤起回忆

1&#xff0c;什么是双亲委派 AppClassLoader在加载类时&#xff0c;会向上委派&#xff0c;取查找缓存。 AppClassLoader >>ExtClassLoader >>BootStrapClassLoader 情况一 向上委派时查找到了&#xff0c;直接返回。 情况二 当委派到顶层之后&#xff0c;缓…

python网络爬虫之Urllib

概述 urllib的request模块提供了最基本的构造HTTP请求的方法&#xff0c;使用它可以方便地实现请求的发送并得到响应&#xff0c;同时它还带有处理授权验证&#xff08;authentication&#xff09;、重定向&#xff08;redirection&#xff09;、浏览器Cookies以及其他内容。 …

java算法day11

二叉树的递归遍历二叉树的非递归遍历写法层序遍历 递归怎么写&#xff1f; 按照三要素可以保证写出正确的递归算法&#xff1a; 1.确定递归函数的参数和返回值&#xff1a; 确定哪些参数是递归的过程中需要处理的&#xff0c;那么就在递归函数里加上这个参数&#xff0c; 并且…