leetcode/notes/src/stack_and_queue.md

5.0 KiB
Raw Blame History

总结

使用场景:

  1. 需要先进后出的数据结构
  2. 匹配问题

经典题目:

20. 有效的括号

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

变体:单调栈

栈中的元素单调递增或单调递减。

使用场景:通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置。

拿找右边第一个比自己大的元素举例:

栈中存储元素的下标 i比较栈顶元素 arr[i] 和当前元素 arr[j] 的大小,

如果栈顶元素 < 当前元素,那么 j - i 就是答案,栈顶元素出栈(注意是循环出栈,直到栈顶元素 > 当前元素);

如果栈顶元素 > 当前元素,则当前元素入栈。

时间复杂度O(n)

经典题目:

739. 每日温度

496. 下一个更大元素 I

503. 下一个更大元素 II

Tips: 循环数组的处理方法:

for (int i{0}; i < 2 * len; ++i) {
  nums[i % len] ...
}

队列

使用场景:

  1. 先进先出的数据结构
  2. 滑动窗口最值问题

变体:优先级队列

队列中的数据以 <priority, value> 的形式存储,每一个 value 都有一个 priority

当入队,我们根据 priority 将元素插入到队列的相应位置,队列中的元素总是按优先级升序或者降序排列。

#include <iostream>
#include <queue>
#include <string>
#include <utility>

int main(int argc, const char *argv[]) {
    // priority 为 int 类型value 为 std::string 类型
    std::priority_queue<std::pair<int, std::string>> q;
    // 写入元素
    q.push(std::make_pair(3, "Microsoft"));
    q.push(std::make_pair(1, "Surface"));
    q.push(std::make_pair(2, "Apple"));
    q.push(std::make_pair(4, "MacBook"));
    // 访问顶部元素
    std::cout << q.top().first << q.top().second << std::endl;
    // 删除顶部元素
    q.pop();
    // 检查是否为空
    if (q.empty()) {
        std::cout << "Empty" << std::endl;
    } else {
        std::cout << "Not empty" << std::endl;
    }
    return 0;
}

输出:

4MacBook
Not empty

优先队列用一般用堆来实现,具有 O(log n) 时间复杂度的插入元素性能,O(n) 的初始化构造的时间复杂度。

经典题目:239. 滑动窗口最大值

奇技淫巧:随机删除元素

现在我们想以 O(logn) 的时间复杂度来删除元素,应该怎么做呢?

思路很简单,维护另一个优先队列——删除队列。

当我们要删除一个元素时,我们并不真的在原队列中删除它,而是把它放到删除队列中;

当我们要访问原队列栈顶时,看看原队列栈顶是不是等于删除队列栈顶,如果是,则循环删除原队列和删除队列的栈顶,直到栈顶不想等或者为空。

为什么这样做一定是正确的呢?有没有可能我们想要删除 x ,原队列栈顶也是 x ,而删除队列栈顶是 y ( y >= x ) 呢?

这是不可能的,因为如果 y >= x ,那么原队列栈顶就不可能是 x 。

变体:单调队列

优先队列有另外一个名字:二叉堆,它的数据结构本质上并不是队列,时间复杂度不是线性的。

是否有线性复杂度的数据结构呢?

有,这就是单调队列。

单调队列的特性如下:

  1. 它的数据结构本质依然是队列,因此具有线性时间复杂度
  2. 它并不能保证你弹出一个元素后,这个元素就真的在队列里删除了,也有可能当你在插入元素时会删掉队列里的其它元素
  3. 它能保证的是,队列中所有元素单调(递减),队首元素一定是队列里的最值。

经典题目:239. 滑动窗口最大值

我们看看这道题里的单调队列是怎么实现的。这里我们采用 deque ,相比于 queue ,它能同时对队首和队尾进行操作。

#include <deque>

class MyQueue {
 public:
  std::deque<int> que;

  void pop(int value) {
    // 只有当要弹出的元素等于队首时,才会弹出
    // 这样做没问题吗?
    // 没问题,因为我们只关注队首元素是不是最大的,只要我们想弹出的元素不是队首元素,那就可以不用管
    if (value == que.front() && !que.empty()) {
      que.pop_front();
    }
  }

  void push(int value) {
    // 当我们要插入的元素比队尾元素大时,就一直弹出队尾元素,直到小于等于队尾元素为止
    // 这样做没问题吗?
    // 没问题,因为我们只关注队首元素是不是最大的,其它元素要不要都无所谓。
    while (value > que.back() && !que.empty()) {
      que.pop_back();
    }
    que.push_back(value);
  }

  int front(void) { return que.front(); }
};