【C++】stack和queue类

stack和queue

1、stack

1.1 stack的介绍

  • stack是一种容器适配器,专门设计用于先出环境(后进先出),即只从容器一端插入和提取元素。
  • stack以容器适配器的形式实现,容器适配器是使用特定容器类封装对象作为底层容器的类,提供特定的成员函数集以访问其元素。元素是从特定容器的尾部(栈顶)推送/弹出的,这被称为堆叠顶部。
  • stack的底层容器可以是任何标准容器类模板,也可以是其他专门设计的容器类。这些应支持以下操作:
  1. empty:判空
  2. size:大小
  3. back:获取尾部元素
  4. push_back:尾部插入
  5. pop_back:尾部删除

1.2 stack的使用

函数说明 接口说明
stack() 构造空的栈
empty() 检测 stack 是否为空
size() 返回 stack 中元素的个数
top() 返回栈顶元素的引用
push() 将元素 val 压入 stack 中
pop() 将 stack 中尾部的元素弹出

1.2.1 最小栈

思路:用两个栈实现,其中一个栈_st用来正常存储数据,另一个栈_minst用来存储最小的数据。具体实现是往_st中插入数据时判断,如果当前插入的数据val小于等于_minst栈顶的数据,就将val插入到_minst这个栈中。否则直接将数据插入_st中。在pop数据时,先取_st的栈顶元素和_minst的栈顶元素进行比较,如果两者相等,那就同时取_st和_minst的栈顶元素,要获取堆栈中的最小元素直接返回_minst的栈顶元素即可

c 复制代码
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 栈的压入、弹出序列

本题思路是先定义一个栈st,然后给栈中入一个数据,接着取栈顶的数据和出栈序列popV当前位置进行比较,如果不相等则继续从入栈序列pushV中拿数据往st中入,如果相等就出栈,最后判断栈st是否为空,如果为空说明出站序列正确,如果不为空说明出站序列有问题

c 复制代码
class Solution {
public:
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
        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();
    }
};

1.2.3 逆波兰表达式求值

逆波兰表达式也叫后缀表达式,中缀表达式是我们平时最常见的,例如:10*2-8,变成后缀表达式后得到:10 2 * 8 -,操作顺序不变,操作符按优先级重排。这道题要求对后缀表达式进行求解,可以借助栈,遇到操作数入栈,遇到操作符从栈中出两个元素进行运算,将运算结果继续入栈。最终栈顶的元素就是整个逆波兰表达式的结果

c 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(auto str : tokens)
        {
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 运算符,运算,运算结果入栈
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                
                switch(str[0])
                {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right);
                        break;
                    default:
                        break;
                }
            }
            else
            {
                // 运算数入栈
                st.push(stoi(str));
            }
        }
        return st.top();
    }
};

注意:从栈中取数据时,第一次取出的是右操作数,第二次取出的是左操作数,顺序不能颠倒

1.2.4 用栈实现队列

将一个栈作为输入栈,用于压入push传入的数据,另一个栈作为输出栈,用于pop和peek,输入栈和入队顺序一致,输出栈和出队顺序一致

c 复制代码
typedef int STDataType;
typedef struct Stack {
    STDataType* arr;
    int top;     //指向栈顶的位置
    int capacity;//栈的容量
}ST;
//栈的结构

//初始化
void StackInit(ST* ps) {
    ps->arr = NULL;
    ps->top = ps->capacity = 0;
}

//栈的销毁
void StackDestroy(ST* ps) {
    if (ps->arr) {
        free(ps->arr);
    }
    ps->arr = NULL;
    ps->top = ps->capacity = 0;
}

//入栈---栈顶
void StackPush(ST* ps, STDataType x) {
    assert(ps);
    if (ps->top == ps->capacity) {
        int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
        STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
        if (tmp == NULL) {
            perror("realloc fail!");
            exit(1);
        }
        ps->arr = tmp;
        ps->capacity = newCapacity;
    }
    ps->arr[ps->top++] = x;
}

//判断栈是否为空
bool StackEmpty(ST* ps) {
    assert(ps);
    return ps->top == 0;
}

//出栈---栈顶
void StackPop(ST* ps) {
    assert(!StackEmpty(ps));
    --ps->top;
}

//取栈顶元素
//数据有效个数不减少
//只取栈顶数据  不删除数据
STDataType StackTop(ST* ps) {
    assert(!StackEmpty(ps));
    return ps->arr[ps->top - 1];
}

//获取栈中的有效元素个数
int StackSize(ST* ps) {
    return ps->top;
}

//入队  往pushST中插入数据

//出队  popST不为空从popST中删除数据  否则将pushST中的数据导入popST中再出数据

//取队头元素  popST不为空从popST中取数据  否则将pushST中的数据导入popST中再取数据

typedef struct {
	ST pushST;
	ST popST;
}MyQueue;

MyQueue* myQueueCreate() {
	MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
	StackInit(&pq->pushST);
	StackInit(&pq->popST);

	return pq;
}

void myQueuePush(MyQueue* obj, int x) {
	//入队  往pushST中插入数据
	StackPush(&obj->pushST, x);
}

int myQueuePop(MyQueue* obj) {
	//出队  popST不为空从popST中删除数据  否则将pushST中的数据导入popST中再出数据
    if (StackEmpty(&obj->popST)) {
		//将pushST中的数据导入popST中
        while (!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));
			StackPop(&obj->pushST);
        }
    }
	int top=StackTop(&obj->popST);
	StackPop(&obj->popST);
	return top;
}

int myQueuePeek(MyQueue* obj) {
    if (StackEmpty(&obj->popST)) {
        //将pushST中的数据导入popST中
        while (!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));
            StackPop(&obj->pushST);
        }
    }
    return  StackTop(&obj->popST);
}

bool myQueueEmpty(MyQueue* obj) {
	return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}

void myQueueFree(MyQueue* obj) {
	StackDestroy(&obj->pushST);
	StackDestroy(&obj->popST);
	free(obj);
	obj = NULL;
}

2、queue

2.1 queue的介绍

  • 队列是一种容器适配器,专门设计用于先进先出环境下,即将元素插入容器的一端,从另一端提取。
  • 队列实现为容器适配器,这些类使用特定容器类的封装对象作为底层容器,提供特定的成员函数集以访问其元素。元素从不队尾入队列,从队头出队列。
  • 底层容器可以是标准容器类模板或其他专门设计的容器类。该底层容器应至少支持以下操作:
    • empty:检查队列是否为空
    • size:返回队列中有效元素个数
    • front:返回队头元素的引用
    • back:返回队尾元素的引用
    • push_back:在队列尾部入队列
    • pop_front:在队列头部出队列

2.2 queue的使用

函数声明 接口说明
queue() 构造空的队列
empty() 检测队列是否为空,是返回true,否则返回false
size() 返回队列中有效元素的个数
front() 返回队头元素的引用
back() 返回队尾元素的引用
push() 在队尾将元素val入队列
pop() 将队头元素出队列

2.2.1 二叉树的层序遍历

二叉树的层序遍历可以借助队列实现,先让根节点入队列,首先取出队头元素,把队头删除,左孩子不为空时让左孩子入队列,右孩子不为空时让右孩子入队列,充当根节点

c 复制代码
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        // 1. 定义队列
        queue<TreeNode*> q;
        // 2. 如果根不为空,入队
        if(root) q.push(root);
        // 3. 初始化当前层节点数
        int levelSize = q.size();

        while (!q.empty())
        {
            vector<int> v;
            // 控制一层一层出
            while (levelSize--)
            {
                TreeNode* front = q.front();
                q.pop();
                v.push_back(front->val);

                if (front->left)
                    q.push(front->left);
                if (front->right)
                    q.push(front->right);
            }
            vv.push_back(v);

            // 当前层出完了,下一层节点都进队列了,队列数据个数就是下一层的节点个数
            levelSize = q.size();
        }
        return vv; // 4. 必须返回结果
    }
};

3、模拟实现

3.1 stack模拟实现

c 复制代码
template<class T, class Container = deque<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

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

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

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

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

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

	private:
		Container _con;
	};

注意

  • 因为stack可以由vector或list或deque多种容器作为内核实现,所以将容器写成一个模版参数Container,让用户自己选择使用哪种容器
  • stack可以用vector或者list实现

3.2 queue模拟实现

c 复制代码
template<class T, class Container = deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}

		void pop()
		{
			//_con.erase(_con.begin());
			_con.pop_front();
		}

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

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

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

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

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

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

	private:
		Container _con;
	};

注意:不能借助vector实现,因为出队列相当于删除vector中的第一个元素,效率低

4、deque的介绍

4.1 deque的原理

deque是一种双端开口的数据结构,可以在头尾两端进行插入和删除,时间复杂度为O(1),与vector相比,头插效率高,与list相比,空间利用率高

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问的假象,落在了deque的迭代器身上 ,因此deque的迭代器设计就比较复杂,如下图所示:

4.2 deque的缺陷

  • 与vector比较 ,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高 ,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
  • 与list比较 ,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
  • 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下 ,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list ,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构

4.3 为什么选择deque作为stack和queue的底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

  • stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
  • 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

5、priority_queue

5.1 priority_queue的介绍

  • 优先队列是一种容器适配器,专门设计使其第一个元素总是其所包含元素中最大的一个,符合某种严格的弱排序标准。

  • 这种上下文类似于堆,元素可以随时插入,且只能检索最大堆元素(优先队列顶部的那个)。

  • 优先队列以容器适配器的形式实现,这些类使用特定容器类的封装对象作为底层容器,queue提供特定的成员函数集以访问其元素。元素是从特定容器的"尾部"弹出的,这被称为优先队列的顶部。

  • 底层容器可以是任何标准容器类模板,也可以是其他专门设计的容器类。容器应通过随机访问迭代器访问,并支持以下操作:

    • empty():检查是否为空
    • size():返回容器中有效元素个数
    • front():返回容器中第一个元素的引用
    • push_back():在容器尾部插入元素
    • pop_back():删除容器尾部元素
  • 标准容器类vector和deque满足这些要求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

  • 为了始终保持堆结构,需要支持随机访问迭代器。这由容器适配器自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作(即存在建堆算法、向上向下调整算法

5.2 priority_queue的使用

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆

函数声明 接口说明
priority_queue() / priority_queue(first, last) 构造一个空的优先级队列
empty() 检测优先级队列是否为空,是返回true,否则返回false
top() 返回优先级队列中最大(最小元素),即堆顶元素
push(x) 在优先级队列中插入元素x
pop() 删除优先级队列中最大(最小)元素,即堆顶元素

注意:默认情况下,priority_queue是大堆。如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供>或<的重载

5.2.1 数组中的第k个最大元素

c 复制代码
class Solution {
public:
	int findKthLargest(vector<int>& nums, int k) {
		// 将数组中的元素先放入优先级队列中
		priority_queue<int> p(nums.begin(), nums.end());
		// 将优先级队列中前k-1个元素删除掉
		for (int i = 0; i < k - 1; ++i)
		{
			p.pop();
		}
		return p.top();
	}
};

5.3 priority_queue模拟实现

5.3.1 仿函数

仿函数是个类,其内部重载了operator()函数,可以使一个类的对象当做是一个函数去用,和正常函数一样,因此叫做仿函数。

eg:

c 复制代码
#include <iostream>
using namespace std;

// 定义一个仿函数类
struct Add {
    // 重载 () 运算符
    int operator()(int a, int b) {
        return a + b;
    }
};

int main() {
    Add add;  // 创建仿函数对象
    
    // 像函数一样使用
    int res = add(10, 20); 
    cout << "结果:" << res << endl; // 输出 30
    
    return 0;
}

仿函数一般都是类模板,可以支持不同类型的变量

priority_queue的实现:本质是堆

c 复制代码
#pragma once

#include<vector>

namespace central
{
	template<class T, class Container = vector<T>, class Compare = Less<T>>
	class priority_queue
	{
	public:
		priority_queue() = default;

		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			:_con(first, last)
		{
			// 向下调整建堆
			for (int i = (_con.size()-1-1)/2; i >= 0; i--)
			{
				adjust_down(i);
			}
		}

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

			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[0];
		}

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

		size_t size() const
		{
			return _con.size();
		}
	private:
		void adjust_up(int child)
		{
			Compare com;

			int parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[child] > _con[parent])
				//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(int parent)
		{
			Compare com;
			 
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				// 选出大的那个孩子
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
					++child;

				if (com(_con[parent], _con[child]))
				{
					swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}
	private:
		Container _con;
	};
}

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
郝学胜-神的一滴2 小时前
罗德里格斯旋转公式(Rodrigues‘ Rotation Formula)完整推导
c++·unity·godot·图形渲染·three.js·unreal
lzh200409192 小时前
深入理解进程:从PCB内核结构到写时拷贝的底层实战
linux·c++
aseity3 小时前
跨平台项目中QString 与 非Qt 跨平台动态库在字符集上的一个实用的互操作约定.
c++·经验分享
CN-Dust3 小时前
【C++】while语句例题专题
数据结构·c++·算法
用户805533698033 小时前
现代Qt开发教程(新手篇)1.11——定时器
c++·qt
澈2073 小时前
STL迭代器:容器遍历的万能钥匙
开发语言·c++
azoo3 小时前
emplace_back和push_back() 函数添加 cv::Point 类型数据
c++·opencv
样例过了就是过了4 小时前
LeetCode热题 不同路径
c++·算法·leetcode·动态规划