[C++ Grocery Store] Explore the underlying implementation of stack and queue

Insert image description here

1. Introduction and use of stack

1.1 Introduction to stack

  • stack is a container adapter designed for use in a last-in-first-out context. Elements can only be inserted and extracted from one end of the container.

  • 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 so that the elements are pushed at the end of the specific container (top of the stack). and pop up.

  • 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: empty operation

    • back: Get the tail element operation

    • push_back: tail insertion element operation

    • pop_back: tail deletion element operation

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

Insert image description here

1.2 Use of stack

function description Interface Description
stack() Construct an empty stack
empty() Check whether the 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

1.2.1 Minimum stack

Insert image description here
The idea of ​​​​this question is to use two stacks to implement it. One stack _stis used to store data normally, and the other stack _minstis used to store minimal data. The specific implementation is _stto make a judgment when inserting data into . If the currently inserted data valis less than or equal to _minstthe data on the top of the stack, then valalso insert into _minstthis stack. Otherwise, insert the data directly into _st. When popcollecting data, first compare _stthe top element of the stack with the top element of the stack. If the two are equal, then pop the top element of both the stack of and the top of the stack, otherwise the top element of the pop stack will be used . To get the smallest element in the stack, just return the top element of the stack._minst_st_minst_st_minst

class MinStack 
{
    
    
public:
    MinStack() {
    
    }
    
    void push(int val) 
    {
    
    
        _st.push(val);

        if(_minst.empty() || val <= _minst.top())
        {
    
    
            _minst.push(val);
        }
    }
    
    void pop() 
    {
    
    

        if(_st.top() == _minst.top())
        {
    
    
            _minst.pop();
        }
        _st.pop();

    }
    
    int top() 
    {
    
    
        return _st.top();
    }
    
    int getMin() 
    {
    
    
        return _minst.top();
    }
private:
    stack<int> _st;
    stack<int> _minst;
};

1.2.2 Stack push and pop sequence

Insert image description here
The idea of ​​solving this problem is to use a stack to simulate. That is to define a stack first st, then put a piece of data into the stack, and then take the data on the top of the stack and popVcompare it with the current position element of the stack sequence, if they are not equal, continue to pushVtake the data from the push sequence stinto the stack, if they are equal Just pop out of the stack. It should be noted here that it may be necessary to pop the stack multiple times in a row. Until finally pushVall the data in the stacking sequence is pushed onto the stack, and finally it is judged stwhether the stack is empty. If it is empty, it means that the popping sequence is correct. If it is not empty, it means that there is a problem with the popping sequence.

class Solution {
    
    
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pushV int整型vector 
     * @param popV int整型vector 
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
    
    
        // write code here
        stack<int> st;
        size_t push_pos = 0, pop_pos = 0;

        while(push_pos < pushV.size())
        {
    
    
            //先入一个元素
            st.push(pushV[push_pos++]);

            if(st.top() != popV[pop_pos])
            {
    
    
                //不匹配继续入数据
                continue;
            }
            
            while(!st.empty() && st.top() == popV[pop_pos])
            {
    
    
                //匹配,出数据
                st.pop();
                pop_pos++;
            }
        }

        return st.empty();
    }
};

Code optimization : We can find that the mismatch logic in the above code actually does nothing, so we can delete this code and add it above to make the logic clearer.

class Solution {
    
    
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pushV int整型vector 
     * @param popV int整型vector 
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
    
    
        // write code here
        stack<int> st;
        size_t push_pos = 0, pop_pos = 0;

        while(push_pos < pushV.size())
        {
    
    
            //先入一个元素
            st.push(pushV[push_pos++]);

            while(!st.empty() && st.top() == popV[pop_pos])
            {
    
    
                //匹配,出数据
                st.pop();
                pop_pos++;
            }
        }

        return st.empty();
    }
};

1.2.3 Evaluating reverse Polish expressions

Insert image description here
Reverse Polish expressions are also called postfix expressions. What is a postfix expression? Let’s first learn about infix expressions. Infix expressions are the most common ones we usually use, for example: 2 + 3 ∗ 1 2+3*12+31 , is a typical infix expression. Turn the previous infix expression into a postfix expression to get:2 1 3 * +, which is a postfix expression. Compared with the infix expression, the postfix expression does not change the order of the operands, and the operators are rearranged according to priority. This question requires us to solve suffix expressions. To solve the postfix expression, we can use a stack, push the operands into the stack, take out two elements from the stack when encountering the operator, perform operations, and continue to push the operation results onto the stack. The final element on the top of the stack is the result of the entire reverse Polish expression.

class Solution {
    
    
public:
    int evalRPN(vector<string>& tokens) {
    
    
        stack<int> st;
        size_t pos = 0;
        while(pos < tokens.size())
        {
    
    
            
            if(tokens[pos] != "+" && tokens[pos] != "-" && tokens[pos] != "*" && tokens[pos] != "/")
            {
    
    
                //如果是数字就入栈
                int num = stoi(tokens[pos]);
                st.push(num);
            }
            else
            {
    
    
                //不是数字就从栈中取两个元素出来
                int val1 = st.top();
                st.pop();
                int val2 = st.top();
                st.pop();
                int ret = 0;
                if(tokens[pos] == "+")
                {
    
    
                    ret = val2 + val1;
                }
                else if(tokens[pos] == "-")
                {
    
    
                    ret = val2 - val1;
                }
                else if(tokens[pos] == "*")
                {
    
    
                    ret = val2 * val1;
                }
                else if(tokens[pos] == "/")
                {
    
    
                    ret = val2 / val1;
                }

                //将计算结果继续入栈
                st.push(ret);
            }

            pos++;
        }

        return st.top();
    }
};

Note : There are a few points that need special attention when writing the above code. First of all, this is an stringarray and will involve stringconversion int. Secondly, it should be noted that when fetching numbers from the stack, the first fetch is the right operand, and the second fetch is the left operand, so it should be the left operand and the right operand, especially for val2subtraction val1and For division operations, the order of the two operands must be guaranteed.

Supplement : Here is a little knowledge point: how to convert infix expressions into postfix expressions. The main process is divided into the following steps:

  1. Output when encountering an operand (the output here is to store it in some kind of container).

  2. When an operator is encountered, it is divided into the following two situations according to the order of priority:

    • If the stack is empty or the current operator has a higher priority than the top of the stack, continue pushing onto the stack.

    • If the stack is not empty and the current operator has a lower or equal priority than the top of the stack, output the operator on the top of the stack and continue to the second step.

  3. After the infix expression ends, the operators in the stack are popped in sequence.

Small Tips : Whether the current operator can be calculated depends on whether the priority of the latter operator is higher than itself, so whenever we encounter an operator, don’t rush to push it into the stack, first and the operator on the top of the stack Perform a priority comparison. If the priority of the current operator is lower than or equal to that of the operator on the top of the stack, we can take out the operator on the top of the stack for operation. If you encounter parentheses, you can do a recursion. Secondly, we need to find a way to determine the priority of symbols.

1.2.4 Using stack to implement queue

Insert image description here
Treat one stack as the input stack for pushing incoming data, and the other stack as the output stack for pop and peek operations. Every time pop or peek, if the output stack is empty, all the data in the input stack will be popped and pushed into the output stack in turn, so that the order of the output stack from the top of the stack to the bottom of the stack is the order of the queue from the head to the end of the queue.

class MyQueue {
    
    
public:
    MyQueue() 
    {
    
    }
    
    void push(int x) 
    {
    
    
        input_st.push(x);
    }
    
    int pop() 
    {
    
    
        if(output_st.empty())
        {
    
    
            while(!input_st.empty())
            {
    
    
                output_st.push(input_st.top());
                input_st.pop();
            } 
        }

        int ret = output_st.top();
        output_st.pop();

        return ret;
    }
    
    int peek() 
    {
    
    
        if(output_st.empty())
        {
    
    
            while(!input_st.empty())
            {
    
    
                output_st.push(input_st.top());
                input_st.pop();
            }
        }
        
        return output_st.top();
    }
    
    bool empty() 
    {
    
    
        return input_st.empty() && output_st.empty();
    }
private:
    stack<int> input_st;
    stack<int> output_st;
};

2. Introduction and use of queue

2.1 Introduction to queue

  • A queue is a container adapter designed to perform first-in-first-out operations in a FIFO context, where elements are inserted from one end of the container and extracted from the other.

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

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

    • empty: Check if the queue is empty

    • size: Returns the number of valid elements in the queue

    • front: Returns a reference to the head element

    • back: Returns a reference to the last element of the queue

    • push_back: queue at the end of the queue

    • pop_front: Dequeue at the head of the queue

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

Insert image description here

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

2.2.1 Level-order traversal of binary trees

Insert image description here
The level-order traversal of a binary tree can be implemented with the help of a queue. Starting from the root node, first put the parent node into the queue, and while dequeuing the tail node, put the left child and right child of the node into the queue in sequence. Until the queue is empty, the result coming out of the queue is the result of the level-order traversal. This question requires storing all nodes in the same layer into a one-dimensional array, and then combining these one-dimensional arrays into a two-dimensional array and returning it. Our previous approach will cause two layers of nodes to be mixed in the queue, so we can define a variable levelSizeto record the number of nodes in each layer. The specific code is as follows:

class Solution 
{
    
    
public:
    vector<vector<int>> levelOrder(TreeNode* root) 
    {
    
    

        vector<vector<int>> retV;
        
        queue<TreeNode*> qu;
        int levelSize = 0;

        qu.push(root);

        while(!qu.empty())
        {
    
    
            vector<int> tmp;
            levelSize = qu.size();
            while(levelSize != 0)
            {
    
    
                TreeNode* top = qu.front();
                if(top != nullptr)
                {
    
    
                    tmp.push_back(top->val);
                    qu.push(top->left);
                    qu.push(top->right);
                } 

                qu.pop();
                levelSize--;
            }

            if(!tmp.empty())
            {
    
    
                retV.push_back(tmp);
            }
            
        }

        return retV;
    }
};

3. Simulation implementation

3.1 Stack simulation implementation

template<class T, class Continer = vector<T>>
	class stack
	{
    
    
	public:
		stack()
		{
    
    }

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

		void pop()
		{
    
    
			_con.pop_back();
		}

		T& top()
		{
    
    
			return _con.back();
		}

		size_t size()
		{
    
    
			return _con.size();
		}

		bool empty()
		{
    
    
			return _con.empty();
		}

	private:
		Continer _con;
	};

Small Tips : 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.2 queue simulation implementation

template<class T, class Continer = std::list<T>>
class queue
{
    
    
public:
	queue()
	{
    
    }

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

	void pop()
	{
    
    
		_con.pop_front();//这里不再支持vector
	}

	T& front()
	{
    
    
		return _con.front();
	}

	T& back()
	{
    
    
		return _con.back();
	}

	size_t size()
	{
    
    
		return _con.size();
	}

	bool empty()
	{
    
    
		return _con.empty();
	}
private:
	Continer _con;
};

Small Tips : The stack cannot be implemented with the help of vector, because dequeuing is equivalent to deleting the first element in the vector, and deleting the vector head will involve moving data, and the efficiency will be reduced compared to the list.

4. Container Adapter

4.1 What is an adapter?

Adapter is a design pattern (a design pattern is a set of repeated use, known to most people, classified and cataloged, and a summary of code design experience), which converts the interface of a class into another that customers want. interface.
Insert image description here

4.2 The underlying structure of stack and queue in the STL standard library

Although elements can also be stored in stack and queue, they are not divided into container ranks in STL, but are called container adapters, because stack and queue only wrap the interfaces of other containers, and in STL stack and queue use deque by default.

4.3 A brief introduction to deque

4.3.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 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.
Insert image description here
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
The bottom layer of the double-ended queue is an imaginary continuity. Space is actually segmented and continuous. In order to maintain its "overall continuity" and the illusion of random access, it falls on the iterator of deque. Therefore, the iterator design of deque is more complicated, as shown in the following figure:
Insert image description here
Insert image description here

4.3.2 Defects of deque

  • Compared with vector, the advantage of deque is that when the head is inserted and deleted, there is no need to move elements, which is particularly efficient , and when expanding, it does not need to move a large number of elements, so its efficiency is higher than vector.

  • Compared with list, its underlying space is continuous, the space utilization 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, a linear structural formula is required. In most cases, vector and list are given priority. There are not many applications of deque. One application scenario that can be seen currently is that STL uses it as the underlying data structure of stack and queue. .

4.4 Why choose deque as the underlying default container for stack and queue?

Stack is a special linear data structure of last-in-first-out, so as long as it is a linear structure with push_back() and pop_back() operations, it can be used as the underlying container of stack, such as vector and list; queue is first-in-first-out Special linear data structures, as long as they have push_back() and pop_front() operations, can be used as queue de underlying containers, such as lists. However, in STL, deque is selected as the underlying container by default for stack and queue, mainly because:

  • 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 queue grow, deque is not only efficient, but also has high memory utilization.

5. Conclusion

Today's sharing is over here! If you think the article is not bad, you can support it three times . There are many interesting articles on Chunren's homepage . Friends are welcome to comment. Your support is the driving force for Chunren to move forward!

Insert image description here

Guess you like

Origin blog.csdn.net/weixin_63115236/article/details/132661116