LeetCode 42接雨水全解:暴力超时→DP降维打击→双指针极限压缩空间→单调栈栈式凹槽定位,全景式解析算法优化路径

LeetCode 42接雨水全解:暴力超时→DP降维打击→双指针极限压缩空间→单调栈栈式凹槽定位,全景式解析算法优化路径

文章目录

在这里插入图片描述

本篇摘要

本篇围绕LeetCode 42“接雨水”展开,剖析四种解法:暴力法通过嵌套循环统计每柱接水量,易超时;动态规划预先记录左右最大值,将复杂度降至O(n);双指针边遍历边更新极值,空间优化至O(1);单调栈利用栈维护“凹槽”,高效定位存水区间。各方法层层递进,展现算法优化的核心思路。

LeetCode 42 接雨水 详解

在这里插入图片描述

① 暴力解法(多循环嵌套,卡超时,因此后续使用了两种基于暴力优化的方法)

拿到这道题,我们首先看图:

在这里插入图片描述
  • 这里我们不难发现当前位置装水的高度其实和两边高度有关,但是并不是相邻的,就拿索引为5的位置来说,这里最多装2个,其实和对应的3号索引以及对应大的7号索引位置有关。
  • 多观察几个就能得到一个结论:i位置处能装多少水取决于它左侧比它大的最大值与右侧比它大的最大值取个min减去i位置高度即可。

因此我们能得到:

int water =min(leftMax, rightMax)- height[i];

下面可以通过时间复杂度为O(N^2)走一遍,详细思路:

超时!!!!!
暴力法: 我们不难得出,只要我们遍历一遍数组,然后当前位置能接的水的高度其实,就是它左边比它大的,最大值与它右边比它大的最大值求个min
然后再减去当前位置高度即水的高度;然后我们暴力法是一个个位置走的,也就是求出每单位宽度接入的雨水进行累加,故此时的面积就是它的高度!

代码如下(详细见注释):

classSolution{public:inttrap(vector<int>& height){int n = height.size();int totalWater =0;for(int i =1; i < n -1; i++){int leftMax =0, rightMax =0;// 找左边最大值,如果不存在比它大的,就找最大的for(int j =0; j < i; j++){ leftMax =max(leftMax, height[j]);}// 找右边最大值,如果不存在比它大的,就找最大的for(int j = i +1; j < n; j++){ rightMax =max(rightMax, height[j]);}// 这里如果对应的左右的最大值有一个小于当前位置的高度,则说明当前位置不能接雨水if((leftMax < height[i])||(rightMax < height[i]))continue;// 计算当前位置能接的水量int water =min(leftMax, rightMax)- height[i];if(water >0){//这一步就可以取消了(如果前面判断了即continue那里) totalWater += water;}}return totalWater;}};

复杂度分析:

  • 时间复杂度:O(n²) - 平方时间复杂度
  • 空间复杂度:O(1) - 常数空间复杂度

② 动态规划解法

核心思想

对于每个位置 i,它能存多少水,取决于:

左边最高柱子右边最高柱子 中的较小者,再减去当前高度。

公式:

water[i] = max(0, min(leftMax[i], rightMax[i]) - height[i]) 
  • leftMax[i]:从 0i 的最大高度(包含 i)
  • rightMax[i]:从 in-1 的最大高度(包含 i)

步骤(三步走)
  1. 从左到右扫描,记录每个位置左边(含自己)的最大高度 → leftMax
  2. 从右到左扫描,记录每个位置右边(含自己)的最大高度 → rightMax
  3. 遍历每个位置,用上述公式累加雨水量

举例说明
height = [4, 2, 0, 3, 2, 5] 
iheight[i]leftMax[i]rightMax[i]min(L,R)water[i]
044540
124542
204544
334541
424542
555550

总和: 0 + 2 + 4 + 1 + 2 + 0 = 9


代码实现思路
  • 从暴力优化:按照暴力肯定会超时,因此我们不妨提前保存对应位置,也就是左边或者右边的最大值,用的时候直接取即可。
  • 先设置状态转移方程: dp_left[i]表示i位置及i位置以左比大的最大高度(如果不存在比它大的就是它自己) dp_right[i]表示i位置及i位置以右比大的最大高度(如果不存在比它大的就是它自己)

代码实现(详细见注释):

classSolution{public:inttrap(vector<int>&height){ vector<int>dp_left(height.size(),0);//当前位置左边的最大高度(包含当前位置) vector<int>dp_right(height.size(),0);//当前位置右边的最大高度(包含当前位置) dp_left[0]= height[0];//初始化防止越界,保证后面填表的正确性for(int i=1;i<height.size();i++){ dp_left[i]=max(dp_left[i-1], height[i]);}int n=height.size(); dp_right[n-1]= height[n-1];//初始化防止越界,保证后面填表的正确性for(int i=n-2;i>=0;i--){ dp_right[i]=max(dp_right[i+1], height[i]);}//再遍历一遍保存结果:int ans=0;//保存结果for(int i=1;i<n-1;i++){int h =min(dp_left[i], dp_right[i])- height[i];int w=1;//这里也是如果当前位置dp_left或者dp_right可能为height[i],此时故自然h就为0了:if(h>0) ans += h*w;}return ans;}};

复杂度分析

  • 时间复杂度:O(n) —— 三次遍历
  • 空间复杂度:O(n) —— 两个辅助数组
虽然空间不是最优(双指针可做到 O(1)),但这是最容易理解的动态规划解法

③ 双指针解法(优化对应的dp的空间复杂度变成O(1))

下面这个有点难理解,虽然代码写起来比较简单,做到了时间复杂度 O(N),空间复杂度O(1)。

双指针优化思路

首先我们使用dp的时候需要先把所有的左最大值以及右最大值找出保存起来,那么是不是可以边记录这俩种最大值,边进行雨水统计呢

首先要想理解下面这句话👇,我们首先直到这个原理:

双指针开始走,left和right哪个位置对应高度小,就统计哪个,然后决定++还是- -,所以不会出现比如left左边有比right大的,甚至比rightmax都大的(也就是这个位置被统计完了)这种情况!

下面再理解下:

因此我们就想到两个指针一个从左往右走,一个从右往左走,直到相遇,然后如果左边比右边大,说明右边的话,肯定左边比它高(也就是leftmax一定大于它),而右边👉没有比leftmax大的,然后我们直接拿着rightmax去right位置进行比较获取新的rightmax即可,然后再与right位置处作差即可(此时可能rightmax与right位置高度相同,此时right位置也就是不能接水);如果右边比左边大同理即可!!!

如果还不理解那就直接上图:

下面我们就以这模式为例:

在这里插入图片描述

下面我们采用反证法证明下:

在这里插入图片描述
  • 如果这种情况发生了,也就是对应的走到了图中的位置,那么此时我们就需要更新leftmax,然后就是8-6=2;难道最后真的是2吗,肯定是错的。
  • 因为我们要知道left只有比right小才会进行统计更新等,然后右移动,如果left能走到6处,说明8这个位置已经被统计更新完了,然而这样就违背我们代码的判断逻辑了,故不可能。
  • 也就是上面的情况不可能发生,即最后到达中间6位置处的一定是right,此时就是7-6=1了。

对应代码:

///双指针解法:优化对应的dp的空间复杂度变成O(1)//两个指针从首尾边走边统计接雨水量以及维护左侧和右侧对应的最大值(包括当前位置)//这里采取两个指针都向中间走的策略,然后走过的位置就要完成对应的雨水统计+跟新leftmax或者rightmaxinttrap(vector<int>& height){int left =0, right = height.size()-1;int leftMax =0, rightMax =0;int ans =0;while(left <= right){//这里如果left能走到这里,那么必然left左边没有比rightmax大的高度,因为要是有比右边最大还大的那么此时来到当前left位置的必然是right了,故不可能if(height[left]< height[right]){ leftMax =max(leftMax, height[left]); ans += leftMax - height[left]; left++;}//这里如果right能走到这里,那么必然right右边没有比leftmax大的高度,因为要是有比左边最大还大的那么此时来到当前right位置的必然是left了,故不可能else{ rightMax =max(rightMax, height[right]); ans += rightMax - height[right]; right--;}}return ans;}};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

④单调栈解法

单调栈简介

首先简单认识下单调栈 :

单调栈是一种特殊的栈结构,栈内元素始终保持单调递增单调递减的顺序。

核心特点
  • 单调性:栈内元素要么一直增大(单调递增栈),要么一直减小(单调递减栈)。
  • 出栈规则:新元素入栈时,若破坏单调性,则弹出栈顶元素,直到满足单调性再入栈。
常见用途

主要用于解决 “下一个更大/更小元素” 类问题,例如:

  • 找数组中每个元素右边第一个比它大的数(单调递增栈,从右往左遍历);
  • 找数组中每个元素左边第一个比它小的数(单调递增栈,从左往右遍历);
  • 计算柱状图中最大矩形面积、接雨水等问题。
左边最近比当前数大的数(用单调栈)
步骤:
  1. 初始化一个空栈和一个结果数组(初始值设为-1)。
  2. 遍历数组中的每个元素:
    • 当栈不为空且当前元素 ≥ 栈顶元素时,弹出栈顶元素(因为栈顶元素不可能是后续元素的“左边更大的数”了),然后依次循环比较。
    • 如果栈不为空,说明栈顶元素就是当前元素左边最近的比它大的数,将其存入结果数组;否则存入-1。
    • 将当前元素的索引入栈。
示例:

数组:[7, 5, 3, 2, 4, 1, 6, 9, 8]

在这里插入图片描述

详细步骤:

步骤当前元素栈状态操作说明结果数组
17[]栈空,结果=-1;入栈7[-1, _, _, _, _]
25[7]5 < 7,结果=7;入栈5[-1, 7, _, _, _]
33[7, 5]3 < 5,结果=5;入栈3[-1, 7, 5, _, _]
42[7, 5, 3]2 < 3,结果=3;入栈2[-1, 7, 5, 3, _]
54[7, 5, 3, 2]4 > 2 → 弹出2;
4 > 3 → 弹出3;
4 > 5?否,结果=5;入栈4
[-1, 7, 5, 3, 5]
61[7, 5, 4]1 < 4,结果=4;入栈1[-1, 7, 5, 3, 5, 4]
76[7, 5, 4, 1]6 > 1 → 弹出1;
6 > 4 → 弹出4;
6 > 5 → 弹出5;
6 < 7,结果=7;入栈6
[-1, 7, 5, 3, 5, 4, 7]
89[7, 6]9 > 6 → 弹出6;
9 > 7 → 弹出7;
栈空,结果=-1;入栈9
[-1, 7, 5, 3, 5, 4, 7, -1]
98[9]8 < 9,结果=9;入栈8[-1, 7, 5, 3, 5, 4, 7, -1, 9]
最终结果:

[-1, 7, 5, 3, 5, 4, 7, -1, 9]

单调栈一般模版
在这里插入图片描述
关键点
  • 单调栈性质:栈中元素从栈底到栈顶递减(大→小),保证每次弹出的都是“不可能成为后续元素左边更大数”的元素。
  • 时间复杂度:O(n),每个元素最多入栈、出栈一次。
注意点
  • 单调栈是一种数据结构,用于维护可能成为答案的候选集合。
  • 当保持栈内的元素单调递增时,可以通过该结构找到左边离自己最近且比自己小的数。
  • 若从右向左遍历数组,则可以利用单调栈来查找右边离自己最近且比自己更大或更小的数。
  • 我们一般题目实际应用并不是直接按照这个模版求的,而是有些附加的条件,此时一般情况就是我们在进行出栈逻辑的时候也就是对应while循环里面,根据对应条件完成对应统计 计算等操作(结合题目要求),但是思想上都是一样的。
单调栈不同选型需求
目标遍历方向栈的类型弹出条件
左边第一个比它大从左向右单调递减栈当前元素 ≥ 栈顶
左边第一个比它小从左向右单调递增栈当前元素 ≤ 栈顶
右边第一个比它大从右向左单调递减栈当前元素 ≥ 栈顶
右边第一个比它小从右向左单调递增栈当前元素 ≤ 栈顶
优势
  • 时间复杂度 O(n):每个元素最多入栈、出栈一次;
  • 空间复杂度 O(n):最坏情况栈存储所有元素。

单调栈通过维护单调性,将暴力枚举的 O(n²) 问题优化为线性时间,是处理“相邻元素大小关系”类问题的利器。

引入单调栈

根据暴力的思路(它左边比它大的,最大值与它右边比它大的最大值求个min然后再减去当前位置高度即水的高度),进行优化–>是否能转化成左边离它最近比它大的以及右边离它最近比它大的---->引入单调栈(这里有两种选择):

  • 1·单调递减栈( 即右边第一个比它大 )
  • 2·单调递减栈(即左边第一个比它大 )

下面我们选择第二种进行解答。

虽然这种方法理解起来比较抽象,但是博主尽量讲明白:

只要小于top位置的值就一直入栈,直到大于的时候的时候进行出栈处理(也就是找到第一个凹槽处,下面就开始从右往左进行求接入雨水的量了),直到栈出完了,最后退出;下面重复对应操作来找凹槽

下面画图说明下:

在这里插入图片描述
  • 此时我们只需要在出栈逻辑(即while循环时刻)进行对应统计相关操作即可。
  • 因此可以看出,它的处理逻辑是出现一个凹槽,然后从这个凹槽的右边位置开始处理(也就是从右到左的处理逻辑),之前位置处理过之后就默认填平了,因此它的左边在进行处理的时候就可以认为右边对应空位已经填平了就行。

详细代码(注释详解):

classSolution{public:inttrap(vector<int>&height){int ans =0; stack<pair<int,int>> s;//保存对应的索引以及高度 s.push(make_pair(0, height[0]));int Min,w,h;for(int i =1; i < height.size(); i++){// 进行维护的单调递减栈的检查while(!s.empty()&& height[i]> s.top().second){int mid = s.top().second;//为了求top--mid--i位置能接的最大雨水 s.pop();//如果当前i位置位置比我们要求接雨水位置小,必然top--mid--i范围内无法接入雨水,而栈是单调递减的,后面的也肯定如此,直接//break即可!if(height[i]< mid)break;//可能pop后为空了即,此次出栈任务完成,直接breakif(s.empty())break; Min =min(s.top().second, height[i]); w =abs(i - s.top().first)-1; h = Min - mid;//因为我们把可能与栈顶元素高度相同的也给入栈了,故此时可能mid和top相同因此面积及为0了,需要特判一下!if(w * h >0) ans += w * h;} s.push(make_pair(i, height[i]));}return ans;}};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

本篇小结

从暴力枚举到动态规划,再到双指针与单调栈,四种解法覆盖时间与空间复杂度的权衡。暴力法直观但低效,动态规划预处理打破嵌套循环,双指针进一步压缩空间,单调栈则以栈结构精准捕捉存水条件。掌握这些技巧,可灵活应对“区间极值”类问题。

Read more

【牛客CM11】链表分割

【牛客CM11】链表分割

刷爆LeetCode系列 * 牛客CM11: * github地址 * 前言 * 题目描述 * 题目与思路分析 * 代码实现 * 算法代码优化 牛客CM11: github地址 有梦想的电信狗 前言 本文用C++实现牛客CM11题 题目描述 题目链接:https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId=8&&tqId=11004&rp=2&ru=/activity/oj&qru=/ta/cracking-the-coding-interview/question-ranking 题目与思路分析 目标分析: 1. 编写代码,给定链表的头指针pHead,以给定值x为基准,将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 2. 不能改变原来数据的顺序

By Ne0inhk
手撕力扣138题:优雅复制带随机指针的链表,三步搞定经典算法题

手撕力扣138题:优雅复制带随机指针的链表,三步搞定经典算法题

手撕力扣138题✨:优雅复制带随机指针的链表,三步搞定经典算法题 * 一、题目核心剖析🔍 * 题目要求 * 解题难点 * 节点结构定义(C++) * 二、核心解题思路💡:三步法原地复制 * 步骤1:原地插入复制节点,打造“原节点-复制节点”成对链表 * 图形演示 * 核心代码片段 * 步骤2:修正复制节点的random指针,指向正确的复制节点 * 图形演示 * 核心代码片段 * 步骤3:拆分原链表与复制链表,得到最终的深拷贝链表 * 图形演示 * 核心代码片段 * 三、完整C++代码实现📝 * 四、算法性能分析📊 * 时间复杂度 * 空间复杂度 * 对比哈希表法 * 五、解题总结与拓展📚 * 解题核心要点 * 算法拓展 在链表的算法考察中,带随机指针的链表复制绝对是高频考点,力扣138题虽被标注为中等难度,但实则是锻炼链表操作思维的经典简单题。普通链表的复制仅需遍历处理next指针即可,而带random随机指针的链表,因random可

By Ne0inhk
【数据结构】常见时间复杂度以及空间复杂度

【数据结构】常见时间复杂度以及空间复杂度

时间复杂度与空间复杂度 * 一、复杂度的概念 * 二、时间复杂度 * 1、大O的渐进表示法 * 2、函数clock计算运算时间 * 3、常见复杂度对比 * 3.1常数项复杂度 * 3.2线性时间复杂度 * 案例1 * 案例2 * 3.3平方阶复杂度 * 3.4对数复杂度 * 3.5递归函数 * 单递归 * 双递归 * 三、空间复杂度 * 冒泡排序O(1) * 三个反置O(N) 一、复杂度的概念 * 一个算法的好坏,主要是对比两者的时间和空间两个维度,也就是时间和空间复杂度。 * 时间复杂度主要衡量一个算法运行的快慢,空间复杂度主要衡量一个算法运行需要的额外空间 二、时间复杂度 * 算法的时间复杂度是一个函数式T(N),算法中的基本操作的执行次数,为算法的时间复杂度。 * 注:编译器的不同,编译所需要的时间也不同。越新的编译器,编译的时间往往比旧的编译器快 * 当一个算法函数式为T(

By Ne0inhk
Flutter 三方库 statistics 鸿蒙高性能数据回归科学系统全域适配:将顶尖数理统计算法与重负载大模型双栈引擎植入微距节点彻底盘活泛计算终端底层数据-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 statistics 鸿蒙高性能数据回归科学系统全域适配:将顶尖数理统计算法与重负载大模型双栈引擎植入微距节点彻底盘活泛计算终端底层数据-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 statistics 鸿蒙高性能数据回归科学系统全域适配:将顶尖数理统计算法与重负载大模型双栈引擎植入微距节点彻底盘活泛计算终端底层数据感知系统 前言 在鸿蒙生态的智慧医疗、金融理财及运动健康类应用中,实时对传感器数据或业务流水进行深度统计分析是核心能力。例如,通过运动步频计算方差以识别走跑状态,或根据心率波动进行回归分析以预测压力指数。statistics 库作为 Dart 生态中轻量且纯粹的数学工具集,为这类需求提供了高性能的底层支持。本文将探讨如何在 OpenHarmony 上适配该库,实现设备侧的大数据即时运算。 一、原理解析 / 概念介绍 1.1 基础原理/概念介绍 statistics 库不依赖外部厚重的二进制 C++ 库,它通过 Dart 语言级优化实现了对 Iterable<num> 的原生扩展。其核心逻辑聚焦于描述性统计(Descriptive Statistics)与回归模型(Regression

By Ne0inhk