【C++】stack & queue

1. Container Adapter

Adapter is a design pattern (a design pattern is a set of code design experiences that are repeatedly used, known to most people, classified and cataloged). This pattern converts the interface of a class into another one that we want. interface.

Although elements can also be stored in stacks and queues , they are not classified into containers in STL . Instead, they are called container adapters . This is because stacks and queues just wrap the interfaces of other containers. STL In stack and queue , deque is used by default (described later), for example:

Insert image description here

Insert image description here

In fact, container adapters reuse other containers and use the functions of other containers to adapt a new container.

2. deque (understand)

deque (double-ended queue): It is a double-opening "continuous" space data structure. The meaning of double-opening is that insertion and deletion operations can be performed at both ends, and the time complexity is O(1), which is the same as Compared with vector , header insertion is more efficient and does not require moving elements; compared with list , space utilization is relatively high. If you need efficient random access and a large number of insertions and deletions, it is recommended to use deque.

Deque is not a truly continuous space, but is made up of continuous small spaces. The actual deque is similar to a dynamic two-dimensional array. Its underlying structure is as shown in the figure below:

Insert image description here

Compared with vector , the advantage of deque is: when inserting and deleting the head, there is no need to move elements, which is very efficient. Moreover, when expanding, there is no need to move a large number of elements, so its efficiency is higher than vector ;

Compared with list , its bottom layer is continuous space, the space utilization rate is relatively high, and there is no need to store additional fields;

However, deque has a fatal flaw: it is not suitable for traversal, because during traversal, the iterator of deque needs to frequently detect whether it has moved to the boundary of a certain small space, resulting in low efficiency. In sequential scenarios, it may be necessary to frequently Traversal, so in practice, when a linear structure is needed, vector and list are given priority in most cases. Deque does not have many applications, and one application that can be seen so far is that STL uses it as the underlying data structure of stack and queue . .

So why choose deque as the underlying default container for stack and queue ?

Stack is a special linear data structure with last-in-first-out . Therefore, any linear structure with push_back() and pop_back() operations can be used as the underlying container of stack , such as vector and list ;

Queue is a special linear data structure that is first in, first out . As long as the linear structure has push_back and pop_front operations, it can be used as the underlying container of queue , such as list . However, deque is selected as the underlying container by default for stack and queue in STL , mainly because:

  1. Stack and queue do not need to be traversed (so stack and queue do not have iterators) , they only need to operate on one or both fixed ends.
  2. When the elements in the stack grow, deque is more efficient than vector (there is no need to move a large amount of data when expanding); when the elements in the queue grow, deque is not only efficient, but also has high memory usage. It combines the advantages of deque and perfectly avoids its shortcomings.

3. stack

1. Introduction to stack

We can first take a look at stack ’s documentation: stack .

  1. Stack is a container adapter that is specially used in contexts with last-in-first-out operations. Its deletion can only insert and extract elements from one end of the container.

  2. Stack is implemented as a container adapter. A container adapter is a container that encapsulates a specific class as its underlying container and provides a set of specific member functions to access its elements. It uses a specific class as its underlying, element-specific container at the end ( That is, the top of the stack) is pushed and popped.

  3. The underlying container of the stack can be any standard container class template or some other specific container class. These container classes should support the following operations:

     		empty:判空操作
     		back:获取尾部元素操作
     		push_back:尾部插入元素操作
     		pop_back:尾部删除元素操作
    
  4. The standard containers vector, deque, and list all meet these requirements. By default, if no specific underlying container is specified for the stack , deque is used by default .

Let’s first briefly look at the use of stack :

		void test_stack()
		{
			stack<int> st;
			st.push(1);
			st.push(2);
			st.push(3);
			st.push(4);
			st.push(5);
			while (!st.empty())
			{
				cout << st.top() << ' ';
				st.pop();
			}
			cout << endl;
		}

The running results are as follows:

Insert image description here

2. Simulate and implement stack

We use deque as the adapter simulation implementation of stack :

		#pragma once
		#include <vector>
		#include <deque>
		
		namespace Young
		{
			template <class T, class Container = deque<T>>
			class Stack
			{
			public:
				// 入栈
				void push(const T& val)
				{
					_con.push_back(val);
				}
		
				// 出栈
				void pop()
				{
					_con.pop_back();
				}
		
				// 获取栈顶元素
				T& top()
				{
					return _con.back();
				}
		
				// const 对象获取栈顶元素
				const T& top() const
				{
					return _con.back();
				}
				
				// 获取栈的大小
				size_t size()
				{
					return _con.size();
				}
		
				// 判断栈是否为空
				bool empty()
				{
					return _con.empty();
				}
		
			private:
				Container _con;
			};
		}

As above, the common interfaces of stack have been implemented. Let’s test it with our own implemented stack :

Insert image description here

4.queue

1. Use of queue

Let’s take a look at the documentation of queue first: queue .

  1. A queue is a container adapter designed to operate in a FIFO context (first in, first out), where elements are inserted from one end of the container and extracted from the other end.

  2. The queue is implemented as a container adapter, which encapsulates a specific container class as its underlying container class. The queue provides a specific set of member functions to access its elements. Elements are put into the queue from the end of the queue and dequeued from the head.

  3. The underlying container can be one of the standard container class templates or other specially designed container classes. The underlying container should support at least the following operations:

     		empty:检测队列是否为空
     		size: 返回队列中有效元素的个数
     		front:返回队头元素的引用
     		back: 返回队尾元素的引用
     		push_back:在队列尾部入队列
     		pop_front:在队列头部出队列
    
  4. The standard container classes deque and list meet these requirements. By default, if no container class is specified for queue instantiation, the standard container deque is used .

First, let’s take a brief look at the use of queue :

		void test_queue()
		{
			queue<int> q;
			q.push(1);
			q.push(2);
			q.push(3);
			q.push(4);
			q.push(5);
			while (!q.empty())
			{
				cout << q.front() << ' ';
				q.pop();
			}
			cout << endl;
		}

The running results are as follows:

Insert image description here

2. Simulate queue implementation

We also use deque to adapt queue :

		#pragma once
		#include <deque>
		
		
		namespace Young
		{
			template<class T, class Container = deque<T>>
			class Queue
			{
			public:
				// 入队列
				void push(const T& val)
				{
					_con.push_back(val);
				}
		
				// 出队列
				void pop()
				{
					_con.pop_front();
				}
		
				// const 对象获取队头元素
				const T& front() const
				{
					return _con.front();
				}
		
				// 获取队头元素
				T& front()
				{
					return _con.front();
				}
		
				// const 对象获取队尾元素
				const T& back() const
				{
					return _con.back();
				}
		
				// 获取队尾元素
				T& back()
				{
					return _con.back();
				}
		
				// 获取队列长度
				size_t size()
				{
					return _con.size();
				}
		
				// 判断队列是否空
				bool empty()
				{
					return _con.empty();
				}
			private:
				Container _con;
			};
		}

Let’s use the queue we implemented to test it:

Insert image description here

3. priority_queue

(1) Introduction to priority_queue

priority_queue: Priority queue is a type of queue. Let’s first take a look at its documentation to introduce priority_queue .

  1. A priority queue is a container adapter whose first element is always the largest of the elements it contains according to strict weak ordering criteria.

  2. This context is similar to a heap, where elements can be inserted at any time, and only the largest heap element (the element at the top in the priority queue) can be retrieved.

  3. The underlying container can be any standard container class template or other specifically designed container class. The container should be accessible via random access iterators and support the following operations:

     		empty():检测容器是否为空
     		size(): 返回容器中有效元素个数
     		front():返回容器中第一个元素的引用
     		push_back():在容器尾部插入元素
     		pop_back(): 删除容器尾部元素
    
  4. The standard container classes vector and deque meet these needs. By default, vector is used if no container class is specified for a particular priority_queue class instantiation .

  5. Random access iterators need to be supported so that the heap structure is always maintained internally. The container adapter does this automatically by automatically calling the algorithmic functions make_heap, push_heap, and pop_heap when needed.

(2) Use of priority_queue

By default, the priority queue uses vector as its underlying container for storing data. The heap algorithm is used on the vector to construct the elements in the vector into a heap structure . Therefore, priority_queue is a heap. You can consider using priority_queue wherever a heap is needed. . Note: By default priority_queue is a large heap .

Insert image description here

Note that when we look at the document, priority_queue is a large pile by default , but the red box in the picture above is a functor (described later) , which implements comparison. Less means small, but it is implemented into a large pile. , pay attention here. Secondly, when we use the default parameters, we only need to pass the first parameter, and the subsequent parameters can use the default parameters. However, when we need to use a small heap, we need to pass all the parameters in; let's take a look first. use:

		#include <vector>
		#include <queue>
		#include <functional> // greater算法的头文件
		void TestPriorityQueue()
		{
			// 默认情况下,创建的是大堆,其底层按照小于号比较
			vector<int> v{ 3,2,7,6,0,4,1,9,8,5 };
		
			priority_queue<int> q1;
			for (auto& e : v)
				q1.push(e);
		
			cout << q1.top() << endl;
		
		
			// 如果要创建小堆,将第三个模板参数换成 greater 比较方式
			priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
			cout << q2.top() << endl;
		}

The running results are as follows:

Insert image description here

(3) Functor

Functor, also known as function object, is a class that can perform function functions. Its use is the same as our usual function calls.

First we have to implement a class. This class needs to implement ()operator overloading. The functions implemented in it need to be implemented by ourselves. Suppose we need to implement a large heap in priority_queue , as follows:

		// 仿函数 --- 大堆,大的优先级大
		template <class T>
		class Less
		{
		public:
			bool operator()(const T& x, const T& y)
			{
				return x < y;
			}
		};

So how to call it? First we have to create an object and then use this object to call the function:

			Less<int> less;
			cout << less(1, 8) << endl; // 与下等价
			cout << less.operator()(1, 8) << endl;

The running results are as follows:

Insert image description here

Below we use the functor form to simulate and implement priority_queue .

(4) Simulate implementation of priority_queue

	#pragma once
	#include <vector>
	
	namespace Young
	{
		// 模板参数
		template <class T, class Container = vector<T>, class Compare = Less<T>>
		class PriorityQueue
		{
		public:
			// 向上调整 --- 大堆
			void adjust_up(size_t child)
			{
				Compare com;
				size_t parent = (child - 1) / 2;
				while (child > 0)
				{
					//if (_con[parent] < _con[child]) // 与下等价
					if (com(_con[parent], _con[child]))
					{
						swap(_con[child], _con[parent]);
						child = parent;
						parent = (child - 1) / 2;
					}
					else
					{
						break;
					}
				}
			}
	
			// 向下调整 --- 大堆
			void adjust_down(size_t parent)
			{
				Compare com;
				size_t child = parent * 2 + 1;
				
				while (child < _con.size())
				{
					if (child + 1 < _con.size() && _con[child + 1] > _con[child])
					{
						++child;
					}
	
					// if (_con[parent] < _con[child])  // 与下等价
					if (com(_con[parent], _con[child]))
					{
						swap(_con[child], _con[parent]);
						parent = child;
						child = parent * 2 + 1;
					}
					else
					{
						break;
					}
				}
			}
	
			// 入数据
			void push(const T& val)
			{
				_con.push_back(val);
				adjust_up(_con.size() - 1);
			}
	
			// 出数据
			void pop()
			{
				swap(_con[0], _con[_con.size() - 1]);
				_con.pop_back();
	
				adjust_down(0);
			}
	
			// 获取优先级最大的数据
			const T& top()
			{
				return _con.front();
			}
	
			// 判空
			bool empty()
			{
				return _con.empty();
			}
	
		private:
			Container _con;
		};
	}

Below we use the priority queue we implemented for testing:

		void test_priority_queue()
		{
			// Young::PriorityQueue<int, vector<int>, Greater<int>> pq; // 小堆
		
			// Young::PriorityQueue<int, vector<int>, Less<int>> pq; // 大堆,与下等价
			Young::PriorityQueue<int> pq;  // 缺省参数默认是大堆
			pq.push(2);
			pq.push(8);
			pq.push(1);
			pq.push(0);
			pq.push(10);
		
			while (!pq.empty())
			{
				cout << pq.top() << ' ';
				pq.pop();
			}
		
			cout << endl;
		}

The test results are as follows:

Insert image description here

Guess you like

Origin blog.csdn.net/YoungMLet/article/details/133272448