C++_022栈的基本使用

一、栈的定义

1. stack是一种容器适配器,专门用在具有后进先出的操作的上下文当中,其删除只能从容器的一端进行元素的插入与提取操作;

2. stack与deque都没有迭代器!因为不能任意的访问容器,只能从在规定下访问;

接下来我们尝试测试一下:

int main()
{
	stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	st1.push(4);
	// 遍历容器中的数据
	while (!st1.empty())
	{
		cout << st1.top() << " ";
		st1.pop();
	}
	cout << endl;
	return 0;
}

这里我们遍历不能使用迭代器,因此while的判断条件为 !st1.empty() ,依次出数据,当stack为空则停止;

接下来我们测试queue:

	queue<int> de1;
	de1.push(1);
	de1.push(2);
	de1.push(3);
	de1.push(4);
	// 遍历容器中的数据
	while (!de1.empty())
	{
		cout << de1.front() << " ";
		de1.pop();
	}
	cout << endl;

队列的遍历方法与stack的一样,但是有一点不同的是:stack是取栈顶元素top,但是队列是取头部元素front;

练习题1:最小栈

. - 力扣(LeetCode)

在这里,我们首先的思路是:

设置两个成员变量,一个是stack,一个是min最小值;

但是可能会发生下面这种情况:如果我们依次插入的数据时3 4 5 1; 

对应的最小值最初为3,当1出现的时候变为1,但是如果我们此时把1给pop掉,那么怎么获得之前得到的3呢?

重新遍历的话无法满足时间复杂度为常数;

解决方法:

我们可以设置两个栈,如上图所是,一个用于普通的数据记录,一个用于记录stack的最小值;

当左边的stack进行pop操作的时候,右边的最小栈也进行pop!getmin的数据直接从最小栈中获取!

接下来我们可以进行再优化:

当_st的数据和_minst的数据相等时再pop!

并且如果push的值和最小值相等,此时最小栈中也要进行push!

示例代码如下所示:

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;
};

第二题:栈的弹出压入序列 

栈的压入、弹出序列_牛客题霸_牛客网

解:这道题时输入一个栈的压入序列,判断弹出序列是否满足规则,如果不满足返回false;

定义一个空栈来检测是否满足顺序!

class Solution {
public:
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        // 输入一个入栈的顺序,输出一个出栈的顺序
        // 判断是否满足出栈的顺序
        stack<int> st1;
        int pushi = 0;
        int popi = 0;
        while(pushi < pushV.size())
        {
            // 空栈先进数据
            st1.push(pushV[pushi]);
            pushi++;
            // 判断插入的数据与出栈数据是否匹配
            if(st1.top() != popV[popi])
            {
               // 不匹配 --- 插入数据
               continue; 
            }
            else
            {
                // 匹配 --- 出数据
                while(!st1.empty() && st1.top() == popV[popi])
                {
                    st1.pop();
                    ++popi;
                }
            }
            
        }
        return st1.empty();

    }
};

首先我们先插入数据,然后判断插入的数据和对应的出栈的数据是否相等,如果相等就进行pop;如果不相等再进行push;

第三题:求解逆波兰表达式

. - 力扣(LeetCode)

这里我们需要科普两个知识点:

逆波兰表达式就是后缀表达式!

解题思路:

逆波兰表达式比较适合使用来进行运算:

  • 遇到操作数进行入栈;
  • 遇到操作符,将栈顶的两个元素取出进行运算,运算结果继续入栈;
  • 且先取出来的元素是右操作数(符合栈的规则);
  • 最后剩下的一个元素即是所求的值;

根据上面的步骤我们可以得到代码:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> s;
        // 遍历tokens
        for(auto& str:tokens)
        {
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 取出两个栈顶元素进行运算
                // 再将结果进行入栈
                int right = s.top();
                s.pop();
                int left = s.top();
                s.pop();
                // switch只能是整数 --- 这里char类型可以和int类型互通
                switch(str[0])
                {
                    case '+':
                    s.push(left+right);
                    break;
                }
                switch(str[0])
                {
                    case '-':
                    s.push(left-right);
                    break;
                }                
                switch(str[0])
                {
                    case '*':
                    s.push(left*right);
                    break;
                }
                switch(str[0])
                {
                    case '/':
                    s.push(left/right);
                    break;
                }                
            }
            else{
                // 此时元素为数字
                s.push(stoi(str));
            }
        }
        return s.top();
    }
};

科普点:

运算符的比较不是从全局的运算符进行比较,而是相邻之间的运算符比较!

例如这里3*4虽然*的运算级最高,但是不影响进行+

即中缀表达式转化为后缀表达式如下所示(不带括号的情况下):

遇到括号可以考虑使用递归 / 优先级来解决

栈的模拟实现

注意点:分析下面两种情况

其中,stack.h中包含了<vector>和<list>两个头文件;

结果是第一个无法编译成功,第二个可以编译成功!

这是因为编译器编译是从上往下的,分析第一种情况:当include<Stack.h>文件的时候,里面的命令空间中有vector和list的使用,使用这些头文件必须要展开std!但是由于向上编译中没有发现展开,因此报错!

栈的实现可以使用数组/链表,两种都可以!

并且由于栈底层采用空间适配器用数组或链表来实现,因此栈不需要自己写构造/拷贝构造/析构等函数,因为底层会自动调用数组 / 链表的函数!

对于队列queue来说:

std库里面是有基于链表实现的,但是没有基于数组实现,这是因为对于pop来说,如果是用顺序表,效率非常底下(顺序表的头删效率非常低)!

这里我们给出模拟的queue和stack的代码:

对于Stack.h文件如下:

#pragma once
#include<vector>
#include<list>

namespace shyd {
template<class T,class Container = list<T>>
class deque
{
public:
	void push(const T& x)
	{
		_con.push_back(x);
	}
	void pop()
	{
		//_con.pop_front();
		_con.erase(_con.begin());
	}
	T& front()
	{
		return _con.front();
	}
	size_t size()
	{
		return _con.size();
	}
	bool empty()
	{
		return _con.empty();
	}
private:
	Container _con;
};
// 测试案例for deque
void test_deque()
{
	// deque base on 数组(效率底下,不建议使用)
	deque<int,vector<int>> de1;
	de1.push(1);
	de1.push(2);
	de1.push(3);
	de1.push(4);
	while (!de1.empty())
	{
		cout << de1.front() << " ";
		de1.pop();
	}
	cout << endl;
	// deuqe base on链表
	deque<int, list<int>> de2;
	de2.push(1);
	de2.push(2);
	de2.push(3);
	de2.push(4);
	while (!de2.empty())
	{
		cout << de2.front() << " ";
		de2.pop();
	}
	cout << endl;
}
}

Queue.h文件如下:

#pragma once
#include<vector>
#include<list>

namespace shyd {
template<class T,class Container = list<T>>
class deque
{
public:
	void push(const T& x)
	{
		_con.push_back(x);
	}
	void pop()
	{
		//_con.pop_front();
		_con.erase(_con.begin());
	}
	T& front()
	{
		return _con.front();
	}
	size_t size()
	{
		return _con.size();
	}
	bool empty()
	{
		return _con.empty();
	}
private:
	Container _con;
};
// 测试案例for deque
void test_deque()
{
	// deque base on 数组(效率底下,不建议使用)
	deque<int,vector<int>> de1;
	de1.push(1);
	de1.push(2);
	de1.push(3);
	de1.push(4);
	while (!de1.empty())
	{
		cout << de1.front() << " ";
		de1.pop();
	}
	cout << endl;
	// deuqe base on链表
	deque<int, list<int>> de2;
	de2.push(1);
	de2.push(2);
	de2.push(3);
	de2.push(4);
	while (!de2.empty())
	{
		cout << de2.front() << " ";
		de2.pop();
	}
	cout << endl;
}
}

 然后给出简单的test.cpp

#define  _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"stack.h"
#include"Deque.h"
int main()
{
	//shy::test_stack();
	shyd::test_deque();

	return 0;
}

虽然使用数组和链表可以模拟实现stack和queue,但是std的官方使用的是deque作为适配器,接下来我们了解下deque文件:

参考链接如下所示:https://cplusplus.com/reference/deque/deque/?kw=deque

        双端队列(通常发音为“deck” )是端队列的不规则缩写。双端队列是具有动态大小的序列容器,可以在两端(前端或后端)扩展或收缩。特定 库可能以不同的方式实现端队列,通常是某种形式的动态数组。

可以看到,这个容器的接口非常丰富,几乎集合了数组和链表两者的优点。但是deque的效率不高。

接下来我们测试一组数据,分别使用deque直接进行排序和将deque拷贝到vector,使用vector排序,然后再将数据拷贝到deque当中,测试函数如下所示:

void test_op()
{
	srand(time(0));
	// 设置数据
	const int N = 1000000;
	vector<int> v1;
	vector<int> v2;
	//扩容
	v1.reserve(N);
	v2.reserve(N);

	deque<int> dq1;
	deque<int> dq2;

	//对deque插入数据
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		dq1.push_back(e);
		dq2.push_back(e);
	}
	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	// 将dq1的数据先拷贝到vector
	for (auto e : dq1)
	{
		v1.push_back(e);
	}

	// 排序
	sort(v1.begin(), v1.end());

	// 拷贝回去
	size_t i = 0;
	for (auto& e : dq1)
	{
		e = v1[i++];
	}

	int end1 = clock();

	int begin2 = clock();
	//sort(v2.begin(), v2.end());
	sort(dq2.begin(), dq2.end());

	int end2 = clock();
	printf("deque copy vector sort:%d\n", end1 - begin1);
	printf("deque sort:%d\n", end2 - begin2);
}

 结果如下所示:

可以看到,当有100w个数据需要排序的时候,deque的排序效率跟使用vector相差较大。

回顾下数组和链表之间:

数组和链表之间各有好处和坏处!

对于数组来说

优点:可以支持下标访问,即可以随机访问数据;

缺点:再中间部分和头部进行删除数据的时候太过于麻烦,一般是将该位置的数据被下一位置的数据覆盖掉,且后面的数据逐一向前移动。

对于链表来说

优点:可以在任意位置进行插入和删除数据;按需申请释放,不需要扩容

缺点:不支持随机访问数据,每次找数据的位置需要自己进行遍历,当然也可以通过双向循环链表/添加一个索引数组等进行改进。

二、deque的介绍

概念:

        deque(双端队列 ) :是一种双开口的 " 连续 " 空间的数据结构 ,双开口的含义是:可以 在头尾两端进行插入和 删除操作,且时间复杂度为O(1) ,与 vector 比较,头插效率高,不需要搬移元素;与 list 比较,空间利用率比 较高。
deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维 数组 ,其底层结构如下图所示:

 

数据都存储在每个buff当中,且有一个中控数组,实际上是一个指针数组(包含了指向每个buff的指针)

如果中控数组满了,直接扩容即可,因为里面存放的元素类型是指针,扩容花费的代价比较小。

deque最大的弊端,不适合中间进行插入删除!

相比较list申请的空间块是buff类型的,更大,因此cpu的高速缓存利用更好;

        因此,对于deque这种进行头插头删尾插尾删这种极其方便,而在中间进行插入删除非常麻烦的容器来说,用来当作stack和queue的空间适配器非常合适!(stack和queue不能在中间进行元素的删除和插入!)

可以看到官方的std库中stack和queue的底层都是用deque当作空间适配器! 

deque的底层实现非常复杂:

迭代器有四个:

  • cur指向当前的buff中的元素的位置;
  • first指向对应的buff中的第一个元素的位置;
  • last指向对应的buff中的最后一个元素的位置;
  • node指向中控数组中的下一个buff的位置,方便找下一个buff。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一道秘制的小菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值