[C++] STL adapter---use deque to implement stack and queue

Table of contents

Preface

1. Deque

 1. Introduction to the principle of deque

 2. The underlying structure of deque

 3. Iterator of deque

 4. Advantages and disadvantages of deque

  4.1. Advantages

  4.2. Disadvantages

2. Introduction and use of stack

 1. Introduction to stack

 2. Use of stack

 3. Simulation implementation of stack

3. Introduction and use of queue

 1. Introduction to queue 

 2. Use of queue

 3. Simulation implementation of queue


Preface

  Container adapter, literally speaking, is used to match a container. In C++STL, containers include: vector, list, deque, map, set, etc. In C++STL, stack and queue are not included in the scope of the container but are included in the scope of the container adapter because:

  Stack and queue do not have subscript random access and other operations. They only have ordinary pop_front, push_back, pop_back() and other operations. These functions can be found in other containers. The implementation of stack and queue can completely reuse the operations of other containers. , This is why stack and queue serve as container adapters.

  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 the customer wants. interface.

1. Deque

 1. Introduction to the principle of deque

  deque (double-ended queue): It is a double-opening "continuous" space data structure. The meaning of double-opening is that deque can perform insertion and deletion operations at both ends, and the time complexity is O(1); and Compared with vector, head insertion efficiency is high, and there is no need to move elements. Compared with list, space utilization is relatively high, and "random access" efficiency is high.

 2. The underlying structure of deque

  The underlying structure of deque is not actually a continuous space , but is made up of continuous small spaces . The actual deque is similar to a dynamic two-dimensional array. Its structural diagram is as follows:

 3. Iterator of deque

  The bottom layer of the double-ended queue is an imaginary continuous space, which is actually segmented. In order to maintain its "overall continuity" and the illusion of random access, it falls on the iterator of deque, so the iterator design of deque is more complicated. As shown below:

 4. Advantages and disadvantages of deque

  4.1. Advantages

  • It has the advantages of vector---supports random access, high cache hit rate, and high tail insertion and deletion data efficiency;
  • At the same time, it has the advantages of list---less space waste and high efficiency of header insertion and data insertion;

  4.2. Disadvantages

  • The random access efficiency of deque is low---you need to first find the corresponding buffer array through the central control data, and then find the specific position (assuming the offset is i, you need to first get the buffer array i/10, and then i %10 Get the specific position in the buffer array), that is, deque skips one buffer array at a time during random access. It needs to jump multiple times to accurately locate it. Its efficiency is much higher than list, but it is also much lower than vector;
  • The efficiency of inserting and deleting data in the middle of deque is relatively low --- the data needs to be moved, but not necessarily all the data in the subsequent buffer array will be moved. You can control only a part of it to be moved, that is, the efficiency of inserting and deleting data in the middle is higher than that of vector, but it is lower on list.

 So in summary, deque combines the advantages and disadvantages of vector and list. It seems perfect, but its unilateral performance is not as good as vector or list, so  deque is rarely used in practical applications.

The reason why deque is selected as the default adaptation container for stack and queue in STL:

  • 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.
  • 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.

  deque is particularly suitable for scenarios that require a large amount of head insertion and tail data insertion and deletion, occasional random access, and occasional middle insertion and deletion; it is not suitable for scenarios that require a large amount of random access and middle data insertion and deletion, especially sorting.

2. Introduction and use of stack

 1. Introduction to 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 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 classes. These container classes should support the following operations: empty: empty operation back: get the tail element operation push_back: insert the element at the tail operation pop_back: delete the element at the tail operate;
  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.

 2. Use of stack

Function description Interface Description
stack() Construct an empty stack
empty() Check if stack is empty
size() Returns the number of elements in the stack
top() Returns a reference to the top element of the stack
push() Push the element val into the stack
pop() Pop the last elements in the stack

The following is the usage of the stack: 

int main()
{
	//构造空栈
	stack<int> s;

	//元素入栈
	s.push(1);
	s.push(2);

	//获取栈中元素个数
	int Size = s.size();
	cout << Size << endl;

	//获取栈顶元素的引用
	int sTop = s.top();
	cout << sTop << endl;

	//元素出栈
	s.pop();
	sTop = s.top();
	cout << sTop << endl;

	//判断栈是否为空
	cout << s.empty();

	return 0;
}

 3. Simulation implementation of stack

  We define two template parameters for the stack template here: T  is the type of elements stored in the stack , Container  is the underlying structure used by the stack template , the default value of Container  is vector, if you want to use something else, you can here Make settings. I can use the adapter as the second template parameter of the class, and then implement the stack by passing different adapter containers:

//stack.h
template<class T, class Container>
class stack 
{
	//...
};
//test.cpp
void test_stack() 
{
	stack<int, vector<int>> st1;
	stack<int, list<int>> st2;
}

Both vector and list can be used as stack adaptation containers. We can use different containers to adapt stack by giving different second template parameters;

 After preliminary study, it is obvious that it is more suitable as an adaptation container for stack. Then we can also set vector as the default adaptation container for stack:

//stack.h
template<class T, class Container = vector<T>>
class stack 
{
	//...
};
//test.cpp
void test_stack() 
{
	//默认使用vector做适配容器
	stack<int> st1;  
	//使用其他容器做适配容器需要显式指定
	stack<int, list<int>> st2;  
}

 With the adaptation container, we can more easily implement the stack interface by calling the interface of the adaptation container.

namespace xx
{
	//适配器模式/配接器
	template <class T, class Container = deque<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		const T& top()
		{
			return _con.back();
		}
		bool size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	void test_stack()
	{
		//stack<int,vector<int>> st;//数组栈
		stack<int, list<int>> st;//链表栈
		st.push(1);
		st.push(2);
		st.push(3);
		st.push(4);
		while (!st.empty())
		{
			cout << st.top() << " ";
			st.pop();
		}
		cout << endl;
	}
}

Stack can be implemented using vector or list, which is quite efficient. Inserting data is equivalent to tail insertion, and deleting the top element of the stack is equivalent to tail deletion.

3. Introduction and use of queue

 1. Introduction to 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. 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 at least support the following operations: empty: detect whether the queue is empty; size: return the number of valid elements in the queue; front: return a reference to the head element of the queue; back: return a reference to the tail element of the queue; push_back: return the number of valid elements in the queue Enter the queue at the tail; pop_front: dequeue at the head of the queue;
  4. The standard container classes deque and list satisfy these requirements. By default, if no container class is specified for queue instantiation, the standard container deque is used.

 2. Use of queue

function declaration Interface Description
queue() Construct an empty queue
empty() Checks whether the queue is empty, returns true if it is, otherwise returns false
size() Returns the number of valid elements in the queue
front() Returns a reference to the head element of the queue
back() Returns a reference to the last element of the queue
push() Put element val into the queue at the end of the queue
pop() Dequeue the head element of the queue

The following is the usage operation of the queue:

int main() 
{
	//构造空队列
	queue<int> q;

	//元素入队
	q.push(1);
	q.push(2);

	//返回有效元素个数
	int size = q.size();
	cout << size << endl;

	//检查队列是否为空
	cout << q.empty() << endl;

	//获取队头元素的引用
	int front = q.front();
	cout << front << endl;

	//获取队尾元素的引用 
	int back = q.back();
	cout << back << endl;

	//队头元素出队
	q.pop();
	return 0;
}

 3. Simulation implementation of queue

  Its simulation implementation process is similar to stack. Both vector and list can be used as queue adaptation containers. However, because queue needs to delete a large amount of data at the head, deque is used as the queue's default adaptation container. Then the code for queue simulation implementation is as follows :

namespace xx
{
	template <class T, class Container = deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();
		}
		const T& front()
		{
			return _con.front();
		}
		const T& back()
		{
			return _con.back();
		}
		bool size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
	void test_queue()
	{
		queue<int> q;
		q.push(1);
		q.push(2);
		q.push(3);
		q.push(4);
		while (!q.empty())
		{
			cout << q.front() << " ";
			q.pop();
		}
		cout << endl;
	}
}


If there are any shortcomings in this article, you are welcome to comment below and I will correct it as soon as possible.

Guess you like

Origin blog.csdn.net/m0_63198468/article/details/133137162