mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-06 23:28:29 +08:00
Update
This commit is contained in:
@ -36,9 +36,9 @@
|
||||
* 动态规划
|
||||
* 单调栈
|
||||
|
||||
## 双指针解法
|
||||
## 暴力解法
|
||||
|
||||
这道题目使用双指针法并不简单,我们来看一下思路。
|
||||
本题暴力解法也是也是使用双指针。
|
||||
|
||||
首先要明确,要按照行来计算,还是按照列来计算。
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。
|
||||
|
||||
首先从头遍历所有的列,并且**要注意第一个柱子和最后一个柱子不接雨水**,代码如下:
|
||||
|
||||
```CPP
|
||||
for (int i = 0; i < height.size(); i++) {
|
||||
// 第一个柱子和最后一个柱子不接雨水
|
||||
@ -129,19 +130,18 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2)。
|
||||
空间复杂度为O(1)。
|
||||
因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2),空间复杂度为O(1)。
|
||||
|
||||
力扣后面修改了后台测试数据,所以以上暴力解法超时了。
|
||||
|
||||
## 双指针优化
|
||||
|
||||
|
||||
|
||||
|
||||
## 动态规划解法
|
||||
|
||||
在上一节的双指针解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算。
|
||||
在暴力解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算。
|
||||
|
||||
当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。
|
||||
|
||||
为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。
|
||||
为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。
|
||||
|
||||
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
|
||||
|
||||
@ -149,8 +149,6 @@ public:
|
||||
|
||||
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
|
||||
|
||||
这样就找到递推公式。
|
||||
|
||||
代码如下:
|
||||
|
||||
```CPP
|
||||
@ -185,10 +183,13 @@ public:
|
||||
|
||||
## 单调栈解法
|
||||
|
||||
这个解法可以说是最不好理解的了,所以下面我花了大量的篇幅来介绍这种方法。
|
||||
关于单调栈的理论基础,单调栈适合解决什么问题,单调栈的工作过程,大家可以先看这题讲解 [739. 每日温度](https://programmercarl.com/0739.每日温度.html)。
|
||||
|
||||
单调栈就是保持栈内元素有序。和[栈与队列:单调队列](https://programmercarl.com/0239.滑动窗口最大值.html)一样,需要我们自己维持顺序,没有现成的容器可以用。
|
||||
|
||||
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
|
||||
|
||||
而接雨水这道题目,我们正需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。
|
||||
|
||||
### 准备工作
|
||||
|
||||
@ -212,6 +213,7 @@ public:
|
||||
|
||||

|
||||
|
||||
关于单调栈的顺序给大家一个总结: [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 中求一个元素右边第一个更大元素,单调栈就是递增的,[84.柱状图中最大的矩形](https://programmercarl.com/0084.柱状图中最大的矩形.html)求一个元素右边第一个更小元素,单调栈就是递减的。
|
||||
|
||||
3. 遇到相同高度的柱子怎么办。
|
||||
|
||||
@ -227,13 +229,13 @@ public:
|
||||
|
||||
4. 栈里要保存什么数值
|
||||
|
||||
是用单调栈,其实是通过 长 * 宽 来计算雨水面积的。
|
||||
使用单调栈,也是通过 长 * 宽 来计算雨水面积的。
|
||||
|
||||
长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,
|
||||
|
||||
那么栈里有没有必要存一个pair<int, int>类型的元素,保存柱子的高度和下标呢。
|
||||
|
||||
其实不用,栈里就存放int类型的元素就行了,表示下标,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。
|
||||
其实不用,栈里就存放下标就行,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。
|
||||
|
||||
所以栈的定义如下:
|
||||
|
||||
@ -243,9 +245,17 @@ stack<int> st; // 存着下标,计算的时候用下标对应的柱子高度
|
||||
|
||||
明确了如上几点,我们再来看处理逻辑。
|
||||
|
||||
### 单调栈处理逻辑
|
||||
### 单调栈处理逻辑
|
||||
|
||||
先将下标0的柱子加入到栈中,`st.push(0);`。
|
||||
以下操作过程其实和 [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 也是一样的,建议先做 [739. 每日温度](https://programmercarl.com/0739.每日温度.html)。
|
||||
|
||||
以下逻辑主要就是三种情况
|
||||
|
||||
* 情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()]
|
||||
* 情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()]
|
||||
* 情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()]
|
||||
|
||||
先将下标0的柱子加入到栈中,`st.push(0);`。 栈中存放我们遍历过的元素,所以先将下标0加进来。
|
||||
|
||||
然后开始从下标1开始遍历所有的柱子,`for (int i = 1; i < height.size(); i++)`。
|
||||
|
||||
@ -278,7 +288,7 @@ if (height[i] == height[st.top()]) { // 例如 5 5 1 7 这种情况
|
||||
|
||||
当前遍历的元素i,就是凹槽右边的位置,下标为i,对应的高度为height[i](就是图中的高度3)。
|
||||
|
||||
此时大家应该可以发现其实就是**栈顶和栈顶的下一个元素以及要入栈的三个元素来接水!**
|
||||
此时大家应该可以发现其实就是**栈顶和栈顶的下一个元素以及要入栈的元素,三个元素来接水!**
|
||||
|
||||
那么雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,代码为:`int h = min(height[st.top()], height[i]) - height[mid];`
|
||||
|
||||
@ -367,7 +377,7 @@ public:
|
||||
|
||||
### Java:
|
||||
|
||||
双指针法
|
||||
暴力解法:
|
||||
```java
|
||||
class Solution {
|
||||
public int trap(int[] height) {
|
||||
@ -393,7 +403,7 @@ class Solution {
|
||||
}
|
||||
```
|
||||
|
||||
动态规划法
|
||||
双指针:
|
||||
```java
|
||||
class Solution {
|
||||
public int trap(int[] height) {
|
||||
@ -470,7 +480,7 @@ class Solution {
|
||||
|
||||
### Python:
|
||||
|
||||
双指针法
|
||||
暴力解法:
|
||||
```Python
|
||||
class Solution:
|
||||
def trap(self, height: List[int]) -> int:
|
||||
@ -490,7 +500,8 @@ class Solution:
|
||||
res += res1
|
||||
return res
|
||||
```
|
||||
动态规划
|
||||
|
||||
双指针:
|
||||
```python
|
||||
class Solution:
|
||||
def trap(self, height: List[int]) -> int:
|
||||
@ -602,7 +613,7 @@ func trap(height []int) int {
|
||||
}
|
||||
```
|
||||
|
||||
动态规划解法:
|
||||
双指针解法:
|
||||
|
||||
```go
|
||||
func trap(height []int) int {
|
||||
@ -681,7 +692,7 @@ func min(x, y int) int {
|
||||
### JavaScript:
|
||||
|
||||
```javascript
|
||||
//双指针
|
||||
//暴力解法
|
||||
var trap = function(height) {
|
||||
const len = height.length;
|
||||
let sum = 0;
|
||||
@ -702,7 +713,7 @@ var trap = function(height) {
|
||||
return sum;
|
||||
};
|
||||
|
||||
//动态规划
|
||||
//双指针
|
||||
var trap = function(height) {
|
||||
const len = height.length;
|
||||
if(len <= 2) return 0;
|
||||
@ -782,7 +793,7 @@ var trap = function(height) {
|
||||
|
||||
### TypeScript
|
||||
|
||||
双指针法:
|
||||
暴力解法:
|
||||
|
||||
```typescript
|
||||
function trap(height: number[]): number {
|
||||
@ -809,7 +820,7 @@ function trap(height: number[]): number {
|
||||
};
|
||||
```
|
||||
|
||||
动态规划:
|
||||
双指针:
|
||||
|
||||
```typescript
|
||||
function trap(height: number[]): number {
|
||||
|
@ -17,6 +17,12 @@
|
||||
输出:2
|
||||
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
|
||||
|
||||
提示:
|
||||
|
||||
* 1 <= target <= 10^9
|
||||
* 1 <= nums.length <= 10^5
|
||||
* 1 <= nums[i] <= 10^5
|
||||
|
||||
# 思路
|
||||
|
||||
为了易于大家理解,我特意录制了B站视频[拿下滑动窗口! | LeetCode 209 长度最小的子数组](https://www.bilibili.com/video/BV1tZ4y1q7XE),结合视频看本题解,事半功倍!
|
||||
|
@ -82,7 +82,7 @@ for (int i = 0; i < nums1.size(); i++) {
|
||||
|
||||
栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。只要保持递增,才能找到右边第一个比自己大的元素。
|
||||
|
||||
可能这里有一些同学不理解,那么可以自己尝试一下用递减栈,能不能求出来。其实递减栈就是求右边第一个比自己小的元素了。
|
||||
可能这里有一些同学不理解,那么可以自己尝试一下用递减栈,能不能求出来。**其实递减栈就是求右边第一个比自己小的元素了**。
|
||||
|
||||
|
||||
接下来就要分析如下三种情况,一定要分析清楚。
|
||||
@ -101,7 +101,7 @@ for (int i = 0; i < nums1.size(); i++) {
|
||||
|
||||
判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。
|
||||
|
||||
记录结果这块逻辑有一点小绕,要清楚,此时栈顶元素在nums2中右面第一个大的元素是nums2[i]即当前遍历元素。
|
||||
记录结果这块逻辑有一点小绕,要清楚,此时栈顶元素在nums2数组中右面第一个大的元素是nums2[i](即当前遍历元素)。
|
||||
|
||||
代码如下:
|
||||
|
||||
@ -116,7 +116,7 @@ while (!st.empty() && nums2[i] > nums2[st.top()]) {
|
||||
st.push(i);
|
||||
```
|
||||
|
||||
以上分析完毕,C++代码如下:
|
||||
以上分析完毕,C++代码如下:(其实本题代码和 [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 是基本差不多的)
|
||||
|
||||
|
||||
```CPP
|
||||
|
@ -16,6 +16,11 @@
|
||||
* 输出: [2,-1,2]
|
||||
* 解释: 第一个 1 的下一个更大的数是 2;数字 2 找不到下一个更大的数;第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
|
||||
|
||||
提示:
|
||||
|
||||
* 1 <= nums.length <= 10^4
|
||||
* -10^9 <= nums[i] <= 10^9
|
||||
|
||||
|
||||
# 思路
|
||||
|
||||
@ -23,7 +28,7 @@
|
||||
|
||||
这道题和[739. 每日温度](https://programmercarl.com/0739.每日温度.html)也几乎如出一辙。
|
||||
|
||||
不同的时候本题要循环数组了。
|
||||
不过,本题要循环数组了。
|
||||
|
||||
关于单调栈的讲解我在题解[739. 每日温度](https://programmercarl.com/0739.每日温度.html)中已经详细讲解了。
|
||||
|
||||
@ -33,7 +38,7 @@
|
||||
|
||||
确实可以!
|
||||
|
||||
讲两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。
|
||||
将两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。
|
||||
|
||||
代码如下:
|
||||
|
||||
@ -51,12 +56,17 @@ public:
|
||||
|
||||
// 开始单调栈
|
||||
stack<int> st;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
while (!st.empty() && nums[i] > nums[st.top()]) {
|
||||
result[st.top()] = nums[i];
|
||||
st.pop();
|
||||
st.push(0);
|
||||
for (int i = 1; i < nums.size(); i++) {
|
||||
if (nums[i] < nums[st.top()]) st.push(i);
|
||||
else if (nums[i] == nums[st.top()]) st.push(i);
|
||||
else {
|
||||
while (!st.empty() && nums[i] > nums[st.top()]) {
|
||||
result[st.top()] = nums[i];
|
||||
st.pop();
|
||||
}
|
||||
st.push(i);
|
||||
}
|
||||
st.push(i);
|
||||
}
|
||||
// 最后再把结果集即result数组resize到原数组大小
|
||||
result.resize(nums.size() / 2);
|
||||
@ -74,6 +84,36 @@ resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了
|
||||
|
||||
代码如下:
|
||||
|
||||
```CPP
|
||||
// 版本二
|
||||
class Solution {
|
||||
public:
|
||||
vector<int> nextGreaterElements(vector<int>& nums) {
|
||||
vector<int> result(nums.size(), -1);
|
||||
if (nums.size() == 0) return result;
|
||||
stack<int> st;
|
||||
st.push(0);
|
||||
for (int i = 1; i < nums.size() * 2; i++) {
|
||||
// 模拟遍历两边nums,注意一下都是用i % nums.size()来操作
|
||||
if (nums[i % nums.size()] < nums[st.top()]) st.push(i % nums.size());
|
||||
else if (nums[i % nums.size()] == nums[st.top()]) st.push(i % nums.size());
|
||||
else {
|
||||
while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
|
||||
result[st.top()] = nums[i % nums.size()];
|
||||
st.pop();
|
||||
}
|
||||
st.push(i % nums.size());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
可以版本二不仅代码精简了,也比版本一少做了无用功!
|
||||
|
||||
最后在给出 单调栈的精简版本,即三种情况都做了合并的操作。
|
||||
|
||||
```CPP
|
||||
// 版本二
|
||||
class Solution {
|
||||
@ -95,7 +135,6 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
可以版本二不仅代码精简了,也比版本一少做了无用功!
|
||||
|
||||
## 其他语言版本
|
||||
|
||||
|
@ -78,7 +78,7 @@ public:
|
||||
// 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
|
||||
if (prices[i] > minPrice + fee) {
|
||||
result += prices[i] - minPrice - fee;
|
||||
minPrice = prices[i] - fee; // 情况一,这一步很关键
|
||||
minPrice = prices[i] - fee; // 情况一,这一步很关键,避免重复扣手续费
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -32,6 +32,9 @@
|
||||
|
||||
**单调栈的本质是空间换时间**,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。
|
||||
|
||||
**更直白来说,就是用一个栈来记录我们遍历过的元素**,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。
|
||||
|
||||
|
||||
在使用单调栈的时候首先要明确如下几点:
|
||||
|
||||
1. 单调栈里存放的元素是什么?
|
||||
@ -44,7 +47,9 @@
|
||||
|
||||
这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,栈里要加入一个元素i的时候,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。
|
||||
|
||||
文字描述理解起来有点费劲,接下来我画了一系列的图,来讲解单调栈的工作过程。
|
||||
即:如果求一个元素右边第一个更大元素,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。
|
||||
|
||||
文字描述理解起来有点费劲,接下来我画了一系列的图,来讲解单调栈的工作过程,大家再去思考,本题为什么是递增栈。
|
||||
|
||||
使用单调栈主要有三个判断条件。
|
||||
|
||||
@ -56,50 +61,74 @@
|
||||
|
||||
接下来我们用temperatures = [73, 74, 75, 71, 71, 72, 76, 73]为例来逐步分析,输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
|
||||
|
||||
-------
|
||||
|
||||
首先先将第一个遍历元素加入单调栈
|
||||
|
||||

|
||||
|
||||
---------
|
||||
|
||||
加入T[1] = 74,因为T[1] > T[0](当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况)。
|
||||
|
||||
我们要保持一个递增单调栈(从栈头到栈底),所以将T[0]弹出,T[1]加入,此时result数组可以记录了,result[0] = 1,即T[0]右面第一个比T[0]大的元素是T[1]。
|
||||
|
||||

|
||||
|
||||
-----------
|
||||
|
||||
加入T[2],同理,T[1]弹出
|
||||
|
||||

|
||||
|
||||
-------
|
||||
|
||||
加入T[3],T[3] < T[2] (当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况),加T[3]加入单调栈。
|
||||
|
||||

|
||||
|
||||
---------
|
||||
|
||||
加入T[4],T[4] == T[3] (当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况),此时依然要加入栈,不用计算距离,因为我们要求的是右面第一个大于本元素的位置,而不是大于等于!
|
||||
|
||||

|
||||
|
||||
---------
|
||||
|
||||
加入T[5],T[5] > T[4] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[4]弹出,同时计算距离,更新result
|
||||

|
||||
|
||||
----------
|
||||
|
||||
T[4]弹出之后, T[5] > T[3] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[3]继续弹出,同时计算距离,更新result
|
||||

|
||||
|
||||
-------
|
||||
|
||||
直到发现T[5]小于T[st.top()],终止弹出,将T[5]加入单调栈
|
||||
|
||||

|
||||
|
||||
-------
|
||||
|
||||
加入T[6],同理,需要将栈里的T[5],T[2]弹出
|
||||
|
||||

|
||||
|
||||
-------
|
||||
|
||||
同理,继续弹出
|
||||
|
||||

|
||||
|
||||
------
|
||||
|
||||
此时栈里只剩下了T[6]
|
||||
|
||||

|
||||
|
||||
------------
|
||||
|
||||
加入T[7], T[7] < T[6] 直接入栈,这就是最后的情况,result数组也更新完了。
|
||||
|
||||

|
||||
@ -115,6 +144,8 @@ T[4]弹出之后, T[5] > T[3] (当前遍历的元素T[i]大于栈顶元素T[
|
||||
* 情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
|
||||
* 情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
|
||||
|
||||
通过以上过程,大家可以自己再模拟一遍,就会发现:只有单调栈递增(从栈口到栈底顺序),就是求右边第一个比自己大的,单调栈递减的话,就是求右边第一个比自己小的。
|
||||
|
||||
C++代码如下:
|
||||
|
||||
```CPP
|
||||
|
@ -121,7 +121,7 @@
|
||||
|
||||
这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[青](https://wx.zsxq.com/dweb2/index/footprint/185251215558842),所画,总结的非常好,分享给大家。
|
||||
|
||||
这已经是全网对动规最深刻的讲解系列了。
|
||||
这应该是全网对动规最深刻的讲解系列了。
|
||||
|
||||
**其实大家去网上搜一搜也可以发现,能把动态规划讲清楚的资料挺少的,因为动规确实很难!要给别人讲清楚更难!**
|
||||
|
||||
@ -129,12 +129,7 @@
|
||||
|
||||
讲清楚一道题容易,讲清楚两道题也容易,但把整个动态规划的各个分支讲清楚,每道题目讲通透,并用一套方法论把整个动规贯彻始终就非常难了。
|
||||
|
||||
所以Carl花费的这么大精力,把自己对动规算法理解 一五一十的全部分享给了录友们,帮助大家少走弯路!
|
||||
|
||||
**至于动态规划PDF,即将在公众号「代码随想录」全网首发!**
|
||||
|
||||
最后感谢录友们的一路支持,Carl才有继续更下去的动力[玫瑰],[撒花]
|
||||
|
||||
所以Carl花费的这么大精力,把自己对动规算法理解 一五一十的全部分享给了录友们,帮助大家少走弯路,加油!
|
||||
|
||||
|
||||
<p align="center">
|
||||
|
Reference in New Issue
Block a user