list底层实现

news2024/11/19 22:35:00

目录

引言

结构形式

整体框架

插入删除接口实现

析构函数、拷贝构造、赋值重载

const迭代器实现

取得链表长度的方式

迭代器前置\后置operator++ -- 以及operator->


引言

list是非常常用的一个容器,数据结构是链表,数据空间是以链式结构存储的,物理空间上不一定是连续的,不像vector物理空间是连续的,可通过下标访问,在list中有可能排在后面的数据物理地址比在排在前面的数据小,所以它不方便通过下标访问,但它也具有它才有的特性,让我们来学习一下list的底层实现吧。

结构形式

库里实现的是带头双向循环链表,下面我们实现底层的时候也是实现该种链表。

每个节点有2个指针,一个next指向下一个节点,一个prev指向前一个节点,还有一个位置data存储对应数据。

整体框架

list 类中只需要一个成员变量,就是节点node的头指针_head,有_head就能指向其他节点。

而节点node需要将它作为一个自定义结构,里面的成员变量就是上面说的3个,_next,_prev和data

先将大框架构架出来:

namespace bc
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			,_data(x)
		{ }
	};

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		list()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	private:
		node* _head;
	};

为防止和库里的list混,将我们实现的封在namespace bc中。

list创建对象时需要实例化,list<int> , list<double> .......因此在类前给上模板参数T,

构建节点自定义类型,并在里面完成节点的默认构造初始化。

节点包装完成后是class list类的实现,成员变量是node* _head,实现无参构造函数,初始的时候就是节点的头尾指针都指向自己形成循环:

 

插入删除接口实现

 然后再实现一下push_back函数,插入数据也很简单,改变一下指针指向即可:

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

 

测试一下,因为没法用下标访问就想到通用的迭代器,先把测试用例写一下:

	void Test1() {
		list<int> st;
		st.push_back(1);
		st.push_back(1);
		st.push_back(1);
		st.push_back(1);
		list<int>::iterator it = st.begin();
		while (it != st.end()) {
			cout << (*it) << " ";
			++it;
		}
		cout << endl;
	}
}

然后实现迭代器,有了string和vector的经验,迭代器底层可用指针实现,那么list这里是不是也可以直接用指针指代迭代器呢?

————如果可以的话来看一下:typedef node* iterator;

用node* 指代迭代器是绝对不行的,之前说了链表的物理空间不是连续的,迭代器++ -- 的功能就是失效了,对此我们可以借鉴日期类(想比较两个日期或者++ -- 计算日期不可以,封装成类对操作符++ -- 重载就可以实现),这里也是一个道理,将迭代器封装层一个类__list_iterator就解决了

namespace bc
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			,_data(x)
		{ }
	};

	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

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

		__list_iterator<T>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		bool operator!=(const __list_iterator<T>& it) {
			return _pnode != it._pnode;
		}
	};


	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T> iterator;

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

		iterator end() {
			return iterator(_head);
		}

		list()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		void push_back(const T& x)
		{
			node* newnode = new node(x);
			node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

	private:
		node* _head;
	};

push_back还可以通过复用insert来实现,下面来实现一下insert

insert是在指定位置之前插入,位置pos还是迭代器类型。insert会导致迭代器失效吗?显然这里插入迭代器不会失效,因为空间没变还在那里,只是在pos位置链接上其他节点(vector那块会失效是因为挪动数据使得空间位置改变了,并且还有扩容的影响)

但库里的insert给了返回类型,我们保持一致也给一个iterator。

insert代码实现也很简单,

            iterator insert(iterator pos, const T& x) {
			node* newnode = new node(x);
			node* cur = pos._pnode;
			newnode->_prev = cur->_prev;
			newnode->_next = cur;
			cur->_prev->_next = newnode;
			cur->_prev = newnode;
            return iterator(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和insert一样,也是根据迭代器位置删除。它需要返回值,返回删除位置的下一个位置的迭代器。很明显删除操作会使迭代器失效,因为删除会直接删掉节点,空间没了迭代器自然失效。

代码如下:

		iterator erase(iterator pos) {
			assert(pos != end());
			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;
			return iterator(next);
		}

 同样,pop_back,pop_front 都可以复用 erase:

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

析构函数、拷贝构造、赋值重载

然后实现一下拷贝构造和析构函数,先写析构吧简单一点。

在析构之前可以先实现clear,析构函数再复用clear就好了。clear不清头结点,只删后面的节点,而析构函数会清除头结点,所以应该是析构调用clear,别搞反了。

clear()可以创建cur节点遍历删除,也可以复用刚刚实现的erase函数:

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

用迭代器从头开始遍历删除,注意这里迭代器不能++(失效),可以用erase返回值,返回的是删除位置的下一个位置的迭代器,这样相当于迭代器自动往下走了。

析构函数:

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

拷贝构造传统写法:

		list(list<T>& st) {
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;	
			for (auto& e : st) {
				push_back(e);
			}
		}

首先说一下,参数list<T>& st之前我没有加const,按理说可以加上毕竟st不改变,但是加了const会报错,因为范围for底层是迭代器,

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

&st是实参的别名,是迭代器对象,const list<int>& st 作为const对象只能调用const迭代器,而我们此时还没有实现const迭代器,所以先把const去掉保证编译通过。

函数内部原理就是拷贝之前要先创建对象,初始化一下,然后用范围for插入数据,相当于拷贝过去了。

注意auto&,这里不用&消耗很大。因为范围for是将*st 拷贝给e,也就是将容器里的数据拷贝给e,

如果list<T>的T是int等内置类型还好,拷贝消耗不大,如果T是string、map等具有大量数据的容器那么拷贝消耗会很大,所以用&可以减小拷贝带来的消耗。

我看了一下库里的实现,它将list初始化写成了一个函数empty_initialize,这样方便其他函数调用,我们也跟着保持一致吧:

		void empty_initialize() {
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
		list()
		{
			empty_initialize();
		}

		//st1(st)
		list(list<T>& st) {
			empty_initialize();
			for (auto& e : st) {
				push_back(e);
			}
		}

赋值重载传统写法:

		//st1 = st
		list<T>& operator=(list<T>& st) {
			if (this != &st);
			clear();
			for (auto& e : st) {
				push_back(e);
			}
			return *this;
		}

和拷贝构造异曲同工,const和&两个问题也是一样。

注意不要自己给自己赋值。

拷贝构造和赋值重载除了传统写法还有现代写法,在实现之前先写一个迭代器构造函数接口方便操作。

迭代器构造函数

		template <class InputIterator>
		list(InputIterator first, InputIterator last) {
			empty_initialize();
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

 这种构造函数就是通过迭代器区间来构造list<T>对象。

拷贝构造现代写法:

        void swap(list<T>& st) {
			std::swap(_head, st._head);
		}		

        list(list<T>& st)
		{
			empty_initialize();
			list<T>tmp(st.begin(), st.end());
			swap(tmp);
		}

现代写法就是通过上面写的迭代器构造函数构造临时list<T>对象tmp,再通过swap函数交换tmp和st 对象的头指针,也就是交换指向的空间和数据。

这里要注意两点:

1、swap函数我们要自己实现一下,算法库提供的swap函数要进行两次深拷贝代价太大,我们借用算法库中的swap自己实现一个容器中使用的可大大降低消耗。

2、一定要在拷贝构造前先构造初始化对象st,否则st对象的_head指针就是指向一段随机空间,swap交换tmp就指向了这段随机空间,在出函数调用析构函数清除临时对象tmp时就会报错,当然也不能给st._head初始化为空,链表为空析构的时候要求至少有哨兵位的头结点,否则也会报错,所以要先构造初始化。

赋值重载现代写法:

		list<T>& operator=(list<T> st) {
			swap(st);
			return *this;
		}

有了上面的基础, operator= 就很简单了。注意传参不能给引用,否则交换的时候会将st1 和 st 空间数据交换,那么外面st 就改变了,st 真是舍己为人了。

 

const迭代器实现

承接上面的问题,现在要实现一个const迭代器。有的人可能会说,const迭代器有什么难的,重载一下函数,在前面加一个const不就搞定了?——没那么简单,这样操作的不是所谓的const迭代器,因为const对象就不一样。

 这是const迭代器吗?————显然不是!这里const修饰的是cit 对象本身,而不是它指向的值。

const int* a1  和 int* const a2是有区别的:前者const修饰的是指针指向的内容,而不是其本身,也就是它指向的值不能改变,但是a1本身可以改变,对于迭代器来说也就是迭代器指向的值不能修改,但迭代器可以++; 后者相反,迭代器本身不能修改不能++,但能改指向的值。

显然前者符合const迭代器的预期,而这里的写法是属于后者,因此不是我们要的const迭代器。

那么就到迭代器类的内部修改它的重载函数*,使得能用const对象调用该函数,看看这种方法是否可行。

 重载一下解引用* 不同对象调用不同的重载函数,这样是解决了解引用访问的问题,但是++又如何呢?难道还能搞出个const ++?

于是想到既然重载函数不行,那么是否可以再构建一个类,一个const迭代器的类。我们不想着再从迭代器类中解决问题,而是换个角度再搞个类出来,一个const_iterator。

	template<class T>
	struct __list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_const_iterator(node* p)
			:_pnode(p)
		{ }
		const T& operator*(){
			return _pnode->_data;
		}
		__list_const_iterator<T>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		__list_const_iterator<T>& operator--() {
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_const_iterator<T>& it) {
			return _pnode != it._pnode;
		}
	};

整个类相对于iterator类也就是        const T& operator*()的区别,这样就可以通过不同类访问const迭代器和非const迭代器了。但要注意这是两个不同的类,绝不是相近类的概念,就像list<int>和list<double>是两个完全不同的类型一样。

然后在class list中重定义一下这个const类,增加const begin()和const end()

		typedef __list_const_iterator<T> const_iterator;
		const const_iterator begin()const {
			return const_iterator(_head->_next);
		}

		const const_iterator end()const {
			return const_iterator(_head);
		}

测试一下:

void Print(const list<int>& st) {
		list<int>::const_iterator cit = st.begin();
		while (cit != st.end()) {
			cout << *cit << " ";
			++cit;
		}
	}
	void Test2() {
		list<int> st;
		st.push_back(1);
		st.push_back(2);
		Print(st);
	}

const迭代器对象只能读不能写。

但是这样写有一个缺陷,就是代码冗余,创建 __list_const_iterator类确实能解决,可只有一行不一样显得太过多余,对此库里的实现是用模板解决冗余问题。

	template<class T,class Ref>
	struct __list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}
		__list_iterator<T,Ref>& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		__list_iterator<T, Ref >& operator--() {
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const __list_iterator<T, Ref>& it) {
			return _pnode != it._pnode;
		}
	};



	template<class T>
	class list
	{
	public:
		typedef list_node<T> node;
		typedef __list_iterator<T,T&> iterator;
		typedef __list_iterator<T, const T&> const_iterator;

既然两个类的区别只在operator*函数的返回值,那么就再给iterator类一个模板参数Ref,去标识这个返回值是T&还是const T&。 在class list类中重定义一下,模板参数是<T,T&>就对应非const迭代器,是<T,const T&>就对应const迭代器,这样就解决了代码冗余的问题。

非const迭代器实现好了,拷贝构造和operator = 函数就可以调用非const迭代器了。

PS:因为下面还要加一个模板参数,所以为了书写方便,在这里typedef一下__list_iterator<T,Ref>

	template<class T,class Ref>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref> Self;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}
		Self& operator++() {
			_pnode = _pnode->_next;
			return *this;
		}

		Self& operator--() {
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const Self& it) {
			return _pnode != it._pnode;
		}
	};

取得链表长度的方式

首先想到的是实现一个size接口,遍历链表得到长度,但是这样的接口开销很大,试想每一次得到长度都要遍历一次链表肯定不好。

我们可以采取空间换时间的思想,再加一个成员变量_size。

class list
{
public:
    size_t size(){
        return _size;
    }    
private:
    size_t _size;

那么在构造、增删等函数中都要再修改一下_size,尤其是swap,不然拷贝构造和赋值的对象调用size就不对了。

		void swap(list<T>& st) {
			std::swap(_head, st._head);
			std::swap(_size, st._size);
		}

 

迭代器前置\后置operator++ -- 以及operator->

		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;
		}

前置与后置对于内置类型没有什么区别,但对于自定义类型还是有很大差异的,从代码角度就可以看出后置要多两次拷贝,所以还是尽量使用前置。(为区别前置与后置++ --,规定参数部分使用int区分 )

operator->的使用方式很怪,它返回的是节点数据的地址:

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

通过一个场景来理解一下:

	struct Pos
	{
		int _row;
		int _col;
		Pos(int row = 0, int col = 0)
			:_row(row)
			, _col(col)
		{}
	};
	void Test3() {
		list<Pos>st;
		st.push_back(Pos(1, 1));
		st.push_back(Pos(2, 2));
		list<Pos>::iterator it = st.begin();
		while (it != st.end()) {
			cout << it->_row <<":"<< it->_col << endl;
			++it;
		}
	}

 list的实例化模板给了一个自定义类型Pos,通过迭代器访问打印,it->_row调用了重载函数operator->   这里相当于是it.operator->(),返回值是数据的地址T*,也就是Pos*。  不对呀,Pos*怎么能打印出数据呢?————其实这里是调用了两次->  只是没有显示写出来,其实是 it->->_row,因为Pos*->_row就可以打印出数据了,两次->在使用的角度不方便理解(运算符重载的意义就是方便使用理解),所以编译器处理成了一个->的形式。


 再看如下一段代码:

	void Print(const list<Pos>& st) {
		list<Pos>::const_iterator cit = st.begin();
		while (cit != st.end()) {
			cout << cit->_row++;
			cout << cit->_row << ":" << cit->_col << endl;
			++cit;
		}
	}
	void Test3() {
		list<Pos>st;
		st.push_back(Pos(1, 1));
		st.push_back(Pos(2, 2));
		list<Pos>::iterator it = st.begin();
		while (it != st.end()) {
			cout << it->_row <<":"<< it->_col << endl;
			++it;
		}
		Print(st);
	}

在Print函数中,for循环const迭代器指向内容居然能++,cit->_row++并且编译器编译通过了,不是说const迭代器指向内容不能修改吗?

————因为operator-> 返回值都是T*,T*->可以修改指向内容。这里不论是const对象还是非const返回的都是T*,要区分const与非const对象还是用到之前的方法——模板。

	template<class T, class Ref,class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		node* _pnode;
		__list_iterator(node* p)
			:_pnode(p)
		{ }

		Ref& operator*() {
			return _pnode->_data;
		}

		Ptr operator->() {
			return &_pnode->_data;
		}

    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;

上面说的第3个模板参数就是用在这里,实际上和第2个本质上是一样的。

以上就是list的底层实现,因为本人浅薄的理解可能讲的不到位,仅仅只是对自己学习的一个总结,感谢浏览!

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

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

相关文章

QT中控件不满足要求那么为你的控件做“提升”吧

我们在VisualStudio中例如VC或者C#中写一些个性化比较强的项目都会对控件有更高的要求&#xff0c;那么当我们的要求得不到满足时就需要考虑用到自定义控件了&#xff0c;其中自定义控件有两种方式&#xff0c;一种是完全重写&#xff0c;另一种简单的方法是继承自现有的控件。…

指针深度解剖,从内存详细了解指针

目录 &#x1f695;第一组&#xff1a;一维数组 &#x1f68c;第二组&#xff1a;字符数组 &#x1f690;字符数组1.0 &#x1f68e;字符数组1.1 &#x1f691;字符数组2 &#x1f69a;字符数组3 &#x1f69c;第三组&#xff1a;二维数组 &#x1f697;在上次的博客之中我们…

INVS里的DanglingWire的自动化清理实战

在上一讲中一起了解了INVS里的DanglingWire&#xff08;innovus中的DanglingWire&#xff08;悬垂线&#xff09;的理解和处理&#xff09;&#xff0c;常言道理论联系实际&#xff0c;在理解原理的基础上&#xff0c;那一定是要服务项目的&#xff0c;这篇后续火速跟进&#x…

中国第一个 Apache 顶级开源项目的突围之路!

【CSDN 编者按】近些年开源大热&#xff0c;开源创业之风随之四起。作为近两年特别火的商业方向&#xff0c;开源创业者如何才能在一片红海中乘风破浪&#xff0c;在行业中占据一席之地&#xff1f;第一个由中国人主导贡献到 ASF 的顶级开源项目 Apache Kylin 做出了有效探索&a…

字节序,主机字节序与网络字节序

&#x1f4cc;————本章重点————&#x1f4cc; &#x1f517;了解字节序的概念; &#x1f517;了解大小端的概念;&#x1f517;了解主机字节序和网络字节序的区别;&#x1f517;学习主机字节序和网络字节序相机转化的接口; ✨————————————✨字节序 概念&am…

人工智能助力古彝文识别,推动传统文化传承

人工智能助力古彝文识别&#xff0c;推动传统文化传承0. 前言1. 古彝文1.1 古彝文介绍1.2 古彝文识别的重要意义1.3 古彝文识别的挑战2. 古彝文识别国内外研究进展3. 基于深度学习的古彝文识别3.1 深度学习简介3.2 基于深度学习的古彝文识别模型架构4. 古彝文识别进展与展望小结…

HTML小游戏19 —— html5版开心斗地主小游戏(附完整源码)

&#x1f482; 网站推荐:【神级源码资源网】【摸鱼小游戏】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】本节教程我会带大家使用 HTML…

【练习】Day05

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、选择二、编程1. 跳跃游戏[贪心算法]2. 寻找重复数[注意思路&#xff01;]答案1. 选择2. 编程普通小孩也要热爱生活&#xff01; 一、选择 HASH 函数冲突处理方式不包括以下哪一项&#xff1a;&#xff08;&…

YOLOv5源码详解——项目目录

YOLOv5 目录 .github .github相关&#xff0c;不重要 datasets 存放数据集,刚下载下来的源码是不包含这个文件夹的 classify 图像分类模块&#xff0c;包含以下4个文件 predict.py —— 图像分类预测脚本train.py ——图像分类训练脚本val.py —— 图像分类验证脚本tutorial.…

Java开发学习(四十一)----MyBatisPlus标准数据层(增删查改分页)开发

一、标准CRUD使用 对于标准的CRUD功能都有哪些以及MyBatisPlus都提供了哪些方法可以使用呢? 我们先来看张图: 1.1 环境准备 这里用的环境就是Java开发学习(四十)----MyBatisPlus入门案例与简介中使用的环境 二、新增 在进行新增之前&#xff0c;我们可以分析下新增的方法…

马蹄集 公式计算

公式计算 难度&#xff1a;青铜 01 时间限制&#xff1a;1秒巴占用内存&#xff1a;64M 计算公式 (1/2)*(a*xc(ac)/(4*a)》 格式 输入格式&#xff1a;输入为整型x,a,空格分隔 #include <bits/stdc.h> using namespace std; int main() {int x, a;cin >> x >&g…

1、JDK17安装

目录 一、简介 二、安装步骤 三、在Windows 10系统中配置环境变量 四、运行jdk 一、简介 JDK全称Java SE Development kit(JDK)&#xff0c;即java标准版开发包&#xff0c;是Oracle提供的一套用于开发java应用程序的开发包&#xff0c;它提供编译&#xff0c;运行java程…

二叉树【数据结构】【超详细,一学就会!!!】

目录 &#x1f4d6;1.什么是二叉树&#xff1f; &#x1f334;2.满二叉树和完全二叉树 ⛳2.二叉树的性质 &#x1f525;3.二叉树的创建与遍历 3.1 创建二叉树 3.2 前中后序遍历——递归和非递归 &#x1f3f9;4.二叉树的实现 1️⃣获取树中节点的个数 2️⃣获取叶子节点…

MATLAB-三维插值运算

MATLAB中是支持三维及三维以上的高维插值的。三维插值的基本原理与一维插值和二维插值是一样的&#xff0c;但三维插值是对三维函数进行的插值。在MATLAB中&#xff0c;使用interp3函数实现插值&#xff0c;其调用格式如下。vi interp3(x,y,z,v,xi,yi,zi) %返回值 vi是三维插值…

2022ICPC杭州站

A. Modulo Ruins the Legend 题目链接&#xff1a;Problem - A - Codeforces 样例1输入&#xff1a; 6 24 1 1 4 5 1 4样例1输出&#xff1a; 1 0 5样例2输入&#xff1a; 7 29 1 9 1 9 8 1 0样例2输出&#xff1a; 0 0 0题意&#xff1a;给你一个长度为n的数组a[]&#x…

【NI Multisim 14.0原理图的设计——原理图分类】

目录 序言 一、原理图的设计 &#x1f34a;知识点&#xff1a; 二、原理图分类 &#x1f349;1.平坦式电路 &#x1f349; 2.层次式电路图 序言 NI Multisim最突出的特点之一就是用户界面友好。它可以使电路设计者方便、快捷地使用虚拟元器件和仪器、仪表进行电路设计和…

【用JS自制表格软件玩数据】10. 为表格脚本设计一个语法解析器

设计脚本语言的语法解析器概述脚本源码语法预览运行效果如下图设计计算符号的优先级定义一些关键词生成一份关键词的map方便引用枚举关键词的类型错误异常的捕获字符匹配代码的字符转化成迭代器关键词标记器词法分析器设计一个队列处理器源代码字符串迭代器代码的块级运行环境脚…

【HTML基础篇003】前端基础之CSS选择器大全

✨一、CSS的基本介绍 CSS&#xff08;Cascading Style Sheet&#xff0c;层叠样式表)定义如何显示HTML元素。 当浏览器读到一个样式表&#xff0c;它就会按照这个样式表来对文档进行格式化&#xff08;渲染&#xff09;。 ✨二、CSS的几种引入方式 &#x1f338;2.1、方法一&am…

连接查询之内连接(等值连接、非等值连接和自连接)

1、等值连接&#xff1a;表连接条件是等值关系&#xff0c;我们称为等值连接。 需求&#xff1a;查询每个员工所在部门名称&#xff0c;显示员工名和部门名&#xff1a; 查看员工表的ename和deptno字段信息&#xff1a; 查看部门表中的deptno和dname字段信息 SQL92语法&#x…

2023年最值得关注的机器人趋势TOP10

新兴的机器人技术趋势和预测满足了对工业自动化、数字化和可持续性的需求。仓库中的材料处理由自动移动机器人&#xff08;AMR&#xff09;和自动引导车辆&#xff08;AGV&#xff09;实现自动化。相关机构对8949家全球初创企业和2023年机器人趋势规模的样本进行了研究&#xf…