栈和队列详解(知识点+STL实现+相关LeetCode题目)

目录

前言

一、栈与队列

1.单端队列

1.1 单端队列特征

1.2 各种操作的时间复杂度

1.3 C++STL实现操作  queue

2.双端队列

2.1双端队列特征

2.2 C++STL实现操作  deque

3.优先级队列

3.1优先级队列特征

3.2 时间复杂度分析

3.3 C++STL实现操作  priority_queue

4.栈

4.1 栈的特征

4.2 栈的操作复杂度 

4.3 C++STL实现操作 stack

二、力扣题目解析

1.用栈实现队列232

1.1 题目与代码

1.2 详细题解

2.用队列实现栈225

2.1 题目与代码

2.2 详细题解

3.有效的括号20

3.1 题目与代码

3.2 详细题解

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

4.1 题目与代码

4.2 详细题解

 5.逆波兰表达式求值150

5.1 题目与代码

5.2 详细题解

5.3 字符串转换-类型转换函数

6.滑动窗口最大值239

6.1 题目与代码

6.2 详细题解

7.前K个高频元素347

7.1 题目与代码

7.2 无序map -- unordered_map

7.2.1 无序map定义与操作概念

7.2.2 无序map操作

7.3 详细题解

7.4  重要代码解释


前言

主要阐述了数据结构中栈和队列的基本概念与各自的函数操作,基本上收录了所有常用的队列类型和对应STL的实现方式,当前也包括了栈和队列相关的经典力扣题目的解析

一、栈与队列

1.单端队列

1.1 单端队列特征

单端队列,只有一个口可以进,一个口可以出,先进先出

1.2 各种操作的时间复杂度

访问、搜索元素:从头开始到尾,遍历一遍则时间复杂度仍为O(n)

插入、删除元素:只能在队列末尾插入,删除是从头部元素删除

1.3 C++STL实现操作  queue

queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。与栈·一样没有迭代器

  • 1.创建队列:queue<int> values;
  • 2.添加元素(队尾):values.push(T)
  • 3.删除元素(队首):values.pop()
  • 4.获取第一个元素:values.front()
  • 6.获得队列的长度: values.size()
  • 7.判断队列是否为空:values.empty()
  • 8.获取最后一个元素: values.back()

2.双端队列

2.1双端队列特征

 双端队列,两口都可以进,两口都可以出

2.2 C++STL实现操作  deque

  • 1.创建双端队列:deque<T> values;
  • 2.队列头部添加元素:values.push_front();
  • 3.队列头部删除元素:values.pop_front();
  • 4.获得队列头部元素:values.front();
  • 4.队列尾部添加元素:values.push_back();
  • 5.队列尾部删除元素:values.pop_back();
  • 7.获得队列尾部元素:values.back();
  • 8.获得队列的大小:values.size();
  • 9.判断队列是否为空:values.empty();

3.优先级队列

3.1优先级队列特征

在队列中,执行先进先出规则,而在优先级队列中,根据优先级删除值。首先删除具有最高优先级的元素。如果不更改运算规则,顶头元素就是当前队列的最大值

优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。堆数据结构提供了优先队列的有效实现

什么是堆呢?

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

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

3.2 时间复杂度分析

优先级队列--二叉堆节点上浮和下浮的时间复杂度都是logn

3.3 C++STL实现操作  priority_queue

  • 1.创建优先队列
  • //通式 Type为数据类型, Container为保存数据的容器,Functional为元素比较方式
    priority_queue<Type, Container, Functional> name;
    //如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator
    priority_queue<int> p;
  • 2.插入元素(使用operator运算符计算,顶部元素为最大):p.push(T);
  • 3.访问顶部元素:p.top();
  • 4.删除顶部元素:p.pop();
  • 5.容器大小:p.size();
  • 6.判断容器是否为空:p.empty();

4.栈

4.1 栈的特征

栈:先进后出,如图所示,进栈顺序A->B->C,出栈顺序C->B->A

4.2 栈的操作复杂度 

访问元素,直接访问栈顶元素,即为O(1)

搜索元素,未知位置,直到搜索到为止,即为O(n)

插入元素,栈的特殊性只能在尾端插入,即为O(1)

删除元素,栈只能从头部删除,即为O(1)

4.3 C++STL实现操作 stack

  • 1.创建栈:stack<int> values;
  • 2.添加元素(压入栈顶):values.push(T)
  • 3.删除元素(弹出栈顶元素):values.pop()
  • 4.获取第一个(栈顶)元素:values.top()
  • 5.获得栈的元素个数: values.size()
  • 6.判断栈是否为空:values.empty()

二、力扣题目解析

1.用栈实现队列232

1.1 题目与代码

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

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

1 <= x <= 9
最多调用 100 次 push、pop、peek 和 empty
假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

class MyQueue {
private:
    stack<int> stackin;
    stack<int> stackout;
    void inchangeout(){
        while(!stackin.empty()){
            stackout.push(stackin.top());
            stackin.pop();//弹出栈顶元素
        }
        //完全存入stackout
    }
public:
    MyQueue() {

    }
    
    void push(int x) {
        stackin.push(x);
    }
    
    int pop() {
        if(stackout.empty()){
            //如果out栈为空
            inchangeout();
        }
        int topNum = stackout.top();
        stackout.pop();
        return topNum;
    }
    
    int peek() {
        if(stackout.empty()){
            //如果out栈为空
            inchangeout();
        }
        return stackout.top();
    }
    
    bool empty() {
        if(stackout.empty() && stackin.empty()){
            return true;
        }else{
            return false;
        }
    }
};

1.2 详细题解

双栈进行操作,stackin负责存储新的数据,stackout负责保存数据的顺序。

特别要注意的点是,每次进行相关的操作,要保证当前stackout没有数据,如果还有就继续进行下面的操作,因为在stackout栈中的元素顺序是和队列一致的,但是如果有数据的情况下存入,这样就不满足顺序了。所以需要当stackout无数据(全部弹出)的时候,再把stackin数据全部弹出。

2.用队列实现栈225

2.1 题目与代码

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

实现 MyStack 类:

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

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2

class MyStack {
private:
    queue<int> s;
public:
    MyStack() {

    }
    
    void push(int x) {
        s.push(x);
    }
    
    int pop() {
        int sizeNum=s.size();
        sizeNum--;//保留队列最后一个元素==栈顶元素
        while(sizeNum--){
            s.push(s.front());
            s.pop();
        }
        int result = s.front();
        s.pop();
        return result;
    }
    
    int top() {
        return s.back();
    }
    
    bool empty() {
        if(s.empty()){
            return true;
        }else{
            return false;
        }
    }
};

2.2 详细题解

单队列实现。这里主要是巧用了队列的front()和back()函数,front()用来实现循环弹出并存入(队列)真正的栈顶元素前面所有的元素 ,back()用来直接返回栈顶元素

3.有效的括号20

3.1 题目与代码

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

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

示例 1:

输入:s = "()"
输出:true
输入:s = "()[]{}"
输出:true
class Solution {
public:
    bool isValid(string s) {
        stack<char> JudgeS;
        int n = s.size();//获取字符串中字符数量
        if(n % 2 != 0){
            return false;//奇数一定不满足
        }
        for(char ch: s){
            if(ch == '('){
                JudgeS.push(')');
            }
            else if(ch == '{'){
                JudgeS.push('}');
            }
            else if(ch == '['){
                JudgeS.push(']');
            }
            else if(JudgeS.empty() || JudgeS.top() != ch){
                    //如果提前为空了,说明右括号多了||如果不相同,说明类型不同
                    return false;
                
            }
            else{
                //以上都判断完后,说明栈不为空,也相同,那么弹出元素
                JudgeS.pop();
            }
        }

        if(!JudgeS.empty()){
            //如果还有剩余,左括号多
            return false;
        }else{
            return true;
        }

        
    }
};

3.2 详细题解

三种情况

1.左边括号多余了---操作完后栈还剩余,左边括号多了,再返回false

2.配对的括号有,但是类型不一致--直接返回false

3.右边括号多余了--如果栈为空说明右边多余了,左边存的都用完了,则返回false

利用栈的先进后出的特性,只存储左边,并且把其类型转化为右括号存储,当遇到右性质的括号时候,开始于栈顶元素进行比较,此如果没有出错,那么当前右括号是一定和栈顶元素相同的。

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

4.1 题目与代码

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

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

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

class Solution {
public:
    string removeDuplicates(string s) {
        string values;
        for(char ch : s){
            if(values.empty() || values.back() != ch){
                values.push_back(ch);//将元素添加至末尾
            }else if(values.back() == ch){
                values.pop_back();//删除最后一个元素 同时不存入string容器
            }
            
        }
        return values;
    }
};

4.2 详细题解

这道题主要是将string容器模拟成栈的操作,同样是后进先出,依据string容器的函数pop_back()和push_back(),可以在string的末尾,实现元素的存入和元素的弹出。

思路就是存入数据到string容器中的时候,判断数据是否和“栈顶”(string尾部元素)相同,如果相同就不存入并且把尾部元素弹出,这样最终得到的string容器中一定不会含有相邻的重复项

核心操作流程图如下↓

接着↓

 5.逆波兰表达式求值150

5.1 题目与代码

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

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

注意:

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

示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        //运用栈存储的思想Integer.parseInt
        stack<long long> stacks;
        for(int i=0;i<tokens.size();i++){
            string ch = tokens[i];
            if(ch == "+"||ch == "-"||ch == "*"||ch == "/"){
                long long num1 = stacks.top();stacks.pop();//后进先出,num1应该作为乘数,除数
                long long num2 = stacks.top();stacks.pop();
                
                //Switch语句在c++中只适用于int和char类型
                if(ch == "+"){ stacks.push(num2+num1);}
                else if(ch == "-"){ stacks.push(num2-num1);}
                else if(ch == "*"){ stacks.push(num2*num1);}
                else if(ch == "/"){ stacks.push(num2/num1);}
            }
            else{
                stacks.push(stoll(ch));//将字符串转换为longlong长长整形
            }
        }
        return stacks.top();
    }
};

5.2 详细题解

此题是模仿了计算机内部的计算过程,通过栈保存运算数,一旦遇到需要运算符,就开始运算,并且把结果保存在栈中,这样做的好处是优先当前要计算的数(相当于加了括号)只有当前运算完成后,才会进行之后的计算。必要性来自于逆波兰表达式==后缀表达式是其对应的语法树(二叉树)的后序遍历

比如tokens = ["4","13","5","/","+"],后缀算术表达式为

(4 + (13 / 5)),我们可以把它转化为二叉树如下↓

如图所示,当存在两个元素的时候,其后的运算符就一定是对应这两个元素的,所以需要存储运算后的值,以便进行下一层运算 

5.3 字符串转换-类型转换函数

同时在本次计算中需补充以下知识:

1.字符串转为长长整数:

std::stoll(str);

2.字符串转换为整数:

std::stoi(str);

3.字符串转换为浮点数

std::stof(str);

4.数字转换为字符串

std::to_string(num);

5.通用:将一个类型转换为另一个类型

double num1 = 3.14;
int num2 = static_cast<int>(num1);

6.滑动窗口最大值239

6.1 题目与代码

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

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

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

class Solution {
private:
    class MyDeque{
        //引入双端队列deque
    public:
        deque<int> que;
        void pop(int value){
            //只有在队列存在元素和当前弹出元素是队首元素(最大值)才可以弹出/才有必要进行弹出操作(有可能前面的已经被push的操作弹出掉了)
            if(!que.empty() && value == que.front()){
                que.pop_front();//弹出队首
            }
        }

        void push(int value){
            //在存入的时候,要保证自己比前面的都要小,所以要把比自己还小的全部弹出
            while(!que.empty() && que.back() < value){
                //注意是比之前的数,这样才能一个个递推
                que.pop_back();
            }
            que.push_back(value);//加入到字符串的某尾
        }

        int front(){
            //获得最大值--最前面(左边)的元素
            return que.front();
        }
              
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyDeque que;//定义双端队列
        vector<int> result;//定义返回的数组容器
        for(int i = 0; i < k ;i++){
            //先存入一窗口的值
            que.push(nums[i]);
        }
        result.push_back(que.front());
        //之后就是重复操作,弹出和存入,和判断最大值
        for(int i = k;i<nums.size();i++){
            que.pop(nums[i-k]);//弹出当前窗口最前面的数
            que.push(nums[i]);//存入当前的数
            result.push_back(que.front());
        }
        return result;
    }
};

6.2 详细题解

这题主要用双端队列来模拟窗口移动的效果。思路是每次移动,保证当前队列的头部元素为最大,除非到了需要删除的时候(窗口移动到要pop弹出了那个位置了)才会删除,否则是存放在队列中和另外的元素相比较得到最大值。最终将每次移动得到的最大值存储下来。

定义了单调队列的类,也就是队列中始终保证从大至小,使得可以随时获取当前的窗口的最大值。同时注意定义类的结尾一定要加上;分号。

7.前K个高频元素347

7.1 题目与代码

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

提示:

1 <= nums.length <= 105
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
 

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

class Solution {
public:
    //定义小顶堆的运算规则,优先队列本身等于大顶堆,可以看成是二叉树,最大的数为根节点
    class comperation{
        public:
        bool operator()(const pair<int,int>& left,const pair<int,int>& right){
            //定义pair容器传入一组参数,且定义元素为常量引用类型,提升速度
            return left.second>right.second;//排序规则是左大于右,说明顺序是升序,最小的数为根节点
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> Datamap;//定义无序map容器,提高运行效率
        for(int i =0;i<nums.size();i++){
            Datamap[nums[i]]++;//统计出现次数
        }
        //全部存入后定义小顶堆
        priority_queue<pair<int,int>,vector<pair<int,int>>,comperation> smallqueue;
        //遍历存入<int,int>元素
        for(unordered_map<int,int>::iterator it = Datamap.begin();it!= Datamap.end();it++){
            //设置迭代器访问元素--end()为最后一个键值对的后一个正向迭代器
            smallqueue.push(*it);//传入时,遵循运算规则
            if(smallqueue.size()>k){
                smallqueue.pop();
            }
        }
        
        vector<int> result(k);
        for(int i = k-1;i>=0;i--){
            result[i]=smallqueue.top().first;
            smallqueue.pop();
        }
        
        return result;

    }
};

7.2 无序map -- unordered_map

7.2.1 无序map定义与操作概念

首先要引入无序map的定义与操作概念:

unordered_map 容器,直译过来就是"无序 map 容器"的意思。所谓“无序”,指的是 unordered_map 容器不会像 map 容器那样对存储的数据进行排序。换句话说,unordered_map 容器和 map 容器仅有一点不同,即 map 容器中存储的数据是有序的,而 unordered_map 容器中是无序的。

具体来讲,unordered_map 容器和 map 容器一样,以键值对(pair类型--含有first和second两个元素)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。

7.2.2 无序map操作

关于操作方面:

  • 1.创建无序map:unordered_map<int,int> Datamap;
  • 2.访问就直接通过datmap[Key]就可以获取到对应的value
  • 3.begin()    返回指向容器中第一个键值对的正向迭代器。
  • 4.end()     返回指向容器中最后一个键值对之后位置的正向迭代器。
  • 5.empty()    若容器为空,则返回 true;否则 false。
  • 6.size()    返回当前容器中存有键值对的个数。

7.3 详细题解

需要先统计每个元素出现的次数,这里使用无序map实现,在统计次数之后,还需要有序的存入容器中,使得可以按从大到小的顺序保存前k个元素。主要是排序这里,不论是冒泡排序还是快排函数,其时间复杂度再加上遍历保存的时间复杂度都会劣于O(n log n),所以我们使用了优先级队列因为二叉堆节点上浮和下浮的时间复杂度都是logn,而且插入的时候就会去排序(上浮、下浮)所以时间复杂度为O(n logn)。

这道题充分应用了优先级队列的知识,优先级队列实际上就是大顶堆,跟根节点始终为最大的二叉树类似,所以只需要重载相关的运算函数,将根节点变为最小的元素,利用优先级队列的top()函数和pop()函数配合,就可以将数据保存下来。

7.4  重要代码解释

1.bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }


这段代码定义了一个函数对象(Function Object),该对象可以用于比较两个pair<int, int>类型的对象。函数对象可以像函数一样被调用。

函数对象的名称是operator(),它接受两个参数,分别是const pair<int, int>& lhs和const pair<int, int>& rhs。这两个参数都是常量引用类型的pair<int, int>对象。

在C++中,operator()是函数调用运算符(Function Call Operator)的重载形式。它允许将一个对象像函数一样调用,重载了operator(),接受两个pair<int, int>类型的参数,并根据第二个元素的大小来进行比较。返回值为bool类型,表示第一个参数是否大于第二个参数。

使用函数对象作为比较器的好处是它可以在容器的排序、查找等算法中被灵活地使用。通过重载operator(),我们可以自定义元素的比较方式,以满足特定的需求。

函数体中的代码比较了lhs和rhs两个对象的second成员(即pair对象的第二个元素),使用大于号(>)进行比较。如果lhs.second大于rhs.second,则返回true;否则返回false。

在这段代码中,使用了const pair<int, int>& lhsconst pair<int, int>& rhs作为函数对象的参数类型,即常量引用类型。

使用引用作为参数类型的主要原因是避免对象的拷贝。当使用引用作为参数传递对象时,不会创建对象的副本,而是直接使用原始对象的引用。这可以提高代码的效率,特别是当对象较大或者拷贝操作较耗时时,可以避免不必要的开销。

mycomparison是一个自定义的比较函数对象类型。它可能类似于之前提到的bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs)的定义,用于确定在优先队列中如何排序pair<int, int>元素。

综上所述,这段代码创建了一个优先队列(priority_queue)对象pri_que,其中存储了一对具有相同类型的整数值的组合(pair<int, int>)。队列的元素按照特定的比较函数对象mycomparison来进行排序,并使用vector<pair<int, int>>作为底层容器存储这些元素。

2. for(unordered_map<int,int>::iterator it = Datamap.begin();it!= Datamap.end();it++){}

unordered_map<int, int>::iterator 是一个迭代器类型,用于遍历和访问unordered_map<int, int>的元素。

it++ 是循环体内的操作,将迭代器it向后移动一个位置,以便在下次循环中访问下一个元素。

整个循环的作用是遍历map中的每个键值对,并通过迭代器it来访问和操作每个键值对的数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

花火の云

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

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

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

打赏作者

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

抵扣说明:

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

余额充值