栈与队列:理论基础、用栈实现队列、用队列实现栈、有效的括号、删除字符串中的所有相邻重复项、逆波兰表达式求值、滑动窗口最大值(难点)、前k个高频元素(难点)

栈与队列理论基础

最基础:栈先进后出,队列先进先出
那么,思考几个问题?

1.C++中stack 是容器么?
2.我们使用的stack是属于哪个版本的STL?
3.我们使用的STL中stack是如何实现的?
4.stack 提供迭代器来遍历stack空间么?

(STL是C++标准库),分为三个最为普通的版本

1.HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
2.P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
3.SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。

接下来介绍的栈与队列都是SGI STL版本里面的数据结构
先进后出的栈:
栈

栈提供push和pop接口,但不提供走访功能,也不提供迭代器(Iterator),而map或set提供迭代器来遍历所有元素。
队列同样不允许遍历行为,不提供迭代器,因此STL队列也不归为容器,而是容器适配器

栈是以底层容器完成所有工作,对外提供统一的接口,底层容器是可插拔的,即可以控制使用哪种容器来实现栈的功能——>因此在STL中不说栈是容器,而是container adapter(容器适配器)
栈的底层实现可以是vector、deque、list,主要就是数组和链表的底层实现
链表底层实现
在SGI STL中如果没有指定底层实现的话,通常是以deque作为缺省情况下的栈的底层结构
SGI STL中队列底层缺省也是使用deque实现
deque是一个双向队列,只要封住一端,就可以实现栈的功能,如果要定义vector为栈的底层实现,代码如下:

std::stack<int,std::vector<int>> third;//使用vector为底层容器的栈

或list为队列的底层实现

std::queue<int,std::list<int>> third;//定义以list为底层容器的队列

因此,队列也不被归类为容器,而归类为container adapter(容器适配器)。

用栈实现队列

例题232(没有实际应用,但是考察栈和队列的好题):请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例

class MyQueue {
public:
    stack<int> in;
    stack<int> out;
    MyQueue() {

    }
    
    void push(int x) {
  in.push(x);
    }
    
    int pop() {
if (out.empty())
	{
		while (!in.empty())
		{
			int x = in.top();
			out.push(x);
			in.pop();
		}
	}
		int x=out.top();
		out.pop();
		return x;
    }
    
    int peek() {
if (out.empty())
	{
		while (!in.empty())
		{
			int x = in.top();
			out.push(x);
			in.pop();
		}
	}
		int x = out.top();
		return x;
    }
    
    bool empty() {
return in.empty() && out.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->peek();
 * bool param_4 = obj->empty();
 */

主要是在弹出顶部时,需要借助两个栈容器,要搞清楚out栈什么时候该进元素,什么时候直接弹出
在peek()中复用了pop()的功能,可以发现,相同功能的代码需要抽象出来,不要每次都粘贴复制,显得代码很乱

用队列实现栈

例题225:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

在这里插入图片描述

与用栈实现队列相似,但pop()和top()有所不同,将in中元素装入out时,要保留最后一位元素作为顶部元素,在弹出in中的顶部元素后,需要将out中的元素重新装回in中

class MyStack {
public:
queue<int> in;
	queue<int> out;
    MyStack() {

    }
    
    void push(int x) {
    in.push(x);
    }
    
    int pop() {
	if (out.empty())
	{
		while (in.size()!=1)
		{
			int x = in.front();
			out.push(x);
			in.pop();
		}
	}
    int x = in.front();
	in.pop();
    while(!out.empty())
    {
        int y=out.front();
        in.push(y);
        out.pop();
    }
	return x;
    }
    
    int top() {
  int x=this->pop();
  in.push(x);
  return x;
  /*两种判断都可行
    if(!in.empty())
    {
        return in.back();
    }
    else
    {
        return out.back();
    }
    */
    }
    
    bool empty() {
return in.empty() && out.empty();
    }
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

优化(只用一个队列模拟栈):弹出栈顶部元素,只需要将队列最后一个元素前的元素重新添加到队列后,这时第一个元素就是顶部元素

有效的括号

例题20:给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。

在这里插入图片描述
括号匹配是使用栈解决的经典问题
在写代码前要分清楚括号不匹配的情况,一共有以下三种:
1)左括号多余导致的不匹配
在这里插入图片描述
2)括号没有多余但类型不匹配
在这里插入图片描述
3)有括号多余导致的不匹配
在这里插入图片描述
要保证代码只要覆盖了这三种情况就是不匹配,否则不会出问题。

1.具体流程为:如遇到左括号直接入栈,右括号需判断是否与栈顶元素匹配,如果栈已经为空或不匹配则返回false,否则匹配,弹出栈顶元素。最后,如果栈为空则说明字符串全对应上,返回true,否则返回false。
2.注意:使用哈希表可以一步到位对应左右括号,而不用使用过多的if、else来判断,代码具有更高的复用性

class Solution {
public:
    bool isValid(string s) {
        //剪枝:先排除奇数个字符串
        //先分析一共有三种错误的情况,分别是:左、右括号多余,右括号不匹配
        //使用栈来存左括号,哈希表存左右括号对应关系,比较当前括号与栈顶部元素是否对应
        if(s.size()%2!=0) return false;
unordered_map<char,char> hmap={
    {')','('},
    {']','['},
    {'}','{'}
};//注意这个分号
 stack<char> sta;
int i;
for(i=0;i<s.length();i++)
{
    if(s[i]=='(' || s[i]=='[' || s[i]=='{') sta.push(s[i]);
    else if(sta.empty() || sta.top()!=hmap[s[i]]) return false;
    else sta.pop();
}
if(sta.empty()) return true;
else return false;
    }
};

删除字符串中的所有相邻重复项

例题1047:给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例

class Solution {
public:
 string removeDuplicates(string s) {
//将元素入栈,判断栈顶元素是否相同,相同则弹出。将栈弹出到字符串,最后翻转字符串
stack<char> sta;
int i;
sta.push(s[0]);
for(i=1;i<s.length();i++)
{
    if(sta.empty() || s[i]!=sta.top())  sta.push(s[i]);//if中要保证第一个条件成立
    else sta.pop();
}
string res;
i=0;
while(!sta.empty())
{
    res+=sta.top();//c++中字符串可以直接加
    sta.pop();
}
reverse(res.begin(),res.end());//reverse()没有返回值
return res;
    }
};

1.注意c++中字符串可以直接用加号拼接
2.reverse()函数无返回值,也不是某种数据类型带有的函数,直接调用
3.if中首先要满足第一个条件,否则容易出错

逆波兰表达式求值

例题150:给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。示例

适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
stack<long int> sta;
for(int i=0;i<tokens.size();i++)
{
    if(tokens[i]=="+")
    {
        int t=sta.top();
        sta.pop();
        int x=sta.top();
        x+=t;
        sta.pop();
        sta.push(x);
    }
    else if(tokens[i]=="*")
    {
        int t=sta.top();
        sta.pop();
        int x=sta.top();
        x*=t;
        sta.pop();
        sta.push(x);
    }
    else if(tokens[i]=="-")
    {
        int t=sta.top();
        sta.pop();
        int x=sta.top();
        x-=t;
        sta.pop();
        sta.push(x);
    }
    else if(tokens[i]=="/")
    {
        int t=sta.top();
        sta.pop();
        int x=sta.top();
        x/=t;
        sta.pop();
        sta.push(x);
    }
    else{
         sta.push(stoll(tokens[i]));//数字不是只在0-9范围内,不能写在判断最开始
    }
}
return sta.top();
    }
};

1.stoll()函数是将字符串转化为long int;
2.vector<string>表示容器里装的元素是string,不是char;而string字符串中元素装的是char;
3.通常使用的中缀表示,如4+13/5,写入13之后还要判断后续操作优先级是否大于前一个操作,而后缀表示则不用考虑优先级,可以直接写入计算,对计算机的计算来说非常友好。

滑动窗口最大值(难点)

例题239:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

示例


前k个高频元素(难点)

例题347:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例
该题涉及优先队列,其中包括:
1.要统计元素出现的频率;——>可以用map实现
2.对频率排序;——>使用一种容器适配器就是优先队列
3.找出前k个高频元素;

什么是优先队列?

就是一个披着队列外衣的堆,因为其对外接口只是从队头取元素,从队尾添加元素,没有其他取元素方法,看起来就是一个队列。

而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

什么是堆呢?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题我们就要使用优先级队列来对部分频率进行排序。

为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。

那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?

所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

流程

//排序方式:对存放元素频率的哈希表进行排序,然后输出前k个高频元素
class Solution {
public:
//升序排序
static int cmp(pair<int, int> a, pair<int, int> b)
{
	return a.second > b.second;
}
    vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> hmap;
	for (int i = 0; i < nums.size(); i++)
	{
		hmap[nums[i]]++;
	}
	//将哈希表中存入vector,进行排序
	vector<pair<int, int>> t;
	for (auto it = hmap.begin(); it != hmap.end(); it++)
	{
		t.push_back(pair<int, int>(it->first, it->second));
	}
	vector<int> res;
	//根据从大到小方式进行排序
	sort(t.begin(), t.end(),cmp);
	for (int i = 0; i < k; i++)
	{
		res.push_back(t[i].first);
	}
    return res;
    }
};

1.将哈希表中的对存入vector中,vector中数据类型为pair< , >;
2.自定义从大到小的排序方式,可以在sort函数中使用自定义的排序顺序;

栈与队列的理论总结

首先了解栈和队列的理论基础,提到四个问题:
1.C++中stack、queue是容器码?——>它们是容器适配器
2.使用的stack、queue是属于哪个版本的STL?——>SGI STL是开源的
3.使用的STL中的stack、queue是如何实现的?——>底层采用deque、list、链表等向外提供接口实现功能
4.stack、queue提供迭代器来遍历空间吗?——>不提供

面试题:栈里面的元素在内存中是连续分布的吗?

这个问题有两个陷阱:

  • 1.栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不是连续分布;
  • 2.缺省情况下,默认底层容器是deque,那么deque在内存中分布是不连续的。

队列的经典题目

1.滑动窗口最大值问题

难点:单调队列
思想:队列没有必要维护窗口里的所有元素,只需要维护有可能是窗口内最大值的元素就可以,同时保证队列内元素数值是从大到小的。
C++中没有直接支持的单调队列,需要自己定义,与优先队列(对队列进行排序)不同。

1.pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作;

2.push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止;

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

2.求前K个高频元素

难点:优先级队列

思想:披着队列外衣的堆,从对外接口的队头取元素,从队尾添加元素,没有其他取元素方式,看起来是一个队列。内部元素是自动依照元素的权值排列。

排列方式:缺省情况下priority_queue使用max_heap(大顶堆)完成对元素的排序,大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

堆:是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子值。如果父亲结点大于等于孩子就是大顶堆,否则是小顶堆。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值