:::tips 作者:LemonCat🐱
时间:2023-8-4 :::

第五章 栈与队列part01

:::info

  • 理论基础
  • 232.用栈实现队列
    1. 用队列实现栈 :::

      理论基础

      :::tips 了解一下 栈与队列的内部实现机制,文中是以C++为例讲解的。
      文章讲解:代码随想录 :::

      232.用栈实现队列

      :::tips 先看视频,了解一下模拟的过程,然后写代码会轻松很多
      题目链接:力扣 232.用栈实现队列
      文章讲解/视频讲解:代码随想录 :::

方法 - 双栈实现队列

  • 使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的
  • 需要两个栈一个输入栈,一个输出栈

下面动画模拟以下队列的执行过程:

  1. queue.push(1);
  2. queue.push(2);
  3. queue.pop(); 注意此时的输出栈的操作
  4. queue.push(3);
  5. queue.push(4);
  6. queue.pop();
  7. queue.pop();注意此时的输出栈的操作
  8. queue.pop();
  9. queue.empty();

代码随想录 | 第五章 - 栈与队列 - 图2

  1. 在push数据的时候,只要数据放进输入栈就好
  2. 在pop数据的时候,输出栈如果为空,就把进栈数据全部导入进来,再从出栈弹出数据
    1. 如果输出栈不为空,则直接从出栈弹出数据就可以了
  3. 在peek数据的时候,过程同 pop
    1. 我们可以将 两者相同的过程 -> 把进栈数据全部导入到出栈 整合成方法 dumpStackIn
  4. 判断队列为空 -> 如果进栈和出栈都为空的话,说明模拟的队列为空了
  1. class MyQueue {
  2. Stack<Integer> stackIn; // 入队的栈
  3. Stack<Integer> stackOut; // 出队的栈
  4. // 初始化
  5. public MyQueue() {
  6. stackIn = new Stack<>();
  7. stackOut = new Stack<>();
  8. }
  9. // 入队
  10. public void push(int x) {
  11. stackIn.push(x);
  12. }
  13. // pop 和 peek 执行前都执行去执行 dumpStackIn 让入队的栈里的的元素都放到出队的栈中
  14. public int pop() {
  15. dumpStackIn();
  16. return stackOut.pop();
  17. }
  18. public int peek() {
  19. dumpStackIn();
  20. return stackOut.peek();
  21. }
  22. public boolean empty() {
  23. return stackIn.isEmpty() && stackOut.isEmpty();
  24. }
  25. // 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
  26. private void dumpStackIn(){
  27. if (!stackOut.isEmpty()) return;
  28. while (!stackIn.isEmpty()){
  29. stackOut.push(stackIn.pop());
  30. }
  31. }
  32. }

225. 用队列实现栈

:::tips 可以大家惯性思维,以为还要两个队列来模拟栈,其实只用一个队列就可以模拟栈了。
建议大家掌握一个队列的方法,更简单一些,可以先看视频讲解
题目链接:力扣 225. 用队列实现栈
文章讲解/视频讲解:代码随想录 :::

方法一 - 两个Queue

  1. 两个队列
    1. queue1 是正经存放数据的队列 - 存放的顺序与栈出栈顺序一致
    2. queue2 临时队列 用于帮助 queue1 调整顺序用的
  2. 入栈
    1. 在加入元素时先将 q1 中的元素依次出栈压入 q2
    2. 然后将新加入的元素压入 q1
    3. 再将 q2 中的元素依次出栈压入 q1
  3. 其余操作 调用队列的操作即可模拟 栈的操作
    1. 因为顺序已经做了调整 -> 满足出栈顺序! ```java /**
  • 方法一 - 两个 Queue / class MyStack {

    Queue queue1; Queue queue2;

    public MyStack() {

    1. queue1 = new LinkedList<>(); // queue1 作为主要的队列,其元素排列顺序和出栈顺序相同
    2. queue2 = new LinkedList<>(); // queue2 仅作为临时放置

    }

    // 入栈 // 1. 在加入元素时先将q1中的元素依次出栈压入q2 // 2. 然后将新加入的元素压入q1 // 3. 再将q2中的元素依次出栈压入q1 public void push(int x) {

    1. while (!queue1.isEmpty()) {
    2. queue2.add(queue1.poll());
    3. }
    4. queue1.add(x);
    5. while (!queue2.isEmpty()) {
    6. queue1.add(queue2.poll());
    7. }

    }

    // 出栈 public int pop() {

    1. return queue1.poll();

    }

    // 查看栈顶 public int top() {

    1. return queue1.peek();

    }

    public boolean empty() {

    1. return queue1.isEmpty();

    }

}

  1. - 时间复杂度: pushO(n),其他为O(1)
  2. - 空间复杂度: O(n)
  3. <a name="ENRYG"></a>
  4. ### 方法二 - 一个Queue
  5. 1. 入栈`push` -> 入队 `queue.add(x);`
  6. 2. 出栈 pop 返回栈顶 top - 两者都需要进行 环形操作 -> size - 1 个元素 出栈再入栈
  7. 1. 出栈只需 再出队就好
  8. 2. 返回栈顶 需要保存出队的元素 `res` -> 然后将其入队 -> 返回那个 `res`
  9. ```java
  10. /**
  11. * 一个 Queue 实现
  12. */
  13. class MyStack {
  14. Queue<Integer> queue;
  15. public MyStack() {
  16. queue = new LinkedList<>();
  17. }
  18. // 入栈
  19. public void push(int x) {
  20. queue.add(x);
  21. }
  22. // 出栈
  23. public int pop() {
  24. rePosition();
  25. return queue.poll();
  26. }
  27. // 查看栈顶
  28. public int top() {
  29. rePosition();
  30. Integer res = queue.poll();
  31. queue.add(res);
  32. return res;
  33. }
  34. public boolean empty() {
  35. return queue.isEmpty();
  36. }
  37. // 将 size - 1 个元素 出栈再入栈
  38. private void rePosition() {
  39. int size = queue.size();
  40. size--;
  41. while (size-- > 0) {
  42. queue.add(queue.poll());
  43. }
  44. }
  45. }
  • 时间复杂度: push为O(n),其他为O(1)
  • 空间复杂度: O(n)

    第五章 栈与队列part02

    :::info

    1. 有效的括号
    1. 删除字符串中的所有相邻重复项
    1. 逆波兰表达式求值 :::

      20. 有效的括号

      :::tips 栈的经典应用 思考有哪些不匹配的场景
      题目链接:力扣 20. 有效的括号
      文章讲解/视频讲解:代码随想录 :::

      方法 - 消消乐法

      不匹配的情况

      一共有三种情况 - 囊括了所有情形!
  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。

    image.png

  2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。

    image.png

  3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。

    image.png

    代码思路

  • 代码只要覆盖了上面三种不匹配的情况,就不会出问题

代码随想录 | 第五章 - 栈与队列 - 图6

  1. 第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
  2. 第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
  3. 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
  4. 左括号和右括号全都匹配 - > 就是字符串遍历完之后,栈是空的,就说明全都匹配了

    小技巧:

    1. 在匹配左括号的时候,右括号先入栈
    2. 这样就只需要比较当前元素和栈顶相不相等就可以了
    3. 比左括号先入栈代码实现要简单的多了

Code:

  • 注意要 else if 否则判断 stack.isEmpty() 将出错

    1. /**
    2. * 方法 - 消消乐法
    3. */
    4. class Solution {
    5. public boolean isValid(String s) {
    6. // 若长度小于等于 1 直接返回
    7. if (s.length() <= 1) {
    8. return false;
    9. }
    10. Stack<Character> stack = new Stack<>();
    11. char[] sChars = s.toCharArray();
    12. for (char sChar : sChars) {
    13. // 碰到左括号,就把相应的右括号入栈
    14. if (sChar == '(') {
    15. stack.push(')');
    16. } else if (sChar == '[') {
    17. stack.push(']');
    18. } else if (sChar == '{') {
    19. stack.push('}');
    20. } else if (stack.isEmpty() || sChar != stack.peek()) {
    21. return false;
    22. } else { // 如果是右括号判断是否和栈顶元素匹配
    23. stack.pop();
    24. }
    25. }
    26. // 最后判断栈中元素是否相互匹配
    27. return stack.isEmpty();
    28. }
    29. }

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

    :::tips 栈的经典应用。
    要知道栈为什么适合做这种类似于爱消除的操作,因为栈帮助我们记录了 遍历数组当前元素时候,前一个元素是什么。
    题目链接:力扣 1047. 删除字符串中的所有相邻重复项
    文章讲解/视频讲解:代码随想录 :::

    思路

  • 本题也是用栈来解决的经典题目

    • 烦人的地方在于 相同的字符串消除后 还有可能有新的相邻字符串
    • 如果用 暴力的方法 就很不划算
    • 如果用栈 就会眼前一亮
  1. 栈的就是存放遍历过的元素
  2. 当遍历当前的这个元素的时候 去栈里看一下我们是不是遍历过相同数值的相邻元素
  3. 若相同则直接将元素出栈 否则入栈~

    代码随想录 | 第五章 - 栈与队列 - 图7

  4. 从栈中弹出剩余元素,此时是字符串ac

    1. 因为从栈里弹出的元素是倒序的,所以再对字符串进行

      方法一 - 消消乐法

      1. class Solution {
      2. public String removeDuplicates(String s) {
      3. if (s.length() <= 1) {
      4. return s;
      5. }
      6. Stack<Character> stack = new Stack<>();
      7. // 当遍历当前的这个元素的时候 去栈里看一下我们是不是遍历过相同数值的相邻元素
      8. // 若相同则直接将元素出栈 否则入栈
      9. for (int i = 0; i < s.length(); i++) {
      10. if (stack.isEmpty() || s.charAt(i) != stack.peek()) {
      11. stack.push(s.charAt(i));
      12. } else {
      13. stack.pop();
      14. }
      15. }
      16. // 剩余的元素即为不重复的元素
      17. s = "";
      18. while (!stack.isEmpty()) {
      19. s = stack.pop() + s; // 由于栈是先进后出 所以 s 要拼接在字符串后面!!!
      20. }
      21. return s;
      22. }
      23. }
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

    方法二 - 消消乐法 - 优化

  • 拿字符串直接作为栈,省去了栈还要转为字符串的操作

    1. class Solution {
    2. public String removeDuplicates(String s) {
    3. if (s.length() <= 1) {
    4. return s;
    5. }
    6. StringBuilder sb = new StringBuilder(); // 把他当做栈
    7. int index = -1;// 用于标记 sb 的长度 初始长度为 -1
    8. for (int i = 0; i < s.length(); i++) {
    9. if (index == -1 || s.charAt(i) != sb.charAt(index)) {
    10. sb.append(s.charAt(i));
    11. index++;
    12. } else {
    13. sb.deleteCharAt(index--);
    14. }
    15. }
    16. return new String(sb);
    17. }
    18. }
  • 时间复杂度: O(n)

  • 空间复杂度: O(1),返回值不计空间复杂度

    方法三 - 双指针

    ```java /**
  • 方法三 - 双指针 */ class Solution { public String removeDuplicates(String s) {

    1. if (s.length() == 1) {
    2. return s;
    3. }
    4. char[] ch = s.toCharArray();
    5. int fast = 0;
    6. int slow = 0;
    7. while (fast < ch.length) {
    8. // 直接用fast指针覆盖slow指针的值
    9. ch[slow] = ch[fast];
    10. // 遇到前后相同值的就跳过,即slow指针后退一步,下次循环就可以直接被覆盖掉了
    11. if (slow > 0 && ch[slow] == ch[slow - 1]) {
    12. slow--;
    13. } else {
    14. slow++;
    15. }
    16. fast++;
    17. }
    18. return new String(ch, 0, slow);

    } } ```

    150. 逆波兰表达式求值

    :::tips 本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题
    题目链接:力扣 150. 逆波兰表达式求值
    文章讲解/视频讲解:代码随想录 :::

思路

逆波兰表达式:

  • 相当于是二叉树中的后序遍历
  • 把运算符作为中间节点,按照后序遍历的规则画出一个二叉树
    • 后序遍历 即树的 前后中遍历

代码随想录 | 第五章 - 栈与队列 - 图8

  • 其余的操作就是读取栈进行运算就好~

    使用后序遍历原因

  • 我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。

  • 例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算符,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法 十分麻不麻烦!
  • 将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/“, “+”] ,就不一样了,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的

    方法 - 栈的妙用

  • 注意 “-“ 和 “/“ 需要特殊处理 因为需要注意顺序!

  • 米奇妙妙屋

    1. class Solution {
    2. public int evalRPN(String[] tokens) {
    3. Stack<Integer> stack = new Stack<>();
    4. Arrays.stream(tokens).forEach(t -> {
    5. // 注意 - 和/ 需要特殊处理
    6. switch (t) {
    7. case "+": {
    8. int temp1 = stack.pop();
    9. int temp2 = stack.pop();
    10. stack.push(temp2 + temp1);
    11. break;
    12. }
    13. case "-": {
    14. int temp1 = stack.pop();
    15. int temp2 = stack.pop();
    16. stack.push(temp2 - temp1);
    17. break;
    18. }
    19. case "*": {
    20. int temp1 = stack.pop();
    21. int temp2 = stack.pop();
    22. stack.push(temp2 * temp1);
    23. break;
    24. }
    25. case "/": {
    26. int temp1 = stack.pop();
    27. int temp2 = stack.pop();
    28. stack.push(temp2 / temp1);
    29. break;
    30. }
    31. default:
    32. stack.push(Integer.valueOf(t));
    33. break;
    34. }
    35. });
    36. return stack.pop();
    37. }
    38. }
  • 结束啦,晚安,小伙伴们~

    第五章 栈与队列part03

    :::tips

    1. 滑动窗口最大值
  • 347.前 K 个高频元素
  • 总结 :::

    239. 滑动窗口最大值(难)

    :::danger 队列的应用了 本题难,需要自己去构造单调队列
    题目链接:力扣 239. 滑动窗口最大值
    文章讲解/视频讲解:代码随想录 :::

方法一 - 自定义队列

  1. 暴力方法:遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法
    1. n 是数组的长度 k 是滑块的长度
  2. 自定义单调队列:
    1. 调队列维护队列里的元素方式

代码随想录 | 第五章 - 栈与队列 - 图9

  1. 设计单调队列 poppush 操作要保持如下规则:
    1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
    2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
    3. 保持如上规则,每次窗口移动的时候
      1. 只要 queue.peek() 获取栈顶元素
      2. 就可以返回当前窗口的最大值
  2. Deque<Integer> queue = new LinkedList<>();数据结构来实现
    1. 示例:输入: nums = [1,3,-1,-3,5,3,6,7]k = 3

代码随想录 | 第五章 - 栈与队列 - 图10

  1. 最后使用这个自定义单调队列即好

Code:

  1. class Solution {
  2. public int[] maxSlidingWindow(int[] nums, int k) {
  3. // 长度为一 提前返回
  4. if(nums.length == 1){
  5. return nums;
  6. }
  7. MyQueue myQueue = new MyQueue(); // 自定义队列
  8. int[] resArr = new int[nums.length - k + 1]; //存放结果元素的数组
  9. // 先将 k 个元素放入队列
  10. for (int i = 0; i < k; i++) {
  11. myQueue.add(nums[i]);
  12. }
  13. // 开始滑动窗口
  14. for (int i = k; i < nums.length; i++) {
  15. resArr[i - k] = myQueue.peek(); // 获取最大值
  16. myQueue.poll(nums[i - k]); // 移出尾部元素
  17. myQueue.add(nums[i]); // 添加新元素
  18. }
  19. resArr[resArr.length - 1] = myQueue.peek(); // 获取最后一次的最大值
  20. return resArr;
  21. }
  22. }
  23. /**
  24. * 自定义 队列
  25. */
  26. class MyQueue {
  27. Deque<Integer> queue = new LinkedList<>();
  28. // 获取队顶元素 - 永远是最大值
  29. public Integer peek() {
  30. return queue.peek();
  31. }
  32. // 添加元素
  33. // 1. 如果要添加的元素大于入口处的元素,就将入口元素弹出
  34. // 2. 保证队列元素单调递减
  35. // 比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
  36. public void add(Integer val) {
  37. while (!queue.isEmpty() && val > queue.getLast()) {
  38. queue.removeLast();
  39. }
  40. queue.add(val);
  41. }
  42. // 弹出元素
  43. // 1. 判断队列当前是否为空
  44. // 比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
  45. public void poll(Integer val) {
  46. if (!queue.isEmpty() && val.equals(queue.peek())) {
  47. queue.poll();
  48. }
  49. }
  50. }
  • 时间复杂度: O(n)
  • 空间复杂度: O(k)

    方法二 - 双端队列手动实现单调队列

  • @TODO 方法二只简单看了一遍 有时间在研究第二种方法~

    1. /**
    2. * 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
    3. * 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
    4. */
    5. class Solution {
    6. public int[] maxSlidingWindow(int[] nums, int k) {
    7. ArrayDeque<Integer> deque = new ArrayDeque<>();
    8. int n = nums.length;
    9. int[] res = new int[n - k + 1];
    10. int idx = 0;
    11. for(int i = 0; i < n; i++) {
    12. // 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
    13. // 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
    14. while(!deque.isEmpty() && deque.peek() < i - k + 1){
    15. deque.poll();
    16. }
    17. // 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
    18. while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
    19. deque.pollLast();
    20. }
    21. deque.offer(i);
    22. // 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
    23. if(i >= k - 1){
    24. res[idx++] = nums[deque.peek()];
    25. }
    26. }
    27. return res;
    28. }
    29. }

    347.前 K 个高频元素(中等)

    大/小顶堆的应用 在C++中就是优先级队列 大数据中取前k值 的经典思路 题目链接:力扣 347.前 K 个高频元素 文章讲解/视频讲解:代码随想录

方法一 - 小顶堆

  1. 统计元素出现的频率 -> 使用map来进行统计
  2. 对频率排序 -> 容器适配器就是优先级队列 =>PriorityQueue
    1. 优先级队列 -> 一个披着队列外衣的堆
    2. 堆 -> 是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
      1. 大顶堆: 头部比孩子大 根节点是最大的元素
      2. 小顶堆: 根节点是最小的元素,父亲比左右子要小
  3. 找出前K个高频元素
    1. 建议用小顶堆,因为要统计最大前k个元素
    2. 只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

image.png
Code:

  1. /**
  2. * 方法一 - 小顶堆
  3. */
  4. class Solution3 {
  5. public int[] topKFrequent(int[] nums, int k) {
  6. // Map 来存放统计每个数出现的频率
  7. // K:数值
  8. // V:频率
  9. Map<Integer, Integer> map = new HashMap<>();
  10. Arrays.stream(nums).forEach(num -> {
  11. map.put(num, map.getOrDefault(num, 0) + 1);
  12. });
  13. // 在优先队列PriorityQueue 中存储 二元组(num,cnt)
  14. // 1. cnt 表示元素值
  15. // 2. num 在数组中的出现次数
  16. // 出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
  17. PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
  18. for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
  19. if (queue.size() < k) { //小顶堆只需要维持k个元素有序
  20. queue.add(new int[]{entry.getKey(), entry.getValue()});
  21. }
  22. // 当前元素出现次数大于小顶堆的根结点 (当前这k个元素中出现次数最少的那个)
  23. else if (entry.getValue() > queue.peek()[1]) {
  24. queue.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
  25. queue.add(new int[]{entry.getKey(), entry.getValue()});
  26. }
  27. }
  28. // 小顶堆需要反向遍历
  29. // 依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
  30. int[] ans = new int[k];
  31. for (int i = k - 1; i >= 0; i--) {
  32. ans[i] = queue.poll()[0];
  33. }
  34. return ans;
  35. }
  36. }

总结

:::tips 栈与队列总结:代码随想录 :::

  • 感觉不容易 需要复习了~ QAQ