【C++】list的使用及底层实现原理

news2024/11/25 16:55:42

 

  本篇文章对list的使用进行了举例讲解。同时也对底层实现进行了讲解。底层的实现关键在于迭代器的实现。希望本篇文章会对你有所帮助。

文章目录

一、list的使用

1、1 list的介绍

1、2 list的使用

1、2、1 list的常规使用 

1、2、2 list的sort讲解

二、list的底层实现

2、1 初构list底层模型

2、2 迭代器的实现

2、2、1 普通迭代器

2、2、2 const 迭代器

2、3 完善其他底层实现

三、总结


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++  👀

💥 标题:list讲解💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️  

一、list的使用

1、1 list的介绍

  C++中的标准模板库(STL)提供了许多容器类来处理不同类型的数据,其中之一就是list。list是一个双向链表容器,它可以在其内部存储各种类型的元素,并且支持动态地添加、删除和修改元素

  以下是list的几个主要特点:

  1.  list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)。

  list的特点主要是基于list底层实现是双向带头循环链表

1、2 list的使用

1、2、1 list的常规使用 

  使用list容器时,可以使用其公共接口提供的各种方法来执行常见的操作,如插入、删除、访问等。常用的成员函数包括:

  • push_back(element):将element添加到列表的末尾。
  • push_front(element):将element添加到列表的开始位置。
  • pop_back():从列表的末尾删除元素。
  • pop_front():从列表的开始位置删除元素。
  • insert(position, element):在指定位置插入element。
  • erase(position):删除指定位置上的元素。
  • size():返回列表中的元素数量。
  • empty():检查列表是否为空。
  • clear():清空列表中的所有元素。
  • sort():对列表元素排序。

  除了以上常用函数外,list还提供了许多其他功能,如合并、翻转等。下面我们结合实际例子来理解以下上述的list的使用。

  具体代码如下:

void test_list()
{
	// 创建一个列表
	list<int> numbers;

	// 添加元素到列表末尾
	numbers.push_back(1);
	numbers.push_back(2);
	numbers.push_back(3);
	numbers.push_back(4);
	numbers.push_back(5);

	//打印链表中的元素
	list<int>::iterator it = numbers.begin();
	while (it != numbers.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//添加元素到列表头部
	numbers.push_front(10);
	numbers.push_front(20);
	numbers.push_front(30);
	numbers.push_front(40);
	
	//删除链表尾部元素
	numbers.pop_back();

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

	// 插入元素到指定位置
	auto pos = find(numbers.begin(), numbers.end(), 3);
	if (pos != numbers.end())
	{
		// pos是否会失效?不会
		numbers.insert(pos, 30); 
	}

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

	//删除指定位置元素
	pos = find(numbers.begin(), numbers.end(), 4);
	if (pos != numbers.end())
	{
		// pos是否会失效?会 删除后pos位置直接会被释放
		numbers.erase(pos);
		// cout << *pos << endl;
	}

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

	// 对列表进行升序排序
	numbers.sort();

	// 输出列表元素
	for (int number : numbers) {
		cout << number << " ";
	}
	cout << endl;

}
int main()
{
	test_list();
	return 0;
}

  我们再看一下实际的运行结果:

  如上结果可以对应着代码一起看,对list的使用理解会容易一点。 

1、2、2 list的sort讲解

  list容器内部自己提供了sort函数。为什么呢?我们知道,STL中有一个重要组件就是算法,其中提供了sort函数,那这两者又有什么区别呢?

  首先STL中的算法库中提供的sort函数底层使用的是快排。list中的迭代器并不支持随机访问迭代器,所以不能使用STL算法库中提供的sort。list容器中的sort函数底层使用的是归并排序。两者是有所区别的。

  我们先来测试一下两者的效率,看看是否属于一个级别的。测试代码如下:

void test_op()
{
	srand(time(0));
	const int N = 100000;
	vector<int> v;
	v.reserve(N);

	list<int> lt1;
	list<int> lt2;

	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		//v.push_back(e);
		lt1.push_back(e);
		lt2.push_back(e);
	}

	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	for (auto e : lt1)
	{
		v.push_back(e);
	}
	sort(v.begin(), v.end());
	size_t i = 0;
	for (auto& e : lt1)
	{
		e = v[i++];
	}
	int end1 = clock();

	int begin2 = clock();
	// sort(lt.begin(), lt.end());
	lt2.sort();
	int end2 = clock();

	printf("copy vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}
int main()
{
	test_op();
	return 0;
}

  针对上述代码,讲解一下比较的思路。首先我们创建两个链表。随机生成一百万个元素加入到链表中。一个链表用于把元素拷贝到vector排序,然后调用STL算法库中的sort函数,排完以后再拷贝回来,计算此过程的时间。另一个链表直接调用自己容器内的sort进行排序,计算排序时间。我们看一下时间对比:

  我们发现,在debug版本下调试,差距基本上不大。但是在release版本下调试,还是有一定差距的。发现在release版本下,拷贝到vector中排序更快。数据量越大,效果会越明显。所以,当数据量大的时候,我们尽量去使用拷贝到vector中,调用STL算法库中的sort函数去排序。

二、list的底层实现

2、1 初构list底层模型

  我们知道list底层是带头双向循环列表后,我们可大概构建一个模型。如下:

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

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

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

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

			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}
	private:
		Node* _head;
	};

  上述代码中,也实现了尾插。尾插来说相对简单,这里就不再做过解释。关键是list的迭代器的底层实现。

2、2 迭代器的实现

2、2、1 普通迭代器

  我们先想一下vector和string的迭代器。 vector和string的迭代器底层无非就是指针,该指针指向元素内容。支持++、--、解引用等操作。

  list的迭代器底层能直接是Node* 指针吗?假如是Node* 指针,我们 ++ 和 -- 操作,是不能够找到下一个元素的。因为链表本身每个节点的地址不是连续的。其次,我们解引用操作并不是找到节点所存储的值,而是找到的是该节点。

  综上主要原因,为了解决上述情况,C++ list 的底层迭代器采用了把 Node* 封装成了一个类。在该类中重载运算符,以达到我们想要的效果。我们可结合如下代码理解一下:

    template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;
        
        // 构造函数
		__list_iterator(Node* node)
			:_node(node)
		{}

        // 解引用操作,返回节点所存储的值
		T& operator*()
		{
			return _node->_val;
		}

        // 前置++
        __list_iterator<T>& operator++()
		{
			_node = _node->_next;
			return *this;
		}

        // 后置++ 返回 ++前的结果
		__list_iterator<T> operator++(int)
		{
			__list_iterator<T> tmp(*this);

			_node = _node->_next;

			return tmp;
		}
        
        // 比较的节点的地址是够相同
		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}

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

  我们再把封装后的迭代器类融入到 list 类中。为什么还要融入到 list 类中呢?融入到 list 中又该怎么使用呢?我们先看如下代码:

        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();

  我们定义 list 迭代器时,是根据 list 对象 lt,通过调用该类的成员函数 begin()或者end()来创建的。那我们自己实现时,在list中提供这两个成员函数即可。代码如下:

    template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _val(val)
		{}
	};

	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{}

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

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

		__list_iterator<T> operator++(int)
		{
			__list_iterator<T> tmp(*this);

			_node = _node->_next;

			return tmp;
		}

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

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


	template<class T>
	class list
	{
		typedef list_node<T> Node;

	public:
		typedef __list_iterator<T> iterator;


		iterator begin()
		{
            //单参数的构造函数支持隐式类型转换
            //  Node* 会进行隐式类型转换,中间会生成一个匿名对象作为临时变量
			//return _head->_next;
         
			return iterator(_head->_next);
		}

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

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


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

			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;
		}

	private:
		Node* _head;
	};

2、2、2 const 迭代器

  我们实现的普通的迭代器,那const 修饰的迭代器,不就是在普通的迭代器上加上一个指针就可以了。代码如下:

// const 迭代器
const __list_iterator<T> 
typedef const __list_iterator<T> const_iterator;

  注意:上述的 const 修饰的是这个类型,这个类型是自定义类型,并不是我们之前学的指针。我们也可结合如下代码理解:

//const __list_iterator<T> 
//typedef const __list_iterator<T> const_iterator;

	const list<int> numbers;

	// 添加元素到列表末尾
	numbers.push_back(1);
	numbers.push_back(2);
	numbers.push_back(3);
	numbers.push_back(4);
	numbers.push_back(5);

    list<int>::const_iterator cit=numbers.begin();
    // const int a = 10;

  上述的 const 是修饰的迭代器本身,并不是迭代器指向的内容。这样修饰只是迭代器本身不能修改。而我们想要是迭代器指向的内容不能被修改。也就是解引用返回的值不能被修改。当然,我们可以再封装一个const迭代器类,代码如下:

	template<class T>
	struct __list_const_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

		__list_const_iterator(Node* node)
			:_node(node)
		{}

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

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

		__list_const_iterator<T> operator++(int)
		{
			__list_const_iterator<T> tmp(*this);

			_node = _node->_next;

			return tmp;
		}

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

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

  这样我们发现会不会设计的代码优点冗余了。C++ STL中,list底层实现也并非如此,而是通过增加模板参数选择复用代码。如下图:

  我们可以通过增加模板参数(第三个模板参数我们后面也会讲到),来控制解引用返回的是否是用const修饰的引用。代码如下:

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> iterator;

		Node* _node;

		// 休息到17:02继续
		__list_iterator(Node* node)
			:_node(node)
		{}

		bool operator!=(const iterator& it) const
		{
			return _node != it._node;
		}

		bool operator==(const iterator& it) const
		{
			return _node == it._node;
		}

		// *it  it.operator*()
		// const T& operator*()
		// T& operator*()
		Ref operator*()
		{
			return _node->_data;
		}


		// ++it
		iterator& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		
		// it++
		iterator operator++(int)
		{
			iterator tmp(*this);
			_node = _node->_next;
			return tmp;
		}

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

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

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

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

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

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

		void push_back(const T& x)
		{
			//Node* tail = _head->_prev;
			//Node* newnode = new Node(x);

			 _head          tail  newnode
			//tail->_next = newnode;
			//newnode->_prev = tail;
			//newnode->_next = _head;
			//_head->_prev = newnode;

			insert(end(), x);
		}

	private:
		Node* _head;
	};

  我们在 list 类中添加 const 迭代器的返回即可。当我们定义const对象时,会自动调用const修饰的迭代器。当调用const修饰的迭代器时,__list_iterator的模板参数就会实例化为const T&。实际上在实例化时,const和非const修饰的还是两个不同类,只不过是实例化的代码工作交给了编译器处理了。

  以上就是迭代器的底层实现。list底层中,关键重点就是迭代器的实现。其他部分相对来说简单。接下来我们完善剩余的底层实现部分。 

2、3 完善其他底层实现

  我们上述代码中有尾插,再把常用接口和拷贝构造、赋值重载实现即可。代码如下:

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

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

			Node* newnode = new Node(x);

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;
			delete cur;

			return iterator(next);
		}

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

		void pop_front()
		{
			erase(begin());
		}

        void empty_init()
		{
			// 创建并初始化哨兵位头结点
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		template <class InputIterator>  
		list(InputIterator first, InputIterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		list()
		{
			empty_init();
		}

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

		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_init();
			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		// lt1 = lt3
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

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

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

  上述代码,把初始化的代码再次封装了一个函数。list初始化构造也可用一段区间来初始化,这个区间可以是vector迭代器区间,或者数组的指针区间等等。其他实现均较为简单。

三、总结

  我们之前是由顺序表和链表的对比,现在我们再对比一下vector和list的优缺点。如下:

vectorlist
动态顺序表,一段连续空间
带头结点的双向循环链表
访
支持随机访问,访问某个元素效率O(1)
不支持随机访问,访问某个元素效率O(N)
任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低
任意位置插入和删除效率高,不需要搬移元素,时间复杂度为
O(1)
底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高
底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
原生态指针
对原生态指针(节点指针)进行封装
在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效
插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使
需要高效存储,支持随机访问,不关心插入删除效率
大量插入和删除操作,不关心随机访问

  list底层实现关键是迭代器的实现。我们要清楚的是, list的迭代器底层就是一个对节点进行封装的类。初学可能会感觉有点难以理解。但是,应多看几遍理解整体结构就很容易清楚了。本篇文章的讲解就到这里,感谢观看ovo~

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

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

相关文章

vue文件上传,文件打不开。文件上传 on-progress不触发

//文件上传带进度条<el-uploadv-model:file-list"fileList":limit"3":on-progress"beforeAvatarUpload"//on-progress 不触发&#xff0c;触发的关键在于覆盖原有的http请求:http-request"getFile":action"${app.api}/student…

【从零开始学习JAVA | 第二十九篇】Stream流

目录 前言&#xff1a; Stram流&#xff1a; 设计目标&#xff1a; 使用步骤&#xff1a; 1.先得到一条Stream流&#xff0c;并把数据放上去。 2.利用Stream流中的各种API进行操作。 使用Stream流的注意事项&#xff1a; 总结: 前言&#xff1a; 本文我们将学习Stream流…

Windows用户注意!LokiBot恶意软件正通过Office文档传播

Windows用户再次成为被称为LokiBot恶意软件的攻击目标&#xff0c;该恶意软件通过Office文档进行传播。 根据Fortinet安全研究员Cara Lin的最新报告&#xff0c;攻击者利用已知漏洞&#xff0c;如CVE-2021-40444和CVE-2022-30190&#xff0c;在微软Office文档中嵌入恶意宏。 …

Eulaceura 版本 23H1:RISC-V 架构新的里程碑

近日&#xff0c;RISC-V SIG 发布了 Eulaceura 版本 23H1&#xff0c;这是发行版的一次重大更新&#xff0c;新版本搭建了 linux kernel 6.1 版本内核&#xff0c;适用于矽速科技 LicheePi 4A 的映像也已公布。Eulaceura 是首个将 RISC-V 架构纳入第一级支持范围的 openEuler 发…

深入理解网络栈

网络路径 发送端 应用层 1、socket 各种网络应用程序基本上都是通过 Linux Socket 编程接口来和内核空间的网络协议栈通信的 socket 是网络编程的入口&#xff0c;它提供了大量的系统调用&#xff0c;构成了网络程序的主体 udp UDP 是面向无连接的协议&#xff0c;不需要与…

CycleGAN论文的阅读与翻译,无监督风格迁移、对抗损失

CycleGAN论文的阅读与翻译&#xff0c;无监督风格迁移 CycleGAN 论文原文 arXiv CycleGAN junyanz&#xff0c;作者自己用 lua 在 GitHub 上的实现 CycleGAN tensorflow PyTorch by LynnHo&#xff0c;一个简单的 TensorFlow 实现 0. 摘要&#xff1a; 图像到图像的翻译 (Ima…

黑马大数据学习笔记2-HDFS基本操作

目录 进程启停管理一键启停脚本单进程启停 文件系统操作命令HDFS文件系统基本信息介绍创建文件夹查看指定目录下内容上传文件到HDFS指定目录查看HDFS文件内容下载HDFS文件拷贝HDFS文件追加数据到HDFS文件HDFS数据移动HDFS数据删除其他命令HDFS WEB浏览 HDFS权限修改权限 https:…

如何设计一个注册中心?(2)实现注册接口

1. 创建SpringBoot工程 创建父工程及三个子模块&#xff0c;其中一个模块作为注册中心&#xff0c;另外两个作为服务提供者。 pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns…

OpenCV for Python 入坑第三天 :图片处理(2)

上一篇博客我们了解了图像在OpenCV中的保存方式。并且我们自己上手创建了一张灰度图像和一张彩色图像。除此之外&#xff0c;我们还了解到了彩色图像通道在OpenCV中和我们日常所了解的不一样&#xff0c;是通过BGR的顺序进行编码的。咱们一定要记清楚哦~ 那么今天&#xff0c;我…

tinymce编辑器导入docx、doc格式Word文档完整版

看此文章之前需要注意一点 在前端使用导入Word文档并自动解析成html再插入到tinymce编辑器中&#xff0c;在这里我使用的是mammoth.js识别Word内容&#xff0c;并set到编辑器中&#xff0c;使用mammoth只可解析.docx格式的Word&#xff0c;目前的mammoth不支持.doc格式&#x…

Ghost Buster Pro for mac(快速清理卸载的应用残存文件)

Ghost Buster Pro for mac可从您已卸载的应用程序中查找并删除文件。该应用程序速度快如闪电&#xff0c;可立即释放内存。 许多应用程序都安装在计算机上&#xff0c;但它们通常只会在您的计算机上停留很短的时间。每个应用程序都会创建文件&#xff0c;但删除应用程序不会删…

01 - 如何制定性能调优标准?

1、为什么要做性能调优&#xff1f; 一款线上产品如果没有经过性能测试&#xff0c;那它就好比是一颗定时炸弹&#xff0c;你不知道它什么时候会出现问题&#xff0c;你也不清楚它能承受的极限在哪儿。 有些性能问题是时间累积慢慢产生的&#xff0c;到了一定时间自然就爆炸了…

使用dhtmlx-gantt甘特图插件定制预约表【实战】

示例传送门 定制预约表展示 一、安装 npm i dhtmlx-gantt二、配置解释与汇总 //自定义表头列 gantt.config.columns = [{name: "

服务器部署 Python 项目总结

title: 服务器部署 Python 项目总结 date: 2023-07-05 16:33:49 tags: 服务器Python categories:服务器 cover: https://cover.png feature: false 1. 准备 Python 项目需要 Python 的环境&#xff0c;假如服务器操作系统为 CentOS 7 的话&#xff0c;默认安装了 Python2 与 …

安达发|APS高级排程系统界的天花板!

APS 系统不仅为生产部门提供制造依据&#xff0c;而且涉及到采购计划、安全库存、订单交付等方面。这是非常复杂的管理。一个易于使用的 APS 系统不仅能够充分整合生产相关业务流程&#xff0c;保证生产计划的顺利进行&#xff0c;而且能够大大提高生产效率&#xff0c;降低管理…

导出python环境的所有安装包

导出操作 pip freeze > requests.txt批量导入操作 pip install -r requests.txt

redis数据类型基本操作(list,string,hash,keys相关操作),mongodb(增删改查)

1、 string类型数据的命令操作&#xff1a; &#xff08;1&#xff09; 设置键值&#xff1a; &#xff08;2&#xff09; 读取键值&#xff1a; &#xff08;3&#xff09; 数值类型自增1&#xff1a; &#xff08;4&#xff09; 数值类型自减1&#xff1a; &#xff08;5…

Java面试突击

Java面向对象有哪些特征&#xff0c;如何应用 ​ 面向对象编程是利用类和对象编程的一种思想。万物可归类&#xff0c;类是对于世界事物的高度抽象 &#xff0c;不同的事物之间有不同的关系 &#xff0c;一个类自身与外界的封装关系&#xff0c;一个父类和子类的继承关系&…

三菱PLC 控制灯一秒钟交替闪烁

三菱PLC中常用的特殊继电器&#xff1a; M8000 上电一直ON标志 M8002 上电导通一次 M8004 PLC出错 M8005 PLC备用电池电量低标志 M8011 10ms时钟脉冲 M8012 100ms时钟脉冲 M8013 1s时钟脉冲 M8014 1min时钟脉冲 M8034…

【论文笔记】图像修复MPRNet:Multi-Stage Progressive Image Restoration 含代码解析

目录 一、介绍 二、使用方法 1.推理 2.训练 三、MPRNet结构 1.整体结构 2.CAB(Channel Attention Block) 3.Stage1 Encoder 4.Stage2 Encoder 5.Decoder 6.SAM(Supervised Attention Module) 7.ORSNet(Original Resolution Subnetwork) 四、损失函数 1.Charbonni…