栈和堆理论基础

文章目录

  • 栈和堆理论基础
    • [1. 栈(Stack)的基本概念](#1. 栈(Stack)的基本概念)
      • [1.1 基本术语](#1.1 基本术语)
      • [1.2 栈的特点](#1.2 栈的特点)
      • [1.3 C++中栈的使用](#1.3 C++中栈的使用)
    • [2. 队列(Queue)的基本概念](#2. 队列(Queue)的基本概念)
      • [2.1 基本术语](#2.1 基本术语)
      • [2.2 队列的特点](#2.2 队列的特点)
      • [2.3 C++中队列的使用](#2.3 C++中队列的使用)
    • [3. 堆(Heap/Priority Queue)的基本概念](#3. 堆(Heap/Priority Queue)的基本概念)
      • [3.1 基本术语](#3.1 基本术语)
      • [3.2 堆的特点](#3.2 堆的特点)
      • [3.3 C++中堆的使用](#3.3 C++中堆的使用)
    • [4. 栈的应用模板](#4. 栈的应用模板)
      • [4.1 有效的括号](#4.1 有效的括号)
      • [4.2 删除字符串中的所有相邻重复项](#4.2 删除字符串中的所有相邻重复项)
      • [4.3 逆波兰表达式求值](#4.3 逆波兰表达式求值)
      • [4.4 最小栈](#4.4 最小栈)
      • [4.5 字符串解码](#4.5 字符串解码)
    • [5. 栈和队列的相互实现](#5. 栈和队列的相互实现)
      • [5.1 用栈实现队列](#5.1 用栈实现队列)
      • [5.2 用队列实现栈](#5.2 用队列实现栈)
    • [6. 单调栈模板](#6. 单调栈模板)
      • [6.1 每日温度](#6.1 每日温度)
      • [6.2 接雨水](#6.2 接雨水)
      • [6.3 柱状图中最大矩形](#6.3 柱状图中最大矩形)
      • [6.4 单调栈总结](#6.4 单调栈总结)
    • [7. 单调队列模板](#7. 单调队列模板)
      • [7.1 滑动窗口最大值](#7.1 滑动窗口最大值)
    • [8. 堆(优先队列)的应用模板](#8. 堆(优先队列)的应用模板)
      • [8.1 前K个高频元素](#8.1 前K个高频元素)
    • [9. 栈和队列的时间复杂度](#9. 栈和队列的时间复杂度)
      • [9.1 时间复杂度分析](#9.1 时间复杂度分析)
      • [9.2 空间复杂度分析](#9.2 空间复杂度分析)
    • [10. 何时使用栈、队列和堆](#10. 何时使用栈、队列和堆)
      • [10.1 使用栈的场景](#10.1 使用栈的场景)
      • [10.2 使用队列的场景](#10.2 使用队列的场景)
      • [10.3 使用堆的场景](#10.3 使用堆的场景)
      • [10.4 判断标准](#10.4 判断标准)
    • [11. 栈、队列和堆的优缺点](#11. 栈、队列和堆的优缺点)
      • [11.1 栈的优缺点](#11.1 栈的优缺点)
      • [11.2 队列的优缺点](#11.2 队列的优缺点)
      • [11.3 堆的优缺点](#11.3 堆的优缺点)
    • [12. 常见题型总结](#12. 常见题型总结)
      • [12.1 栈的基础应用类](#12.1 栈的基础应用类)
      • [12.2 栈和队列的相互实现](#12.2 栈和队列的相互实现)
      • [12.3 单调栈类](#12.3 单调栈类)
      • [12.4 单调队列类](#12.4 单调队列类)
      • [12.5 堆(优先队列)类](#12.5 堆(优先队列)类)
    • [13. 总结](#13. 总结)

栈和堆理论基础

1. 栈(Stack)的基本概念

**栈(Stack)**是一种后进先出(LIFO - Last In First Out)的线性数据结构。

1.1 基本术语

  • 栈顶(Top):栈中允许插入和删除的一端
  • 栈底(Bottom):栈中不允许插入和删除的一端
  • 入栈(Push):向栈顶插入元素
  • 出栈(Pop):从栈顶删除元素
  • 栈空(Empty):栈中没有任何元素
  • 栈满(Full):栈中元素达到最大容量

1.2 栈的特点

  • 后进先出(LIFO):最后进入的元素最先被取出
  • 只能在一端操作:只能在栈顶进行插入和删除
  • 线性结构:元素之间是一对一的关系
  • 动态大小:可以根据需要动态调整(使用动态数组或链表实现)

示例

复制代码
栈的操作过程:

初始:    []
push(1):  [1]
push(2):  [1, 2]
push(3):  [1, 2, 3]
pop():    [1, 2]     返回3
pop():    [1]        返回2
pop():    []         返回1

1.3 C++中栈的使用

C++标准库中的栈

cpp 复制代码
#include <stack>

stack<int> st;

// 基本操作
st.push(x);      // 入栈
st.pop();        // 出栈(不返回值)
st.top();        // 获取栈顶元素(不删除)
st.empty();      // 判断栈是否为空
st.size();       // 获取栈中元素个数

注意

  • pop() 不返回值,需要先 top()pop()
  • top()pop() 前需要检查栈是否为空

2. 队列(Queue)的基本概念

**队列(Queue)**是一种先进先出(FIFO - First In First Out)的线性数据结构。

2.1 基本术语

  • 队头(Front):队列中允许删除的一端
  • 队尾(Rear):队列中允许插入的一端
  • 入队(Enqueue):向队尾插入元素
  • 出队(Dequeue):从队头删除元素
  • 队空(Empty):队列中没有任何元素
  • 队满(Full):队列中元素达到最大容量

2.2 队列的特点

  • 先进先出(FIFO):最先进入的元素最先被取出
  • 两端操作:在队尾插入,在队头删除
  • 线性结构:元素之间是一对一的关系
  • 动态大小:可以根据需要动态调整

示例

复制代码
队列的操作过程:

初始:    []
enqueue(1): [1]
enqueue(2): [1, 2]
enqueue(3): [1, 2, 3]
dequeue():  [2, 3]     返回1
dequeue():  [3]        返回2
dequeue():  []         返回3

2.3 C++中队列的使用

C++标准库中的队列

cpp 复制代码
#include <queue>

queue<int> que;

// 基本操作
que.push(x);      // 入队
que.pop();        // 出队(不返回值)
que.front();      // 获取队头元素(不删除)
que.back();       // 获取队尾元素(不删除)
que.empty();      // 判断队列是否为空
que.size();       // 获取队列中元素个数

双端队列(deque)

cpp 复制代码
#include <deque>

deque<int> dq;

// 基本操作
dq.push_front(x);  // 在队头插入
dq.push_back(x);   // 在队尾插入
dq.pop_front();    // 删除队头元素
dq.pop_back();     // 删除队尾元素
dq.front();        // 获取队头元素
dq.back();         // 获取队尾元素

3. 堆(Heap/Priority Queue)的基本概念

**堆(Heap)**是一种特殊的完全二叉树,满足堆序性质。在C++中通常使用优先队列(priority_queue)实现。

3.1 基本术语

  • 大顶堆(Max Heap):父节点的值大于等于子节点的值
  • 小顶堆(Min Heap):父节点的值小于等于子节点的值
  • 堆顶(Top):堆的根节点,大顶堆中是最大值,小顶堆中是最小值
  • 堆化(Heapify):调整堆使其满足堆序性质

3.2 堆的特点

  • 完全二叉树:除了最后一层,其他层都是满的,最后一层从左到右填充
  • 堆序性质:父节点和子节点之间满足大小关系
  • 快速访问最值:可以在O(1)时间内获取最大值或最小值
  • 动态调整:插入和删除的时间复杂度为O(log n)

示例

复制代码
大顶堆:
        10
       /  \
      8    9
     / \  / \
    5  6 7   8

小顶堆:
        1
       /  \
      3    2
     / \  / \
    5  4 6   7

3.3 C++中堆的使用

C++标准库中的优先队列

cpp 复制代码
#include <queue>

// 大顶堆(默认)
priority_queue<int> pq;

// 小顶堆
priority_queue<int, vector<int>, greater<int>> pq_min;

// 自定义比较器
class mycomparison {
public:
    bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
        return lhs.second > rhs.second;  // 小顶堆
    }
};
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;

// 基本操作
pq.push(x);      // 插入元素
pq.pop();        // 删除堆顶元素(不返回值)
pq.top();        // 获取堆顶元素(不删除)
pq.empty();      // 判断堆是否为空
pq.size();       // 获取堆中元素个数

4. 栈的应用模板

4.1 有效的括号

适用场景:判断字符串中的括号是否匹配

核心思路

  • 遇到左括号,将对应的右括号入栈
  • 遇到右括号,检查是否与栈顶匹配
  • 最后检查栈是否为空

模板代码

cpp 复制代码
// LeetCode 20. 有效的括号
class Solution {
public:
    bool isValid(string s) {
        if (s.size() % 2 != 0) return false;  // 奇数长度一定不匹配
        
        stack<char> st;
        
        for (int i = 0; i < s.size(); i++) {
            // 遇到左括号,将对应的右括号入栈
            if (s[i] == '(') st.push(')');
            else if (s[i] == '[') st.push(']');
            else if (s[i] == '{') st.push('}');
            // 遇到右括号,检查是否匹配
            else if (st.empty() || s[i] != st.top()) {
                return false;  // 不匹配或右括号多了
            } else {
                st.pop();  // 匹配,弹出
            }
        }
        
        return st.empty();  // 栈为空说明全部匹配
    }
};

关键点

  • 遇到左括号入栈对应的右括号
  • 遇到右括号检查栈顶是否匹配
  • 最后检查栈是否为空

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

适用场景:删除字符串中所有相邻的重复字符

核心思路

  • 遍历字符串,如果栈为空或当前字符与栈顶不同,入栈
  • 如果当前字符与栈顶相同,出栈
  • 最后将栈中元素反转输出

模板代码

cpp 复制代码
// LeetCode 1047. 删除字符串中的所有相邻重复项
class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> st;
        
        for (char c : s) {
            if (st.empty() || c != st.top()) {
                st.push(c);
            } else {
                st.pop();  // 相邻重复,删除
            }
        }
        
        // 将栈中元素反转输出
        string result = "";
        while (!st.empty()) {
            result += st.top();
            st.pop();
        }
        reverse(result.begin(), result.end());
        
        return result;
    }
};

关键点

  • 相邻重复判断:c == st.top()
  • 结果需要反转:栈是后进先出

4.3 逆波兰表达式求值

适用场景:计算逆波兰表达式(后缀表达式)的值

核心思路

  • 遇到数字则入栈
  • 遇到运算符则取出栈顶两个数字进行计算,将结果压入栈中
  • 最后栈顶元素就是结果

模板代码

cpp 复制代码
// LeetCode 150. 逆波兰表达式求值
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        
        for (const string& token : tokens) {
            if (token == "+" || token == "-" || token == "*" || token == "/") {
                // 运算符:取出栈顶两个数字
                int b = st.top();
                st.pop();
                int a = st.top();
                st.pop();
                
                // 计算结果并入栈
                if (token == "+") st.push(a + b);
                else if (token == "-") st.push(a - b);
                else if (token == "*") st.push(a * b);
                else if (token == "/") st.push(a / b);
            } else {
                // 数字:直接入栈
                st.push(stoi(token));
            }
        }
        
        return st.top();
    }
};

关键点

  • 注意运算顺序:先出栈的是第二个操作数(b),后出栈的是第一个操作数(a)
  • 对于减法和除法:a - ba / b(不是 b - ab / a

4.4 最小栈

适用场景:设计一个支持获取最小元素的栈

核心思路

  • 使用两个栈:一个正常栈,一个最小值栈
  • 最小值栈存储每个状态下的最小值
  • 当弹出元素时,如果弹出的是最小值,也要从最小值栈弹出

模板代码

cpp 复制代码
// LeetCode 155. 最小栈
class MinStack {
private:
    stack<int> s;      // 正常栈
    stack<int> minS;   // 最小值栈

public:
    MinStack() {}

    void push(int val) {
        s.push(val);
        // 如果最小值栈为空,或者当前值小于等于最小值栈顶,则入栈
        if (minS.empty() || val <= minS.top()) {
            minS.push(val);
        }
    }

    void pop() {
        // 如果弹出的元素是最小值,也要从最小值栈弹出
        if (s.top() == minS.top()) {
            minS.pop();
        }
        s.pop();
    }

    int top() {
        return s.top();
    }

    int getMin() {
        return minS.top();
    }
};

关键点

  • 使用辅助栈:维护每个状态下的最小值
  • 相等时也要入栈:val <= minS.top(),保证最小值栈的完整性
  • 时间复杂度:所有操作都是O(1)

4.5 字符串解码

适用场景 :解码嵌套的字符串,如 3[a2[c]]accaccacc

核心思路

  • 使用两个栈:一个存储重复次数,一个存储字符串
  • 遇到数字:构造多位数k
  • 遇到[:保存当前k和字符串,重置
  • 遇到]:解码当前层,重复k次后拼接到上一层字符串

模板代码

cpp 复制代码
// LeetCode 394. 字符串解码
class Solution {
public:
    string decodeString(string s) {
        stack<int> countStack;      // 保存每一层的重复次数k
        stack<string> stringStack;  // 保存进入'['之前的字符串
        
        string curr = "";           // 当前正在构建的字符串
        int k = 0;                  // 当前层的重复次数(可能是多位数)
        
        for (char c : s) {
            // 1. 如果是数字,构造多位数k
            if (isdigit(c)) {
                k = k * 10 + (c - '0');
            }
            // 2. 遇到'[',进入新一层
            else if (c == '[') {
                countStack.push(k);      // 保存当前重复次数
                stringStack.push(curr);  // 保存当前字符串
                
                curr = "";               // 重置,开始构造子串
                k = 0;                   // 重置k,准备读下一个数字
            }
            // 3. 遇到']',解码当前层
            else if (c == ']') {
                string temp = curr;      // 当前层要重复的字符串
                
                curr = stringStack.top(); // 回到上一层字符串
                stringStack.pop();
                
                int repeat = countStack.top(); // 当前层重复次数
                countStack.pop();
                
                // 把temp重复repeat次拼到curr后面
                while (repeat--) {
                    curr += temp;
                }
            }
            // 4. 普通字符,直接加到当前字符串
            else {
                curr += c;
            }
        }
        
        return curr;
    }
};

关键点

  • 两个栈:分别存储重复次数和字符串
  • 多位数处理:k = k * 10 + (c - '0')
  • 嵌套处理:遇到[保存状态,遇到]恢复状态并重复

5. 栈和队列的相互实现

5.1 用栈实现队列

适用场景:使用两个栈实现队列的先进先出功能

核心思路

  • 使用两个栈:stIn(输入栈)和 stOut(输出栈)
  • push:直接压入 stIn
  • pop:如果 stOut 为空,将 stIn 中所有元素弹出并压入 stOut,然后从 stOut 弹出
  • peek:复用 pop,但需要将元素再压回去

模板代码

cpp 复制代码
// LeetCode 232. 用栈实现队列
class MyQueue {
public:
    stack<int> stIn;   // 输入栈
    stack<int> stOut;  // 输出栈
    
    MyQueue() {
        
    }
    
    void push(int x) {
        stIn.push(x);
    }
    
    int pop() {
        // 如果输出栈为空,将输入栈的元素全部转移到输出栈
        if (stOut.empty()) {
            while (!stIn.empty()) {
                stOut.push(stIn.top());
                stIn.pop();
            }
        }
        
        int result = stOut.top();
        stOut.pop();
        return result;
    }
    
    int peek() {
        int res = this->pop();  // 复用pop函数
        stOut.push(res);        // 因为pop弹出了元素,所以再添加回去
        return res;
    }
    
    bool empty() {
        return stIn.empty() && stOut.empty();
    }
};

关键点

  • 两个栈:输入栈和输出栈
  • 转移时机:输出栈为空时才转移
  • 时间复杂度:push O(1),poppeek 均摊 O(1)

5.2 用队列实现栈

适用场景:使用队列实现栈的后进先出功能

核心思路

  • 方法1:使用两个队列,一个主队列,一个辅助队列
  • 方法2:使用一个队列,将队列中除最后一个元素外的所有元素重新入队

模板代码(两个队列)

cpp 复制代码
// LeetCode 225. 用队列实现栈(方法1:两个队列)
class MyStack {
public:
    queue<int> que1;  // 主队列
    queue<int> que2;  // 辅助队列
    
    MyStack() {
        
    }
    
    void push(int x) {
        que1.push(x);
    }
    
    int pop() {
        int size = que1.size();
        size--;  // 保留最后一个元素
        
        // 将que1中除最后一个元素外的所有元素转移到que2
        while (size--) {
            que2.push(que1.front());
            que1.pop();
        }
        
        int result = que1.front();  // 最后一个元素就是栈顶
        que1.pop();
        que1 = que2;  // 将que2赋值给que1
        while (!que2.empty()) {
            que2.pop();  // 清空que2
        }
        
        return result;
    }
    
    int top() {
        return que1.back();  // 队列的最后一个元素就是栈顶
    }
    
    bool empty() {
        return que1.empty();
    }
};

模板代码(一个队列)

cpp 复制代码
// LeetCode 225. 用队列实现栈(方法2:一个队列)
class MyStack {
public:
    queue<int> que;
    
    MyStack() {
        
    }
    
    void push(int x) {
        que.push(x);
    }
    
    int pop() {
        int size = que.size();
        size--;  // 保留最后一个元素
        
        // 将队列中除最后一个元素外的所有元素重新入队
        while (size--) {
            que.push(que.front());
            que.pop();
        }
        
        int result = que.front();  // 此时队头就是栈顶
        que.pop();
        return result;
    }
    
    int top() {
        return que.back();  // 队列的最后一个元素就是栈顶
    }
    
    bool empty() {
        return que.empty();
    }
};

关键点

  • 两个队列:主队列和辅助队列,需要转移元素
  • 一个队列:将除最后一个元素外的所有元素重新入队
  • 推荐使用一个队列的方法,更简洁

6. 单调栈模板

6.1 每日温度

适用场景:找到每个元素右边第一个比它大的元素,计算距离

核心思路

  • 维护一个单调递减栈(从栈底到栈顶递减)
  • 遇到比栈顶大的元素,说明找到了栈顶元素的下一个更大元素
  • 计算距离并更新结果

模板代码

cpp 复制代码
// LeetCode 739. 每日温度
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        stack<int> st;  // 存储下标
        vector<int> result(temperatures.size(), 0);
        
        st.push(0);  // 初始化第一个元素
        
        for (int i = 1; i < temperatures.size(); i++) {
            if (temperatures[i] < temperatures[st.top()]) {
                // 当前温度小于栈顶,保持单调递减
                st.push(i);
            } else if (temperatures[i] == temperatures[st.top()]) {
                // 当前温度等于栈顶,也入栈
                st.push(i);
            } else {
                // 当前温度大于栈顶,处理栈顶元素
                while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
                    result[st.top()] = i - st.top();  // 计算距离
                    st.pop();
                }
                st.push(i);
            }
        }
        
        return result;
    }
};

关键点

  • 栈中存储下标:便于计算距离
  • 单调递减栈:从栈底到栈顶递减
  • 处理时机:遇到比栈顶大的元素时处理

6.2 接雨水

适用场景:计算柱状图中可以接的雨水总量

核心思路

  • 维护一个单调递减栈(从栈底到栈顶递减)
  • 遇到比栈顶高的柱子,可以形成凹槽
  • 计算凹槽的面积:宽度 × 高度

模板代码

cpp 复制代码
// LeetCode 42. 接雨水
class Solution {
public:
    int trap(vector<int>& height) {
        if (height.size() <= 2) return 0;
        
        stack<int> st;  // 存储下标
        st.push(0);
        
        int sum = 0;
        for (int i = 1; i < height.size(); i++) {
            if (height[i] < height[st.top()]) {
                // 当前柱子低于栈顶,保持单调递减
                st.push(i);
            } else if (height[i] == height[st.top()]) {
                // 当前柱子等于栈顶,也入栈
                st.push(i);
            } else {
                // 当前柱子高于栈顶,可以形成凹槽
                while (!st.empty() && height[i] > height[st.top()]) {
                    int mid = st.top();  // 凹槽底部
                    st.pop();
                    
                    if (!st.empty()) {
                        int left = st.top();   // 左边界
                        int right = i;         // 右边界
                        int width = right - left - 1;  // 宽度
                        int h = min(height[left], height[right]) - height[mid];  // 高度
                        sum += width * h;  // 累加面积
                    }
                }
                st.push(i);
            }
        }
        
        return sum;
    }
};

关键点

  • 单调递减栈:从栈底到栈顶递减
  • 凹槽计算:需要左边界、右边界和底部
  • 面积公式:width × h,其中 h = min(height[left], height[right]) - height[mid]

6.3 柱状图中最大矩形

适用场景:在柱状图中找到最大的矩形面积

核心思路

  • 使用单调递增栈(从栈底到栈顶递增)
  • 找到每个柱子左右两边第一个比它矮的柱子
  • 计算以当前柱子为高度的最大矩形面积

模板代码

cpp 复制代码
// LeetCode 84. 柱状图中最大矩形
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        // 在两端加0,作为哨兵
        heights.insert(heights.begin(), 0);
        heights.push_back(0);
        
        stack<int> st;  // 存下标
        int maxArea = 0;
        
        for (int i = 0; i < heights.size(); i++) {
            // 如果当前柱子比栈顶矮,说明栈顶柱子的右边界找到了
            while (!st.empty() && heights[i] < heights[st.top()]) {
                int h = heights[st.top()];  // 当前作为最矮柱的高度
                st.pop();
                
                int left = st.top();   // 左边第一个更矮柱
                int right = i;         // 右边第一个更矮柱
                
                int width = right - left - 1;
                maxArea = max(maxArea, h * width);
            }
            st.push(i);
        }
        
        return maxArea;
    }
};

关键点

  • 单调递增栈:从栈底到栈顶递增
  • 哨兵技巧:在两端加0,简化边界处理
  • 面积计算:h * width,其中width = right - left - 1

6.4 单调栈总结

核心思路

  • 维护一个单调递增或递减的栈
  • 用于快速找到每个元素左边或右边第一个比它大/小的元素

模板

cpp 复制代码
stack<int> st;  // 存储下标
st.push(0);     // 初始化第一个元素

for (int i = 1; i < n; i++) {
    // 根据题目需求选择:
    // 1. 单调递减栈:找下一个更大元素
    // 2. 单调递增栈:找下一个更小元素
    
    while (!st.empty() && nums[i] > nums[st.top()]) {
        // 处理栈顶元素
        result[st.top()] = i - st.top();
        st.pop();
    }
    st.push(i);
}

关键点

  • 栈中存储下标:便于计算距离和位置关系
  • 单调性维护:保持栈内元素单调递增或递减
  • 处理时机:遇到破坏单调性的元素时,处理栈顶元素
  • 哨兵技巧:在数组两端添加哨兵,简化边界处理

7. 单调队列模板

7.1 滑动窗口最大值

适用场景:找到滑动窗口中的最大值

核心思路

  • 使用双端队列(deque)实现单调队列
  • 维护一个单调递减队列(从队头到队尾递减)
  • 队列中只保留可能成为最大值的元素

模板代码

cpp 复制代码
// LeetCode 239. 滑动窗口最大值
class Solution {
private:
    class MyQueue {  // 单调队列(从大到小)
    public:
        deque<int> que;  // 使用deque来实现单调队列
        
        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值
        void pop(int value) {
            if (!que.empty() && value == que.front()) {
                que.pop_front();
            }
        }
        
        // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出
        // 这样就保持了队列里的数值是单调从大到小的了
        void push(int value) {
            while (!que.empty() && value > que.back()) {
                que.pop_back();
            }
            que.push_back(value);
        }
        
        // 查询当前队列里的最大值,直接返回队列前端也就是front就可以了
        int front() {
            return que.front();
        }
    };
    
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue que;
        vector<int> result;
        
        // 先将前k的元素放进队列
        for (int i = 0; i < k; i++) {
            que.push(nums[i]);
        }
        result.push_back(que.front());  // 记录前k的元素的最大值
        
        // 滑动窗口
        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]);        // 滑动窗口移除最前面元素
            que.push(nums[i]);           // 滑动窗口前加入最后面的元素
            result.push_back(que.front()); // 记录对应的最大值
        }
        
        return result;
    }
};

关键点

  • 使用 deque:支持两端操作
  • 单调递减队列:从队头到队尾递减
  • 维护窗口:只保留可能成为最大值的元素

8. 堆(优先队列)的应用模板

8.1 前K个高频元素

适用场景:找出数组中出现频率前K高的元素

核心思路

  • 使用哈希表统计每个元素的频率
  • 使用小顶堆(大小为K)维护前K个高频元素
  • 堆中元素按频率从小到大排序,堆顶是最小的频率

模板代码

cpp 复制代码
// LeetCode 347. 前K个高频元素
class Solution {
public:
    // 小顶堆比较器
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;  // 小顶堆:频率小的优先级高
        }
    };
    
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 统计元素出现频率
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }
        
        // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
        
        // 用固定大小为k的小顶堆,扫描所有频率的数值
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) {
                // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }
        
        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first;
            pri_que.pop();
        }
        
        return result;
    }
};

关键点

  • 小顶堆:堆顶是最小频率,便于维护前K个最大频率
  • 堆大小:始终保持为K,超过K时弹出堆顶
  • 结果反转:小顶堆先弹出最小的,需要倒序输出

9. 栈和队列的时间复杂度

9.1 时间复杂度分析

操作 队列 堆(优先队列)
插入 O(1) O(1) O(log n)
删除 O(1) O(1) O(log n)
查找最值 - - O(1)
查找元素 O(n) O(n) O(n)

9.2 空间复杂度分析

数据结构 空间复杂度 说明
O(n) n为栈中元素个数
队列 O(n) n为队列中元素个数
O(n) n为堆中元素个数

注意

  • 栈和队列的基本操作都是O(1)
  • 堆的插入和删除是O(log n),但查找最值是O(1)
  • 单调栈和单调队列的时间复杂度是O(n),每个元素最多入栈/入队一次,出栈/出队一次

10. 何时使用栈、队列和堆

10.1 使用栈的场景

  1. 括号匹配

    • 有效的括号
    • 括号生成
  2. 表达式求值

    • 逆波兰表达式求值
    • 中缀表达式转后缀表达式
  3. 相邻元素处理

    • 删除字符串中的所有相邻重复项
    • 去除重复字母
  4. 单调栈问题

    • 每日温度
    • 接雨水
    • 柱状图中最大矩形
  5. 递归转迭代

    • 二叉树的前中后序遍历(迭代实现)
    • 图的深度优先搜索(迭代实现)

10.2 使用队列的场景

  1. 广度优先搜索(BFS)

    • 二叉树的层序遍历
    • 图的广度优先搜索
    • 最短路径问题
  2. 滑动窗口

    • 滑动窗口最大值(使用单调队列)
    • 滑动窗口最小值
  3. 任务调度

    • 任务队列
    • 消息队列

10.3 使用堆的场景

  1. Top K问题

    • 前K个高频元素
    • 前K个最大元素
    • 前K个最小元素
  2. 合并K个有序序列

    • 合并K个升序链表
    • 合并K个有序数组
  3. 中位数问题

    • 数据流的中位数
    • 滑动窗口中位数
  4. 调度问题

    • CPU任务调度
    • 事件调度

10.4 判断标准

当遇到以下情况时,考虑使用栈

  • 需要后进先出的特性
  • 括号匹配、表达式求值
  • 需要找到左边/右边第一个更大/更小的元素(单调栈)

当遇到以下情况时,考虑使用队列

  • 需要先进先出的特性
  • 广度优先搜索
  • 滑动窗口问题(可能需要单调队列)

当遇到以下情况时,考虑使用堆

  • 需要快速获取最大值或最小值
  • Top K问题
  • 需要动态维护有序序列

11. 栈、队列和堆的优缺点

11.1 栈的优缺点

优点

  • 操作简单:只需要在一端操作
  • 时间复杂度低:基本操作都是O(1)
  • 实现简单:可以用数组或链表实现

缺点

  • 只能访问栈顶元素
  • 不支持随机访问
  • 查找元素需要O(n)时间

11.2 队列的优缺点

优点

  • 操作简单:两端操作,逻辑清晰
  • 时间复杂度低:基本操作都是O(1)
  • 适合BFS:天然适合广度优先搜索

缺点

  • 只能访问队头和队尾元素
  • 不支持随机访问
  • 查找元素需要O(n)时间

11.3 堆的优缺点

优点

  • 快速获取最值:O(1)时间获取最大值或最小值
  • 动态维护:插入和删除是O(log n)
  • 适合Top K问题

缺点

  • 只能访问堆顶元素
  • 不支持随机访问
  • 查找元素需要O(n)时间
  • 插入和删除是O(log n),比栈和队列慢

12. 常见题型总结

12.1 栈的基础应用类

  1. 括号匹配

    • 20.有效的括号:判断括号是否匹配
  2. 表达式求值

    • 150.逆波兰表达式求值:计算后缀表达式
  3. 相邻元素处理

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

12.2 栈和队列的相互实现

  1. 用栈实现队列

    • 232.用栈实现队列:使用两个栈实现队列
  2. 用队列实现栈

    • 225.用队列实现栈:使用队列实现栈(一个或两个队列)

12.3 单调栈类

  1. 找下一个更大/更小元素

    • 739.每日温度:找右边第一个更大元素
    • 496.下一个更大元素I:单调栈经典应用
  2. 面积计算

    • 42.接雨水:计算可以接的雨水总量
    • 84.柱状图中最大矩形:找左右两边更小的元素

12.4 单调队列类

  1. 滑动窗口最值
    • 239.滑动窗口最大值:使用单调队列找滑动窗口最大值

12.5 堆(优先队列)类

  1. Top K问题

    • 347.前K个高频元素:找出出现频率前K高的元素
  2. 合并问题

    • 23.合并K个升序链表:使用堆合并多个有序链表
  3. 中位数问题

    • 295.数据流的中位数:使用两个堆维护中位数

13. 总结

栈、队列和堆是三种重要的数据结构,各有其特点和适用场景。

核心要点

  1. 栈(LIFO):后进先出,适合括号匹配、表达式求值、单调栈问题
  2. 队列(FIFO):先进先出,适合BFS、滑动窗口、任务调度
  3. 堆(优先队列):快速获取最值,适合Top K问题、合并问题
  4. 单调栈/队列:维护单调性,用于找下一个更大/更小元素
  5. 时间复杂度:栈和队列基本操作O(1),堆插入删除O(log n)

使用建议

  • 根据问题特性选择合适的数据结构
  • 掌握栈和队列的相互实现
  • 理解单调栈和单调队列的应用场景
  • 掌握堆的自定义比较器写法
  • 注意边界情况处理(栈/队列为空)

常见题型总结

  • 栈的基础应用:括号匹配、表达式求值、相邻元素处理
  • 栈和队列的相互实现:用栈实现队列、用队列实现栈
  • 单调栈:每日温度、接雨水、柱状图中最大矩形
  • 单调队列:滑动窗口最大值
  • 堆(优先队列):前K个高频元素、合并K个升序链表、数据流的中位数
相关推荐
最爱吃咸鸭蛋2 小时前
LeetCode 97
算法·leetcode·职场和发展
夏幻灵2 小时前
CMD是什么
c++
HABuo2 小时前
【Linux进程(一)】进程深入剖析-->进程概念&PCB的底层理解
linux·运维·服务器·c语言·c++·后端·进程
图形学爱好者_Wu2 小时前
每日一个C++知识点|菱形继承
c++·程序员·编程语言
.简.简.单.单.2 小时前
Design Patterns In Modern C++ 中文版翻译 第十章 外观模式
c++·设计模式·外观模式
core5122 小时前
CatBoost:自带“翻译官”的算法专家
算法·boost·catboost
YGGP2 小时前
【Golang】LeetCode 139. 单词拆分
算法·leetcode
wuguan_2 小时前
C#递推算法
算法·c#·递推算法
十五年专注C++开发2 小时前
Jieba库: 一个中文分词领域的经典库
c++·分布式·自然语言处理·中文分词