单调栈的学习

news2024/11/18 11:37:21

文章目录

  • 单调栈的学习
    • 什么是单调栈?
    • 单调栈模板
      • 暴力解法
      • 单调栈解法
    • 单调栈的简单变形
      • 1.[496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/)
      • 2.[739. 每日温度](https://leetcode.cn/problems/daily-temperatures/)
      • 3.[503. 下一个更大元素 II](https://leetcode.cn/problems/next-greater-element-ii/)
    • 总结

单调栈的学习

什么是单调栈?

栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。

单调栈实际上就是栈,只是利用了一些巧妙的逻辑使得每次新元素入栈后,栈内的元素都保持有序(单调递增或者单调递减)

听起来有点像堆(heap)

但是不是堆,单调栈用途不太广泛,只处理一类典型的问题

  • 下一个更大元素
  • 上一个更小元素

单调栈模板

输入一个数组nums,请返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大元素就保存-1

比如,输入一个数组nums=[2,1,2,4,3],返回数组[4,2,4,-1,-1]。因为第一个2后面比2大的数是4;1后面比1大的数是2;第二个2后面比2大的数是4;4后面没有比4大的数,填-1;3后面没有比3更大的数,填-1.

暴力解法

就是堆每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2)

单调栈解法

我们可以把这个问题抽象为:

把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素2的下一个更大元素?

如果能够看到元素2,那么它后面可见的第一个人就是2的下一个更大元素。

为什么?

因为比2小的元素身高不够,都被2挡住了,所以第一个露出来的就是答案

如图所示:

在这里插入图片描述

代码实现如下:

int[] nextGreaterElement(int[] nums) {
    int n = nums.length;
    // 存放答案的数组
    int[] res = new int[n];
    Stack<Integer> s = new Stack<>(); 
    // 倒着往栈里放
    for (int i = n - 1; i >= 0; i--) {
        // 判定个子高矮
        while (!s.isEmpty() && s.peek() <= nums[i]) {
            // 矮个起开,反正也被挡着了。。。
            s.pop();
        }
        // nums[i] 身后的更大元素
        res[i] = s.isEmpty() ? -1 : s.peek();
        s.push(nums[i]);
    }
    return res;
}

for循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈其实就是正着出栈。

while循环就是把两个个子高元素之间的元素排除掉,因为它们的存在是没有意义的,前面挡着个更高的元素,所以它们不可能被作为后续进来的元素的下一个更大元素

这个算法的时间复杂度并不直观,如果看到for循环嵌套while循环,可能认为这个算法的时间复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)

因为从整体上看,总共有n个元素,每个元素都被push入栈了一次,而最多就会被pop一次,没有任何冗余操作。所以总的计算规模是和元素规模n成正比的,也就是O(n)的复杂度

单调栈的简单变形

1.496. 下一个更大元素 I

这道题就是给你输入两个数组nums1和nums2,让你求nums1中的元素在nums2中的下一个更大元素

其实很容易就可以解决这个问题,因为题目说nums1是nums2的子集,所以我们先把nums2中每个元素的下一个更大元素算出来存到一个映射里,然后让nums1中的元素去查表即可

完整代码如下:

int[] nextGreaterElement(int[] nums1, int[] nums2) {
    // 记录 nums2 中每个元素的下一个更大元素
    int[] greater = nextGreaterElement(nums2);
    // 转化成映射:元素 x -> x 的下一个最大元素
    HashMap<Integer, Integer> greaterMap = new HashMap<>();
    for (int i = 0; i < nums2.length; i++) {
        greaterMap.put(nums2[i], greater[i]);
    }
    // nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果
    int[] res = new int[nums1.length];
    for (int i = 0; i < nums1.length; i++) {
        res[i] = greaterMap.get(nums1[i]);
    }
    return res;
}

int[] nextGreaterElement(int[] nums) {
    // 见上文
}

或者是

class Solution {
    private static Map<Integer,Integer> greaterElements;
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        greaterElements(nums2);
        int[] res=new int[nums1.length];
        for(int i=0;i<nums1.length;i++){
            res[i]=greaterElements.get(nums1[i]);
        }
        return res;
    }
    public static void greaterElements(int[] nums){
        greaterElements=new HashMap<>();
        Stack<Integer> stack=new Stack<>();
        for(int i=nums.length-1;i>=0;i--){
            while(!stack.isEmpty()&&nums[i]>=stack.peek()){
                stack.pop();
            }
            if(stack.isEmpty()){
                greaterElements.put(nums[i],-1);
            }else{
                greaterElements.put(nums[i],stack.peek());
            }
            stack.push(nums[i]);
        }
    }
}

2.739. 每日温度

给你一个温度数组temperatures,这个数组存放的是近几天的天气温度,题目要求返回一个等长的数组,计算对于每一天你还至少要等多少天才能等到一个更暖和的气温,如果等不到那一天填0

比如说给你输入 temperatures = [73,74,75,71,69,76],你返回 [1,1,3,2,1,0]。因为第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。

这个问题本质上也是找下一个更大的元素,只不过现在不是问下一个更大的元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离是多少

解决方法就是记录下一个更大元素的数就可以了

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n=temperatures.length;
        Stack<Integer> elements=new Stack<>();
        //同时存储索引
        Stack<Integer> locations=new Stack<>();
        //存放最后下一个更大的数的索引
        int[] location=new int[n];
        for(int i=n-1;i>=0;i--){
            while(!elements.isEmpty()&&elements.peek()<=temperatures[i]){
                elements.pop();
                locations.pop();
            }
            location[i]=elements.isEmpty()?-1:locations.peek();
            elements.push(temperatures[i]);
            locations.push(i);
        }
        int[] res=new int[n];
        for(int j=0;j<n;j++){
            int num=location[j]-j;
            if(num<=0){
                res[j]=0;
            }else{
                res[j]=num;
            }
        }
        return res;
    }
}

或者是

int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] res = new int[n];
    // 这里放元素索引,而不是元素
    Stack<Integer> s = new Stack<>(); 
    /* 单调栈模板 */
    for (int i = n - 1; i >= 0; i--) {
        while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
            s.pop();
        }
        // 得到索引间距
        res[i] = s.isEmpty() ? 0 : (s.peek() - i); 
        // 将索引入栈,而不是元素
        s.push(i); 
    }
    return res;
}

3.503. 下一个更大元素 II

同样是求下一个更大元素,但是现在这个数组是环形的

比如输入 [2,1,2,4,3],你应该返回 [4,2,4,-1,4],因为拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4

我们一般是通过 % 运算符求模(余数),来模拟环形特效:

int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
    // 在环形数组中转圈
    print(arr[index % n]);
    index++;
}

我们要如何处理呢?

难点在于最后一个元素如何找到比自己大的数

对于这种需求,常用的套路就是将数组长度翻倍

这样最后一个元素就能找到下一个更大的数,其他的数也能被正确计算

当然我们可以不构造新的数组而是利用循环数组的技巧来模拟数组长度翻倍的效果

完整的代码如下:

class Solution {
    public int[] nextGreaterElements(int[] nums) {
        Stack<Integer> elements=new Stack<>();
        int n=nums.length;
        int[] res=new int[n];
        for(int i=2*n-1;i>=0;i--){
            while(!elements.isEmpty()&&elements.peek()<=nums[i%n]){
                elements.pop();
            }
            res[i%n]=elements.isEmpty()?-1:elements.peek();
            elements.push(nums[i%n]);
        }
        return res;
    }
}

这样就能很巧妙地解决环形数组的问题,时间复杂度是O(N)

总结

在这里说的单调栈的模板都是计算每个元素的下一个更大元素,但如果题目让你计算上一个更大的元素或者计算上一个更大或相等的元素又要如何修改对应的模板呢?

而且在实际的应用中,题目不会直接让你计算下一个或上一个更大或更小的元素,那又如何把问题转换成单调栈相关的问题呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/496349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kali Linux 使用远程桌面连接——xrdpxfce

[笔者系统版本] [Kali]: Kali Linux 2023.1 [Kernel]: kernel 6.1.0 [Desktop]: Xfce 4.18.1 1. 前言 在 Windows 中我们会经常使用到远程桌面这样便利的工具&#xff0c;让我们随时随地都可以使用自己想要使用的电脑&#xff0c;或者同时使用多台设备&#xff0c;那么本文就将…

open3d 源码阅读image_processing.py

目录 1. open3d.geometry.Image和numpy互转 2. 对open3d.geometry.Image进行高斯过滤 3. 高斯金字塔过滤 4. sobel过滤 5. 可视化o3d.geometry.Image 1. open3d.geometry.Image和numpy互转 import numpy as np import matplotlib.pyplot as plt import matplotlib.image a…

Midjourney从入门到精通

前言 什么是AI绘画 AI 绘画&#xff0c;顾名思义就是利用人工智能进行绘画&#xff0c;是人工智能生成内容&#xff08;AIGC&#xff09;的一个应用场景。其主要原理就是收集大量已有作品数据&#xff0c;通过算法对它们进行解析&#xff0c;最后再生成新作品&#xff0c;而算…

vue框架快速入门

vue 1、第一个Vue程序1.1、什么是Vue程序1.2、为什么要使用MVVM1.3、Vue1.4、第一个vue程序 2、基础语法2.1、v-bind2.2、v-if&#xff0c; v-else2.3、v-for2.4、v-on 3、Vue表单双绑、组件3.1、什么是双向数据绑定3.2、在表单中使用双向数据绑定3.3、什么是组件 4、Axios异步…

NixOS Legacy Boot(MBR) VmwareWorkstation安装向导

NixOS & Legacy Boot(MBR) VmwareWorkstation安装向导 目录 NixOS & Legacy Boot(MBR) VmwareWorkstation安装向导1. 下载镜像2. 创建空白虚拟机3. 使用命令行安装 NixOS3.1 Legacy Boot(MBR)3.2 格式化 4. configration.nix 配置文件5. 部署NixOS6. 部分教育站镜像源集…

Maven 3.9.1下载安装配置一条龙(无压力)亲测

这里写自定义目录标题 前言一、下载 Apache Maven 3.9.11.1、请先检查自己的IDEA是否有这个条件&#xff0c;是否兼容1.2、Maven下载 二、Windows安装配置Maven2.1、解压2.2、新建 repository 本地仓库2.3、配置环境变量MAVEN_HOME 软件路径M2_HOME 本地仓库路径配置Path2.3.1新…

关于maven

一、maven是什么 一个java项目构建工具 二、maven的作用 &#xff08;1&#xff09;依赖管理 不同框架整合&#xff0c;互相依赖jar包版本不同&#xff0c;版本不一样&#xff0c;程序跑起来就会报错。用maven管理jar包。 &#xff08;2&#xff09;跨平台构建项目 linux服…

数字信号处理3:A/D、D/A转换

信号这个东西&#xff0c;我们是实际应用中用的大多都是模拟信号&#xff0c;比如说语音、地震、雷达、声纳信号&#xff0c;这些都是模拟信号&#xff0c;但是&#xff0c;计算机想要通过数学方法处理模拟信号&#xff0c;就要先将模拟信号转换成具有有限精度的数字序列&#…

L4公司进军辅助驾驶,放话无图也能跑遍中国

作者 | Amy 编辑 | 德新 高阶智能驾驶走向规模量产&#xff0c;高精地图成为关键的门槛之一。今年&#xff0c;多家车企和智驾公司都喊出「不依赖高精地图&#xff0c;快速大规模落地」的口号。 华为、小鹏、元戎以及毫末等&#xff0c;可能是最快在国内量产 无高精图智…

TCP/IP网络编程(一)

TCP/IP网络编程读书笔记 第1章 理解网络编程和套接字1.1 理解网络编程和套接字1.1.1 构建打电话套接字1.1.2 编写 Hello World 套接字程序 1.2 基于Linux的文件操作1.2.1 底层访问和文件描述符1.2.2 打开文件1.2.3 关闭文件1.2.4 将数据写入文件1.2.5 读取文件中的数据1.2.6 文…

AI仿写软件-仿写文章生成器

AI仿写软件&#xff1a;高效出色的营销利器 作为互联网时代的营销人员&#xff0c;我们不仅需要品牌意识&#xff0c;还必须深谙营销技巧。万恶的时限压力使得我们不得不在有限的时间内输出更多的文本内容&#xff0c;以便吸引更多的关注。那么&#xff0c;如何解决这个问题呢…

C++网络基础知识面试题2

目录 1、使用TCP的常见协议有哪些&#xff1f;使用UDP的常见协议有哪些&#xff1f;简单说几个 2、如何判断访问目标地址的网络是通的&#xff1f;如何简单地查看到目标地址的网络是否有丢包和抖动&#xff1f; 3、如果知道目标服务器的服务端口有没有开启&#xff1f; 4、…

【NodeJs】使用Express框架快速搭建一个web网站

如果电脑有安装使用Nodejs&#xff0c;用得次数少的话&#xff0c;忘了怎么弄&#xff0c;可以看看这个文章&#xff0c;按照步骤&#xff0c;能快速搭建一个web网站服务器&#xff0c; 首先&#xff0c;你需要保证电脑系统有安装了Node.js&#xff0c;然后可以用VsCode开发工…

Java多线程基础概述

简述多线程&#xff1a; 是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程&#xff0c;提升性能。 正式着手代码前&#xff0c;需要先理清4个概念&#xff1a;并发&#xff0c;并行&#xff0c;进程&#…

ChatGPT带你领略自动驾驶技术

一、自动驾驶技术现概述 自动驾驶技术是指利用计算机、传感器和其他设备&#xff0c;使车辆能够在不需要人类干预的情况下自主行驶的技术。目前&#xff0c;自动驾驶技术已经在一些汽车厂商和科技公司中得到广泛应用&#xff0c;但仍然存在一些技术和法律上的挑战&#xff0c;需…

c++类友元函数理解(图、文、代码)

序&#xff1a; 1、初学c&#xff0c;理解阶段&#xff0c;一下为个人理解和案例&#xff0c;陆续更新 一、友元函数和普通函数区别 类的友元函数是函数&#xff0c;但是他可以调用类的私有变量&#xff0c;以下代码&#xff0c;Fun2是报错的&#xff0c;因为这个函数跟A没任…

基于SSM框架流浪动物救助及领养管理系统(spring+springmvc+mybatis+jsp+jquery+layui)

一、项目简介 本项目是一套基于SSM框架流浪动物救助及领养管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&…

java错题总结(28-30页)

------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------- 不考虑类加载&#xff0c; --------------------------------------------…

实验三 磁盘调度算法设计

实验三 磁盘调度算法设计 实验目的&#xff1a; 通过对磁盘调度算法的设计&#xff0c;深入理解提高磁盘访问速度的原理。 实验内容&#xff1a; 模拟实现磁盘调度算法&#xff1a;最短寻道时间优先&#xff08;SSTF&#xff09;和扫描&#xff08;SCAN&#xff09;算法。 …

数字化转型导师坚鹏:企业数字化转型培训如何高效推进评价与改进

企业数字化转型培训如何高效推进、评价与改进 ——以推动企业数字化转型战略落地为核心&#xff0c;实现知行果合一 课程背景&#xff1a; 很多企业都在开展企业数字化转型培训工作&#xff0c;目前存在以下问题急需解决&#xff1a; 不清楚如何有效推进企业数字化转型培训…