hot100——栈和队列

栈和队列的定义

栈和队列的区别:栈是先进后出/后进先出(LIFO),队列是先进先出(FIFO)

python

Python 没有专门的栈类,通常使用 列表(list) 来模拟栈,因为列表的 append()pop() 操作在末尾进行,时间复杂度 O(1),符合栈的 LIFO 特性。

python 复制代码
stack = []          # 创建一个空栈
stack.append(1)     # 压栈:元素添加到末尾
stack.append(2)
top = stack[-1]     # 查看栈顶元素(不弹出)
popped = stack.pop() # 弹栈:移除并返回末尾元素
is_empty = not stack  # 判断是否为空
size = len(stack)    # 获取栈大小

C++

C++ 标准库提供了**std::stack** 容器适配器,默认基于**deque** 实现,也可指定底层容器(如 vectorlist)。

需要包含头文件#include <stack>

cpp 复制代码
#include <stack>
using namespace std;

stack<int> st;       // 创建一个存储 int 的空栈
st.push(1);          // 压栈
st.push(2);
int top = st.top();  // 查看栈顶元素(不弹出)
st.pop();            // 弹栈(无返回值)
bool empty = st.empty(); // 判断是否为空
int size = st.size();    // 获取栈大小

队列

python

Python 中可以使用 collections.deque(双端队列)来实现高效队列,因为列表的 pop(0) 是 O(n) 操作,不适合做队列。dequeappend()popleft() 都是 O(1)

python 复制代码
from collections import deque

q = deque()          # 创建一个空队列
q.append(1)          # 入队:从右端添加
q.append(2)
front = q[0]         # 查看队首元素(不弹出)
popped = q.popleft() # 出队:从左端移除并返回
is_empty = not q     # 判断是否为空
size = len(q)        # 获取队列大小

注意deque 也可以从左边添加 appendleft,从右边弹出 pop,实现双端队列。

C++

C++ 标准库提供了 std::queue 容器适配器,默认基于 deque 实现。

需要包含头文件#include <queue>

cpp 复制代码
#include <queue>
using namespace std;

queue<int> q;        // 创建一个存储 int 的空队列
q.push(1);           // 入队:添加到末尾
q.push(2);
int front = q.front(); // 查看队首元素(不弹出)
int back = q.back();   // 查看队尾元素(不弹出)
q.pop();             // 出队:移除队首元素(无返回值)
bool empty = q.empty(); // 判断是否为空
int size = q.size();    // 获取队列大小

双向队列(deque)

C++

是的,deque双端队列(double-ended queue) 的缩写。它支持在头部和尾部高效地插入和删除元素(时间复杂度 O(1)),同时也能通过下标随机访问元素。

不过要注意:

  • 双端队列是一种逻辑上的数据结构,强调的是"两端可操作"。

  • 双向链表 (如 std::list)则是另一种数据结构,强调的是"每个节点有前后指针,可在任意位置快速插入/删除"。

在 C++ 中,std::deque 底层是分段连续数组(不是链表),因此它既能双端操作,又能随机访问;而**std::list 才是真正的双向链表,不能随机访问**。

所以回答你的问题:deque 是双端队列,但不是双向链表。如果你需要"双向队列"这个词的准确定义,它就是双端队列。

python

  • 从右端添加append(x) ------ 类似 push_back 或栈的 push

  • 从左端添加appendleft(x) ------ 类似 push_front

  • 从右端弹出pop() ------ 类似栈的 pop 或队列的出队(从右)。

  • 从左端弹出popleft() ------ 类似队列的出队(从左)

python 复制代码
from collections import deque
q = deque()
q.append(1)        # 右端添加
q.appendleft(2)    # 左端添加
right = q.pop()    # 右端弹出
left = q.popleft() # 左端弹出

底层实现

  • Python :栈使用 list;队列使用 collections.deque(双端队列)或 queue.Queue(线程安全,但较慢)。

  • C++stackqueue 都是容器适配器,底层默认用 deque,也可以手动指定 vectorlist。例如:

cpp 复制代码
stack<int, vector<int>> st;   // 用 vector 作为底层容器
queue<int, list<int>> q;      // 用 list 作为底层容器

push以及push_back的使用(C++)

顺序容器(vector, deque, list)

顺序容器是直接管理元素存储的数据结构,它们提供:

  • 对元素的直接访问(如迭代器、下标等)

  • 插入、删除等操作(可能特定位置效率不同)

  • 底层内存管理由容器自身负责

C++ 中的顺序容器包括:

  • vector:动态数组,尾部插入快,支持随机访问。

  • deque:双端队列,两端插入/删除快,也支持随机访问(但底层是分段连续存储,并非链表)。

  • list:双向链表,任意位置插入/删除快,不支持随机访问。

  • array:固定大小数组。

  • forward_list:单向链表。

deque 是顺序容器 ,因为它直接管理元素存储,并提供了完整的容器接口(如 begin()end()push_backpush_frontoperator[] 等),底层虽然是分段连续内存,但对用户透明。

这些容器支持在尾部 添加元素,使用 push_back;有些还支持在头部 添加,使用 push_front(如 dequelist)。

cpp 复制代码
#include <vector>
#include <deque>
#include <list>

std::vector<int> vec;
vec.push_back(1);   // 尾部添加

std::deque<int> dq;
dq.push_back(1);    // 尾部
dq.push_front(2);   // 头部

std::list<int> lst;
lst.push_back(1);
lst.push_front(2);

为什么叫 push_back 因为"back"指容器的末尾,这些容器是线性序列,强调前后位置。

容器适配器(stack, queue, priority_queue)

容器适配器不是独立的容器 ,而是对底层容器(默认是 deque)的接口进行封装和限制,提供特定的行为(如栈、队列)。它们不提供直接的元素访问或迭代器,只提供受限的操作:

  • stack:后进先出(LIFO),只能从一端操作。

  • queue:先进先出(FIFO),只能从一端入、另一端出。

  • priority_queue:按优先级出队,通常用堆实现。

适配器的底层容器可以是任何支持所需操作的顺序容器(如 vectordequelist),默认是 deque。例如:

cpp 复制代码
stack<int> s1;                // 默认使用 deque
stack<int, vector<int>> s2;   // 使用 vector 作为底层容器

适配器不提供迭代器、不提供 begin()/end(),因为它们的语义不允许遍历。

deque不是适配器

因为 deque 是一个完整功能的容器,它:

  • 支持随机访问([] 操作符)

  • 支持在两端高效插入删除(push_front/pop_front/push_back/pop_back

  • 提供迭代器,可以遍历所有元素

适配器(如 queue 故意隐藏 了这些功能,只暴露符合队列语义的操作(pushpopfrontback),防止用户误用。所以 deque 是基础工具,适配器是基于它(或其他容器)构建的"限制版"接口。

这些适配器提供的是栈、队列 的语义,而不是直接操作底层容器的"前后"。它们使用统一的 push 方法表示**"插入一个元素"**。

cpp 复制代码
#include <stack>
#include <queue>

std::stack<int> st;
st.push(1);   // 压栈

std::queue<int> q;
q.push(1);    // 入队

std::priority_queue<int> pq;
pq.push(1);   // 插入优先队列

为什么叫 push 因为栈和队列的经典操作名就是 push(压入)和 pop(弹出),不强调前后。

常见错误示例

cpp 复制代码
std::stack<int> st;
st.push_back(1);   // ❌ 错误!stack 没有 push_back
st.push(1);        // ✅ 正确

std::vector<int> vec;
vec.push(1);       // ❌ 错误!vector 没有 push
vec.push_back(1);  // ✅ 正确

20.有效的括号

解题思路

本题判断有效括号,核心思路就是要用到栈。

为了防止当栈为空的时候,pop()方法会报错,我们事先在栈中放入"?"元素来避免这种情况。

如果是左括号:( 、[ 、{ ,则压入堆栈中;

如果是右括号: )、]、 },

如果栈为空,则说明栈中没有与之相对应的括号,返回false;

反之则将栈顶元素在字典中所对应的值与之相对应。如若不符,则返回false;反之则弹出栈顶元素,继续遍历当前字符串。

遍历结束后,如果栈的大小为1,说明所有符号都已经匹配上,反之则存在没有匹配上的,返回false。

代码

python

python 复制代码
class Solution(object):
    def isValid(self, s):
        """
        :type s: str
        :rtype: bool
        """
        dict = {'{':'}', '[':']', '(':')', '?':'?'}
        stack = ['?']
        for c in s:
            if c in dict:
                stack.append(c)
            elif dict[stack.pop()] != c:
                return False
        return len(stack) == 1

C++

cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        unordered_map<char, char> tmp{{'{', '}'}, {'[', ']'}, {'(', ')'}, {'?', '?'}};
        stack<char> st;
        st.push('?');
        // 先把?存入栈中,避免后续栈为空出现pop而后报错的情况
        for(char c : s)
        {
            // c确实在栈中
            if(tmp.count(c))
                st.push(c);
            else
            {
                // c和栈顶元素在字典当中所对应的值是否相同?
                if(tmp[st.top()] != c)
                    return false;
                // 相同则弹出栈顶元素
                st.pop();
            }
        }
        // 如果都匹配的话,最终栈中只有?这一个元素
        return st.size() == 1;
    }
};

参考

有效的括号(辅助栈法,极简+图解)

155.最小栈

解题思路

辅助栈

核心就是在原始数据栈的基础上加了一个辅助栈,辅助栈里面存的就是当前数据栈中的最小值(其实就是辅助栈就是一个从小到大的单调栈,栈顶是最小的元素)。

那么具体规则如下:

1.void push(int val) 将元素val推入堆栈:

将val存入数据栈中,同时需要看辅助栈是否为空,或者该值是否小于等于辅助栈的栈顶元素,如果是,则存入辅助栈中;

2.void pop() 删除堆栈顶部的元素:

在数据栈中弹出栈顶元素,几位top_val,如果top_val也等于辅助栈的栈顶元素,那么辅助栈也弹出该元素;

3.int top() 获取堆栈顶部的元素:

直接返回数据栈的栈顶元素即可;

4.int getMin() 获取堆栈中的最小元素

直接返回辅助栈中的最小值即可。

这种同步机制,确保了辅助栈的栈顶始终与当前数据栈的状态同步,记录着当前的最小值,从而让 getMin() 可以在 O(1) 时间内完成。

代码

python

python 复制代码
class MinStack(object):

    def __init__(self):
        self.st = []  # 数据栈
        self.minst = [] # 辅助栈

    def push(self, val):
        """
        :type val: int
        :rtype: None
        """
        self.st.append(val)
        # 当辅助栈为空,或者当前值小于辅助栈的最小值则将该值同样压入辅助栈中
        if not self.minst or val <= self.minst[-1]:
            self.minst.append(val)

    def pop(self):
        """
        :rtype: None
        """
        if self.st:
            top_val = self.st.pop()
            # 如果当前值同样也是辅助栈中的最小值,则辅助栈也需要将该值弹出
            if top_val == self.minst[-1]:
                self.minst.pop()
        
    def top(self):
        """
        :rtype: int
        """
        return self.st[-1] if self.st else None

    def getMin(self):
        """
        :rtype: int
        """
        return self.minst[-1] if self.minst else None


# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(val)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()

C++

cpp 复制代码
class MinStack {
    stack<int> st;
    stack<int> minst;
public:
    MinStack() {
        
    }
    
    void push(int val) {
        st.push(val);
        // 如果minst为空或者当前值小于等于辅助栈的栈顶值,则存入辅助栈
        if(minst.empty() || val <= minst.top())
            minst.push(val);
    }
    
    void pop() {
        int val = st.top();
        st.pop();
        // 如果数据栈弹出的值和辅助栈的栈顶元素相同,则辅助站也要弹出该值
        if(val==minst.top())
            minst.pop();
    }
    
    int top() {
        return st.top();
    }
    
    int getMin() {
        return minst.top();
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj->push(val);
 * obj->pop();
 * int param_3 = obj->top();
 * int param_4 = obj->getMin();
 */

394.字符串解码

解题思路

双栈法

我们用两个栈来解决这个问题,一个是数字栈,一个是字符串栈,其中数字栈存储的是当前累积的数字,而字符串中存储的是当前累积的字符串。

当遇到 ' [ ' 的时候,表示一个嵌套的开始,需要把当前的数字以及字符都压入栈中,重新计数当前数字以及字符,处理内层内容;当遇到 ' ] ' 的时候,表示一个嵌套的结束,需要把之前压入栈中数字以及字符弹出栈,然后将当前字符串重复当前数字次,再拼接到弹出的字符串之后,形成新的当前字符串。

这样当扫描完整个字符串,当前字符串结果就是最终的解码结果。

代码

python

python 复制代码
class Solution(object):
    def decodeString(self, s):
        """
        :type s: str
        :rtype: str
        """
        num_stack = []
        str_stack = []
        cur_num = 0
        cur_str = ""

        for ch in s:
            # 当前字符是数字
            if ch.isdigit():
                cur_num = cur_num * 10 + int(ch)
            # 当前字符是[,代表嵌套开始,将当前数字以及字符串都压入栈,然后重置
            elif ch == '[':
                num_stack.append(cur_num)
                str_stack.append(cur_str)
                cur_num = 0
                cur_str = ""
            # 代表嵌套结束,将当前字符串重复先前数字次,然后拼接到先前的字符串之后
            elif ch == ']':
                prev_num = num_stack.pop()
                prev_str = str_stack.pop()
                cur_str = prev_str + cur_str * prev_num
            else:
                cur_str += ch
        return cur_str

C++

cpp 复制代码
class Solution {
public:
    string decodeString(string s) {
        stack<int> num_stack;
        stack<string> str_stack;
        int cur_num = 0;
        string cur_str = "";
        for(char ch : s)
        {
            // 如果当前字符为数字,则存入当前数字中
            if(isdigit(ch))
                cur_num = cur_num * 10 + (ch - '0');
            // 如果当前字符是'[',则将当前字符串以及数字进入压栈,并重置当前字符串以及数字
            else if(ch == '[')
            {
                num_stack.push(cur_num);
                str_stack.push(cur_str);
                cur_num = 0;
                cur_str = "";
            }
            // 嵌套结束,开始出栈,将当前字符串重复先前数字次数,而后再拼接到先前字符串之前
            else if(ch == ']')
            {
                int prev_num = num_stack.top();num_stack.pop();
                string prev_str = str_stack.top();str_stack.pop();
                //cur_str = prev_str + cur_str * prev_num;
                // 注意C++中没有python那样的直接乘法,需要不断循环追加
                for(int i = 0; i< prev_num;i++)
                    prev_str += cur_str;
                cur_str = prev_str;
            }
            else{
                cur_str += ch;
            }
        }
        return cur_str;
    }
};

239.滑动窗口最大值

解题思路

本题使用单调队列:

以图上例子举例:

nums = [1, 3, -1, -3 , 5 , 3, 6, 7], k = 3

每次移动窗口,我们要pop()窗口左边的元素,然后push()窗口右边的元素,并且用getMaxValue()得到窗口的最大值。我们要实现的就是这么三个函数。

如果使用暴力解法,对每一个窗口都再进行遍历的话,那么时间复杂度就是o(窗口数目 * k),时间复杂度比较高。

那么如果是使用优先级队列的话,priority_queue,那么第一个窗口[1,3,-1],该窗口的最大值是3,在优先级队列当中是[3,1,-1],当窗口向后移动的时候,我们需要把1给pop,但是此时优先级队列中1在中间,无法直接pop出来,所以这个题目优先级队列不太适配。

因此我们使用单调队列,然后自定义上述三个函数,具体思路如下:

首先我们模拟上述例子的全过程:

nums = [1, 3, -1, -3 , 5 , 3, 6, 7], k = 3

1,3,-1\] 最大值3,单调队列中存储的是最大值,当压入队列当中的值比队列末尾的值要大,队列末尾的值则弹出,也即单调队列最左边一直都是窗口的最大值,所以此时单调队列是\[3, -1

3, -1, -3\],-3\<-1,因此单调队列不需要pop()操作,-3压入单调队列即可\[3, -1, -3\],此时的最大值还是3; \[-1, -3, 5\], 5大于此前单调队列中的所有值,因此单调队列pop()掉此前单调队列当中的所有值,此时单调队列中为\[\],压入5,最大值为5,单调队列为\[5\]; \[-3, 5, 3\], 3 \< 5,单调队列不需要pop(),3压入单调队列当中,此时的单调队列\[5, 3\],最大值是5; \[5, 3, 6\],6大于单调队列中的所有元素,因此单调队列需要pop()所有元素,处理之后单调队列为\[\],而后压入push()6,单调队列为\[6\],最大值是6; \[3, 6, 7\],7大于单调队列中的所有元素,因此单调队列需要pop()所有元素,处理之后单调队列为\[\],而后压入push()7,单调队列为\[7\],最大值是7; 所以结果是\[3, 3, 5, 5, 6, 7\],和图上结果一致; #### pop() 如果当前需要移除的值等于单调队列的最大值,则执行pop操作; #### push() 在单调队列当中,队列从左到右按递减次序进行排列,而后窗口向后移动,当要压入的值大于单调队列末尾的值时,需要pop()掉单调队列队尾的值,在压入当前值; #### getMaxValue() 返回单调队列的首部的值即可。 ### 代码 #### python ```python class Solution(object): def maxSlidingWindow(self, nums, k): """ :type nums: List[int] :type k: int :rtype: List[int] """ # ans = [0] * (len(nums) -k + 1) ans = [] dq = deque() for i in range(len(nums)): # pop 当前值比单调队列已有值都大则弹出单调队列的比该值小的值 while dq and nums[i] > nums[dq[-1]]: dq.pop() # 存下标 dq.append(i) # push # 窗口左边元素是i-k+1,但先前窗口左边元素是i-k, # 当nums[i-k]恰好是单调队列的最大值时,单调队列也需要弹出该值 if i-k == dq[0]: dq.popleft() if i >= k - 1: # ans[i - k + 1] = nums[dq[0]] ans.append(nums[dq[0]]) return ans ``` #### C++ ```cpp class Solution { public: vector maxSlidingWindow(vector& nums, int k) { deque dq;// 单调队列当中存储的下标索引 //vector ans(nums.size() - k + 1); vector ans; for(int i =0;inums[dq.back()]) dq.pop_back(); dq.push_back(i);// 保存下标 // pop() // 当前窗口往右移动的时候,要移除的元素刚刚好是单调队列的最大值的时候,需要pop // 窗口的左边元素的索引是i-k+1,要移除的元素的索引是i-k if(i - k == dq.front()) dq.pop_front(); // getMaxvakue if(i>=k-1) { //ans[i-k+1] = nums[dq.front()]; ans.push_back(nums[dq.front()]); } } return ans; } }; ``` ### 参考 [降本增笑,秒懂单调队列!附题单!(Python/Java/C++/C/Go/JS/Rust)](https://leetcode.cn/problems/sliding-window-maximum/solutions/2499715/shi-pin-yi-ge-shi-pin-miao-dong-dan-diao-ezj6 "降本增笑,秒懂单调队列!附题单!(Python/Java/C++/C/Go/JS/Rust)")

相关推荐
Meme Buoy4 小时前
18.补充数学1:生成树-最短路径-最大流量-线性规划
数据结构·算法
汀、人工智能4 小时前
[特殊字符] 第89课:岛屿数量
数据结构·算法·数据库架构·图论·bfs·岛屿数量
九英里路4 小时前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
2401_892070985 小时前
顺序栈(动态数组实现) 超详细解析(C++ 语言 + 可直接运行)
数据结构·c++·顺序栈
漫霂5 小时前
二叉树的翻转
java·数据结构·算法
3秒一个大5 小时前
深入理解 JS 中的栈与堆:从内存模型到数据结构,再谈内存泄漏
前端·javascript·数据结构
旖-旎6 小时前
哈希表(存在重复元素)(3)
数据结构·c++·学习·算法·leetcode·散列表
计算机安禾6 小时前
【数据结构与算法】第39篇:图论(三):最小生成树——Prim算法与Kruskal算法
开发语言·数据结构·c++·算法·排序算法·图论·visual studio code
汀、人工智能6 小时前
[特殊字符] 第77课:最长递增子序列
数据结构·算法·数据库架构·图论·bfs·最长递增子序列