大家好,我是编程熊,双非逆袭选手,字节跳动、旷视科技前员工,ACM金牌,保研985,《ACM金牌选手讲解LeetCode算法系列》作者。

上一篇文章讲解了《线性表》中的数组、链表、栈和队列的概念和基本应用,本文讲解栈和队列的高级应用。

  • 单调栈
  • 双端队列
  • 滑动窗口

单调栈

介绍

单调栈 = 单调 + 栈,因此其同时满足两个特性: 单调性、栈的特点。

  • 单调性: 单调栈里面所存放的数据是有序的(单调递增或递减)。
  • 栈: 后进先出。

因其满足单调性和每个数字只会入栈一次,所以可以在时间复杂度 O(n) 的情况下解决一些问题。

下图是单调栈的图解,栈内数字满足单调性,且满足栈的后进先出的特性。

例题

LeetCode 739. 每日温度

题意

给定每天的温度,求对于每一天需要等几天才可以等到更暖和的一天。如果该天之后不存在 更暖和的天气,则记为 0。

输出一个一维数组,表示每天需要等待的天数。

示例
  1. 输入: temperatures = [73,74,75,71,69,72,76,73]
  2. 输出: [1,1,4,2,1,1,0,0]
题解

建立单调(非增)栈,栈存放每天的温度,为了方便计算天数,栈中存储的每天的温度在数组中下标,可以通过下标得到对应天的温度。

设温度数组为 a,从左向右依次遍历数组 a,假设当前遍历到数组位置为 j,则对应的天温度为 a[j],设栈顶元素的位置为 i,则对应的天的温度a[i] ,分为两种情况讨论。

  • 如果 a[j] > a[i],执行以下三步。

    • 表明比第 i 天更暖和的一天为第 j 天,则第 i 天的答案为 j-i,那么可以将栈顶元素弹出。
    • 重复检查栈顶元素,直至栈顶元素的 a[j] <= a[i] 或者 栈为空。
    • j 入栈。
  • 如果 a[j] <= a[i]
    • 表明第 i 天没有找到更暖和的一天,无需对栈操作。
    • j 入栈。

然后继续遍历温度数组 a,考虑下一天,直至结束。

遍历结束,若栈不为空,则说明栈内的天找不到更暖和的一天,记为 0

代码
  1. class Solution {
  2. public int[] dailyTemperatures(int[] T) {
  3. int[] ans = new int[T.length];
  4. Deque<Integer> s = new LinkedList<Integer>();
  5. for(int i = 0; i < T.length; i++) {
  6. while(!s.isEmpty() && T[i] > T[s.peek()]) {
  7. ans[s.peek()] = i - s.pop();
  8. }
  9. s.push(i);
  10. }
  11. return ans;
  12. }
  13. }

LeetCode 316. 去除重复字母

题意

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例
  1. 输入:s = "bcabc"
  2. 输出:"abc"
题解

首先思考这个问题的一个简单版本。给一个字符串删除一个字符,使得字典序最小。

  • 解法: 字典序就是字母的大小顺序,我们想字典序最小,那应删除满足 s[i] > s[i+1] 的最小位置 i 上的字符。

回到这个问题,我们也是想尽可能的删除满足 s[i] > s[i+1] 的最小位置 i 上的字符,如果每次都是遍历一遍字符串删除一个字符,这样时间复杂度可能退化到 O(n^2)

优化方法: 单调栈。

单调栈中存放的是字符,从左往右遍历字符串 s, 设当前遍历到字符串的位置 i,栈顶字符为c,考虑 s[i] 和 栈顶字符的大小关系、位置 i 的字符不在栈中,可分为两种。

  • c > s[i] 并且 位置 i 的字符不在栈中 并且 在位置 i 后面还存在字符 c,那么将 c 从栈中弹出。重复这个过程,直到 c > s[i] 不成立 或者 栈为空。
  • 不满足上述条件,直接将 s[i] 放入栈中。

继续遍历字符串 s,直至结束,最后栈中的字符就是题目要求的字典序最小的字符串。

代码
  1. class Solution {
  2. public String removeDuplicateLetters(String s) {
  3. int[] count = new int[30];
  4. for (int i = 0; i < s.length(); i++) {
  5. count[s.charAt(i) - 'a']++;
  6. }
  7. boolean[] vis = new boolean[30];
  8. StringBuffer ans = new StringBuffer();
  9. for (int i = 0; i < s.length(); i++) {
  10. int c = s.charAt(i) - 'a';
  11. if (!vis[c]) {
  12. while ((ans.length() > 0) && (count[ans.charAt(ans.length() - 1) - 'a'] > 0)
  13. && ((ans.charAt(ans.length() - 1) - 'a') > c)) {
  14. vis[ans.charAt(ans.length() - 1) - 'a'] = false;
  15. ans.deleteCharAt(ans.length() - 1);
  16. }
  17. vis[c] = true;
  18. ans.append(s.charAt(i));
  19. }
  20. count[c]--;
  21. }
  22. return ans.toString();
  23. }
  24. }

习题推荐

  1. LeetCode 496. 下一个更大元素 I
  2. LeetCode 1475. 商品折扣后的最终价格
  3. LeetCode 503. 下一个更大元素 II

双端队列 & 滑动窗口

介绍

双端队列是普通队列的加强版 ,区别于队列只能从队头出队,队尾入队;双端队列既可以在队头入队和出队,也可以在队尾入队和出队。

下图是双端队列的的图解,可以看出,双端队列既可以在队头入队和出队,也可以在队尾入队和出队。

例题

LeetCode 239. 滑动窗口最大值

题意

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

示例
  1. 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
  2. 输出:[3,3,5,5,6,7]
  3. 解释:
  4. 滑动窗口的位置 最大值
  5. --------------- -----
  6. [1 3 -1] -3 5 3 6 7 3
  7. 1 [3 -1 -3] 5 3 6 7 3
  8. 1 3 [-1 -3 5] 3 6 7 5
  9. 1 3 -1 [-3 5 3] 6 7 5
  10. 1 3 -1 -3 [5 3 6] 7 6
  11. 1 3 -1 -3 5 [3 6 7] 7
题解

滑动窗口经典题,维护一个单调的双端队列,为了方便,双端队列里面的数组的下标,从前往后遍历数组,需要实现两个功能。

  • 队头位置下标当前遍历位置下标 的距离大于 k,则删除队头元素,保证了队头下标在当前滑动窗口内
  • 队尾位置下标对应的值 小于 当前位置的值,则删除队尾元素,保证了队头下标对应的值是最大的

其次将当前遍历位置下标放入双端队列,然后遍历数组的下一个位置,直至结束。

代码
  1. class Solution {
  2. public int[] maxSlidingWindow(int[] nums, int k) {
  3. Deque<Integer> q = new LinkedList<>();
  4. int ans[] = new int[nums.length - k + 1];
  5. for (int i = 0; i < k; i++) {
  6. while (!q.isEmpty() && nums[q.getLast()] < nums[i]) {
  7. q.removeLast();
  8. }
  9. q.addLast(i);
  10. }
  11. ans[0] = nums[q.getFirst()];
  12. for (int i = k; i < nums.length; i++) {
  13. while(!q.isEmpty() && (i - q.getFirst() >= k)) {
  14. q.removeFirst();
  15. }
  16. while(!q.isEmpty() && nums[q.getLast()] < nums[i]) {
  17. q.removeLast();
  18. }
  19. q.addLast(i);
  20. ans[i - k + 1] = nums[q.getFirst()];
  21. }
  22. return ans;
  23. }
  24. }

LeetCode 3. 无重复字符的最长子串

题意

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例
  1. 输入: s = "abcabcbb"
  2. 输出: 3
  3. 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3
题解

观察样例,我们可以发现,依次递增地枚举子串的起始位置,那么合法的结束为止一定是递增的,因为对于起始位置 i-1 ,假设其不含有重复字符的最远右位置 j;那么对于起始位置为 i 的子串,因为 [i-1,j] 不含有重复字符,其不含有重复字符的最远右位置一定大于等于 i,因此我们考虑使用滑动窗口来解决本题。

我们可以固滑动窗口的右边界,找到最远的不含有重复字符的左边界 ,根据上面我们观察得到的性质可以,不含有重复字符的左边界是非递减的。

代码具体实现上我们可以用 双端队列实现滑动窗口,辅助数组 cnt 统计窗口内每个字符出现的次数 ,来判断窗口是否有重复的字符。

代码
  1. class Solution {
  2. public int lengthOfLongestSubstring(String s) {
  3. char[] cnt = new char[128];
  4. LinkedList<Character> q = new LinkedList<Character>();
  5. int ans = 0;
  6. for (int i = 0; i < s.length(); i++) {
  7. q.add(s.charAt(i));
  8. cnt[s.charAt(i)]++;
  9. while (cnt[s.charAt(i)] > 1) {
  10. char frontC = q.pollFirst();
  11. cnt[frontC]--;
  12. }
  13. ans = Math.max(ans, q.size());
  14. }
  15. return ans;
  16. }
  17. }

习题推荐

LeetCode 209. 长度最小的子数组

【下面是粉丝福利】

【计算机学习核心资源】: 涵盖了所有计算机学习核心资源,多看看进大厂问题不大。

【github宝藏仓库】: 对学习和面试都非常有帮助,学完超过99%同龄人。

ACM金牌选手讲解LeetCode算法《栈和队列的高级应用》的更多相关文章

  1. ACM金牌选手讲解LeetCode算法《哈希》

    大家好,我是编程熊. 往期文章介绍了<线性表>中的数组.链表.栈.队列,以及单调栈和滑动窗口. ACM金牌选手讲解LeetCode算法<线性表> ACM金牌选手讲解LeetCo ...

  2. ACM金牌选手算法讲解《线性表》

    哈喽,大家好,我是编程熊,双非逆袭选手,字节跳动.旷视科技前员工,ACM亚洲区域赛金牌,保研985研究生,分享算法与数据结构.计算机学习经验,帮助大家进大厂~ 公众号:『编程熊』 文章首发于: ACM ...

  3. ACM金牌选手整理的【LeetCode刷题顺序】

    算法和数据结构知识点图 首先,了解算法和数据结构有哪些知识点,在后面的学习中有 大局观,对学习和刷题十分有帮助. 下面是我花了一天时间花的算法和数据结构的知识结构,大家可以看看. 后面是为大家 精心挑 ...

  4. 编程熊讲解LeetCode算法《二叉树》

    大家好,我是编程熊. 往期我们一起学习了<线性表>相关知识. 本期我们一起学习二叉树,二叉树的问题,大多以递归为基础,根据题目的要求,在递归过程中记录关键信息,进而解决问题. 如果还未学习 ...

  5. LeetCode算法题-Design HashMap(Java实现)

    这是悦乐书的第299次更新,第318篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第167题(顺位题号是706).在不使用任何内置哈希表库的情况下设计HashMap.具体 ...

  6. LeetCode算法题-Design HashSet(Java实现)

    这是悦乐书的第298次更新,第317篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第166题(顺位题号是705).不使用任何内建的hash表库设计一个hash集合,应包含 ...

  7. LeetCode算法题-Relative Ranks(Java实现)

    这是悦乐书的第248次更新,第261篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第115题(顺位题号是506).根据N名运动员的得分,找到他们的相对等级和得分最高的三个 ...

  8. LeetCode算法题-Linked List Cycle(Java实现)

    这是悦乐书的第176次更新,第178篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第35题(顺位题号是141).给定一个链表,确定它是否有一个循环. 本次解题使用的开发工 ...

  9. 小旭讲解 LeetCode 53. Maximum Subarray 动态规划 分治策略

    原题 Given an integer array nums, find the contiguous subarray (containing at least one number) which ...

随机推荐

  1. MIT Graph实践概述

    MIT Graph实践概述 Features功能 •   iCloud Support •   Multi Local & Cloud Graphs •   Thread Safe •   S ...

  2. CPU的自动调度矩阵乘法

    CPU的自动调度矩阵乘法 这是一个有关如何对CPU使用自动调度程序的文档. 与依靠手动模板定义搜索空间的基于模板的autotvm不同,自动调度程序不需要任何模板.用户只需要编写计算声明,而无需任何调度 ...

  3. 短波红外(SWIR)相机camera

    短波红外(SWIR)相机camera AVs Can't Drive Everywhere. Can TriEye's SWIR Camera Help? TriEye的短波红外(SWIR)摄像机能否 ...

  4. 从“信息化”到“智慧化”,GVS视声将如何赋能智慧医院?

    4月23日-25日,2021年中华医院信息网络大会(CHINC)盛大举办,今年首次携手中国医院建筑与装备创新发展大会,同期同地亮相杭州国际博览中心,塑造了全新的"双引擎"品牌盛会. ...

  5. Linux芯片驱动之SPI Controller

    针对一款新的芯片,芯片厂商如何基于Linux编写对应的 SPI controller 驱动? 我们先看看 Linux SPI 的整体框架: 可以看到,最底层是硬件层,对应芯片内部 SPI contro ...

  6. 微信小程序踩坑之获取手机号

    最近在开发小程序遇到这样一个问题, 在用户点击授权后去解密手机号时会出现第一次失败,第二次成功的情况.研究了一段时间,终于找到比较合理的解决方案,在此记录并总结一下,希望可以帮助到大家. 需求描述 在 ...

  7. SpringBoot系列——admin服务监控

    前言 springboot项目部署起来后,如何实时监控项目的运行状况呢?本文记录使用springboot-admin对服务进行监控. springboot-admin介绍:https://codece ...

  8. Duilib的双缓冲实现,附带GDI、WTL的双缓冲实现

    前言: 闪烁问题,之前的经验是使用双缓冲,借此机会,把双缓冲的研究心得总结下. 双缓冲的含义: 缓冲这个词,相信大家都不陌生,Cache.主要是为了解决上下游(或者模块.或者系统)等性能不匹配问题.如 ...

  9. VueX理解

    什么是Vuex? 官方说法:Vuex 是一个专为 Vue.js应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. 个人理解:Vue ...

  10. UnityPlayerActivity.java使用或覆盖了已过时的 API。

    Root\Temp\gradleOut\unityLibrary\src\main\java\com\unity3d\player\UnityPlayerActivity.java使用或覆盖了已过时的 ...