返回 LeetCode 刷题

Markdown File

单调栈

单调栈.md

1调栈(Monotonic Stack)
21. 前提条件
3
4单调栈适用于这一类题,对于数组中每个元素:
5
6寻找
7
8左边第一个比它大
9
10左边第一个比它小
11
12右边第一个比它大
13
14右边第一个比它小
15
16例如:
17
18739 每日温度
19
20496 下一个更大元素
21
22503 下一个更大元素 II
23
2484 柱状图最大矩形
25
26这些题都有一个共同特点:找最近满足条件的元素
27
282. 为什么需要单调栈?
29
30例如:
31
3273 74 75 71 69 72 76 73
33
34题目:对于每一天
35
36找右边第一个更高温度
37
38例如:
39
4073
41
42
43
4474
45
46答案1
47
48再例如:
49
5071
51
52
53
5472
55
56答案:2
57
58最容易想到双循环
59
60例如:
61
62for(i=0;i<n;i++)
63{
64 for(j=i+1;j<n;j++)
65 {
66 ...
67 }
68}
69
70复杂度O(n²)
71
72如果n=100000就超时。
73
74所以需要O(n)
75
763. 核心思想
77
78栈里面保存还没有找到答案的元素
79
80例如数组:
81
8273 74 75
83
84开始:73
85
86右边不知道有没有更大的。
87
88于是:73入栈。
89
90继续:74
91
92发现:74>73
93
94说明:73终于找到答案。
95
96于是:73出栈。
97
98然后:74自己还不知道未来有没有更大的。
99
100继续入栈。
101
102这就是单调栈。
103
1044. 为什么叫"单调"?
105
106因为:栈里面一直保持一种顺序。
107
108例如每日温度:
109
110维护从栈底到栈顶单调递减
111
112例如:
113
11480
115
11675
117
11873
119
120一直越来越小。
121
122为什么?
123
124因为来了76
125
126以后:
127
12880
129
13075
131
13273
133
13473:没用了。
135弹。
136
137得到:
138
13980
140
14175
142
14376:
144
145继续比75大。
146
14775:也弹。
148
149得到:80
150
151最后:76:
152
153入栈。
154
155得到:
156
15780
158
15976
160
161还是:单调递减。
162
163所以叫单调递减栈。
164
1655. 栈里面放什么?
166
167这是最容易错的地方。
168
169不是温度。而是下标
170
171例如:
172
17373 74 75
174
175栈:
176
177放:
178
1790
180
1811
182
1832
184
185为什么?
186
187因为最后答案要求距离。
188
189例如2-0=2
190
191如果只存73
192不知道它在哪里。
193
194所以必须存下标。
195
1966. 固定模板
197for(int i=0;i<n;i++)
198{
199 while(top!=-1 &&
200 nums[i]>nums[stack[top]])
201 {
202 int index=stack[top--];
203
204 ans[index]=i-index;
205 }
206
207 stack[++top]=i;
208}
209
210以后看到右边第一个更大基本就是这个。
211
212
2137. 每日温度代码(C)
214int* dailyTemperatures(int* temperatures,
215 int temperaturesSize,
216 int* returnSize)
217{
218 *returnSize = temperaturesSize;
219
220 int* ans =
221 calloc(temperaturesSize,sizeof(int));
222
223 int stack[100001];
224
225 int top=-1;
226
227 for(int i=0;i<temperaturesSize;i++)
228 {
229 while(top!=-1 &&
230 temperatures[i]>
231 temperatures[stack[top]])
232 {
233 int index=stack[top--];
234
235 ans[index]=i-index;
236 }
237
238 stack[++top]=i;
239 }
240
241 return ans;
242}
2438. 详细举例
244数组:
245
24673 74 75 71 69 72 76 73
247
248下标:
249
2500 1 2 3 4 5 6 7
251
252第一步
253i=0
254
25573
256
257栈:空。
258
259直接入栈。
260
261stack
262
263
264
2650
266
267第二步
268i=1
269
27074
271
272比较:
273
27474>73
275
276说明:
277
27873找到更高温度。
279
280弹:0
281
282答案:ans[0]=
283
2841-0=1
285
286然后:
287
2881:
289
290入栈。
291
292stack
293
294
295
2961
297
298第三步
299i=2
300
30175
302
303比较:
304
30575>74
306
307弹:
308
3091
310
311得到:
312
313ans[1]=2-1=1
314
315然后:
316
3172:
318
319入栈。
320
321stack
322
323
324
3252
326
327第四步
32871
329
330比较:
331
33271>75?
333
334不是。
335
336直接入栈。
337
3382
339
3403
341
342注意:
343
344这里栈:
345
346对应:
347
34875
349
35071
351
352还是递减。
353
354第五步
355
35669:
357
358继续:
359
36075
361
36271
363
36469
365
366--第六步
367
368来了:72
369
370比较:
371
37272>69
373
374弹:69。
375
376答案:5-4=1
377
378继续:72>71
379
380再弹。
381
382答案:5-3=2
383
384继续:72>75
385
386不是。
387停止。
388
389然后:72:
390
391入栈。
392
393现在:
394
395栈:
396
39775
398
39972
400
401对应:
402
403下标:
404
4052
406
4075
408
409第七步
410
411来了:76
412
413一直弹。
414
41576>72
416
417
418
41976>75
420
421
422
423得到:
424
425ans[5]=1
426
427ans[2]=4
428
429最后:76:
430
431入栈。
432
433最后:
434
435得到:
436
4371
438
4391
440
4414
442
4432
444
4451
446
4471
448
4490
450
4510
452
453和题目:一致。
454
4559. 为什么是 while?
456
457例如:
458
45980
460
46175
462
46373
464
465来了:
466
46790
468
469如果:
470
471if(...)
472
473只能:
474
475弹:
476
47773
478
479但是:
480
48190:
482
483还:
484
485大于:
486
48775。
488
489还:
490
491大于:
492
49380。
494
495所以:
496
497必须:
498
499一直弹。
500
501因此:
502
503必须:
504
505while(...)
506
507这和希尔排序那个 while 的思想有点像:满足条件时要连续处理,而不是只处理一次。
508
50910. 时间复杂度为什么 O(n)?
510
511很多人觉得:
512
513while
514
515是不是
516
517O(n²)?
518
519其实:
520
521不是。
522
523因为:
524
525每个元素:
526
527最多:
528
529入栈一次
530
531出栈一次
532
533例如:
534
53573:
536
537入一次
538
539弹一次
540
541结束。
542
543所以:
544
545总操作:
546
5472n
548
549复杂度:
550
551★★★★★
552
553O(n)
55411. 易错点
555① 栈里面放的是下标,不是值。
556
557② while,不是 if。
558
559③ 维护的是单调递减栈(每日温度)。
560
561④ 弹栈时更新的是被弹出的那个元素答案。
562
563⑤ 栈里剩下的元素说明右边没有更大的,答案默认是0。
56412. 单调栈总结(★★★★★)
565单调栈
566
567作用:
568
569寻找
570
571最近更大
572
573最近更小
574
575--------------------------------
576
577每日温度:
578
579维护:
580
581单调递减栈
582
583--------------------------------
584
585栈里:
586
587存下标
588
589--------------------------------
590
591模板:
592
593for(i)
594
595 while(当前更大)
596
597 弹栈
598
599 更新答案
600
601 入栈
602
603--------------------------------
604
605复杂度:
606
607O(n)
608
609因为:
610
611每个元素
612
613最多:
614
615入一次
616
617出一次。
618看一下灵神代码
619写法一:从右到左
620栈中记录下一个更大元素的「候选项」的下标。
621
622每次循环,我们可以在「候选项」中找到答案。
623
624class Solution:
625 def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
626 n = len(temperatures)
627 ans = [0] * n
628 st = []
629 for i in range(n - 1, -1, -1):
630 t = temperatures[i]
631 while st and t >= temperatures[st[-1]]:
632 st.pop()
633 if st:
634 ans[i] = st[-1] - i
635 st.append(i)
636 return ans
637复杂度分析
638时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
639空间复杂度:O(min(n,U)),其中 U=max(temperatures)−min(temperatures)+1。返回值不计入,仅考虑栈的最大空间消耗。
640
641写法二:从左到右
642栈中记录还没算出下一个更大元素的那些数的下标。
643
644相当于栈是一个 todolist,在循环的过程中,现在还不知道答案是多少,在后面的循环中会算出答案。
645
646class Solution:
647 def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
648 n = len(temperatures)
649 ans = [0] * n
650 st = [] # todolist
651 for i, t in enumerate(temperatures):
652 //栈不为空并且该天温度值大于栈顶温度
653 while st and t > temperatures[st[-1]]:
654 j = st.pop()
655 ans[j] = i - j
656 st.append(i)
657 return ans
658复杂度分析
659时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
660空间复杂度:O(n)。注意这种写法栈中可以有重复元素。
661
662接雨水:
663面的方法相当于「竖着」计算面积,单调栈的做法相当于「横着」计算面积。
664
665这个方法可以总结成 16 个字:找上一个更大元素,在找的过程中填坑。
666
667注意 while 中加了等号,这可以让栈中没有重复元素,从而在有很多重复元素的情况下,使用更少的空间。
668
669看复杂度的话,单调栈不如双指针的做法。但如果输入的 height 是一个流(stream),只能从左到右遍历,那么单调栈(在这种场景下)就是不错的方法了。
670
671class Solution:
672 def trap(self, height: List[int]) -> int:
673 ans = 0
674 st = []
675 for i, h in enumerate(height):
676 while st and height[st[-1]] <= h:
677 bottom_h = height[st.pop()]
678 if not st: # 栈是空的
679 break
680 left = st[-1]
681 dh = min(height[left], h) - bottom_h # 面积的高
682 ans += dh * (i - left - 1)
683 st.append(i)
684 return ans
685
686#define MIN(a, b) ((b) < (a) ? (b) : (a))
687
688int trap(int* height, int heightSize) {
689 int ans = 0;
690
691 // 用数组模拟栈,栈大小最多为 heightSize
692 int* st = malloc(sizeof(int) * heightSize);
693 int top = -1; // 栈顶指针,初始为空栈
694
695 for (int i = 0; i < heightSize; i++) {
696 int h = height[i];
697 while (top >= 0 && height[st[top]] <= h) {
698 int bottom_h = height[st[top]];
699 top--; // 出栈
700 if (top < 0) {
701 break;
702 }
703 int left = st[top];
704 int dh = MIN(height[left], height[i]) - bottom_h; // 面积的高
705 ans += dh * (i - left - 1);
706 }
707 st[++top] = i; // 入栈
708 }
709
710 free(st);
711 return ans;
712
713
Rendered Preview

调栈(Monotonic Stack)

  1. 前提条件

单调栈适用于这一类题,对于数组中每个元素:

寻找

左边第一个比它大

左边第一个比它小

右边第一个比它大

右边第一个比它小

例如:

739 每日温度

496 下一个更大元素

503 下一个更大元素 II

84 柱状图最大矩形

这些题都有一个共同特点:找最近满足条件的元素

  1. 为什么需要单调栈?

例如:

73 74 75 71 69 72 76 73

题目:对于每一天

找右边第一个更高温度

例如:

73

74

答案1

再例如:

71

72

答案:2

最容易想到双循环

例如:

for(i=0;i<n;i++)
{
for(j=i+1;j<n;j++)
{
...
}
}

复杂度O(n²)

如果n=100000就超时。

所以需要O(n)

  1. 核心思想

栈里面保存还没有找到答案的元素

例如数组:

73 74 75

开始:73

右边不知道有没有更大的。

于是:73入栈。

继续:74

发现:74>73

说明:73终于找到答案。

于是:73出栈。

然后:74自己还不知道未来有没有更大的。

继续入栈。

这就是单调栈。

  1. 为什么叫"单调"?

因为:栈里面一直保持一种顺序。

例如每日温度:

维护从栈底到栈顶单调递减

例如:

80

75

73

一直越来越小。

为什么?

因为来了76

以后:

80

75

73

73:没用了。
弹。

得到:

80

75

76:

继续比75大。

75:也弹。

得到:80

最后:76:

入栈。

得到:

80

76

还是:单调递减。

所以叫单调递减栈。

  1. 栈里面放什么?

这是最容易错的地方。

不是温度。而是下标

例如:

73 74 75

栈:

放:

0

1

2

为什么?

因为最后答案要求距离。

例如2-0=2

如果只存73
不知道它在哪里。

所以必须存下标。

  1. 固定模板
    for(int i=0;i<n;i++)
    {
    while(top!=-1 &&
    nums[i]>nums[stack[top]])
    {
    int index=stack[top--];

     ans[index]=i-index;
    

    }

    stack[++top]=i;
    }

以后看到右边第一个更大基本就是这个。

  1. 每日温度代码(C)
    int* dailyTemperatures(int* temperatures,
    int temperaturesSize,
    int* returnSize)
    {
    *returnSize = temperaturesSize;

    int* ans =
    calloc(temperaturesSize,sizeof(int));

    int stack[100001];

    int top=-1;

    for(int i=0;i<temperaturesSize;i++)
    {
    while(top!=-1 &&
    temperatures[i]>
    temperatures[stack[top]])
    {
    int index=stack[top--];

         ans[index]=i-index;
     }
    
     stack[++top]=i;
    

    }

    return ans;
    }

  2. 详细举例
    数组:

73 74 75 71 69 72 76 73

下标:

0 1 2 3 4 5 6 7

第一步
i=0

73

栈:空。

直接入栈。

stack

0

第二步
i=1

74

比较:

74>73

说明:

73找到更高温度。

弹:0

答案:ans[0]=

1-0=1

然后:

1:

入栈。

stack

1

第三步
i=2

75

比较:

75>74

弹:

1

得到:

ans[1]=2-1=1

然后:

2:

入栈。

stack

2

第四步
71

比较:

71>75?

不是。

直接入栈。

2

3

注意:

这里栈:

对应:

75

71

还是递减。

第五步

69:

继续:

75

71

69

--第六步

来了:72

比较:

72>69

弹:69。

答案:5-4=1

继续:72>71

再弹。

答案:5-3=2

继续:72>75

不是。
停止。

然后:72:

入栈。

现在:

栈:

75

72

对应:

下标:

2

5

第七步

来了:76

一直弹。

76>72

76>75

得到:

ans[5]=1

ans[2]=4

最后:76:

入栈。

最后:

得到:

1

1

4

2

1

1

0

0

和题目:一致。

  1. 为什么是 while?

例如:

80

75

73

来了:

90

如果:

if(...)

只能:

弹:

73

但是:

90:

还:

大于:

75。

还:

大于:

80。

所以:

必须:

一直弹。

因此:

必须:

while(...)

这和希尔排序那个 while 的思想有点像:满足条件时要连续处理,而不是只处理一次。

  1. 时间复杂度为什么 O(n)?

很多人觉得:

while

是不是

O(n²)?

其实:

不是。

因为:

每个元素:

最多:

入栈一次

出栈一次

例如:

73:

入一次

弹一次

结束。

所以:

总操作:

2n

复杂度:

★★★★★

O(n)
11. 易错点
① 栈里面放的是下标,不是值。

② while,不是 if。

③ 维护的是单调递减栈(每日温度)。

④ 弹栈时更新的是被弹出的那个元素答案。

⑤ 栈里剩下的元素说明右边没有更大的,答案默认是0。
12. 单调栈总结(★★★★★)
单调栈

作用:

寻找

最近更大

最近更小


每日温度:

维护:

单调递减栈


栈里:

存下标


模板:

for(i)

while(当前更大)

    弹栈

    更新答案

入栈

复杂度:

O(n)

因为:

每个元素

最多:

入一次

出一次。
看一下灵神代码
写法一:从右到左
栈中记录下一个更大元素的「候选项」的下标。

每次循环,我们可以在「候选项」中找到答案。

class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
ans = [0] * n
st = []
for i in range(n - 1, -1, -1):
t = temperatures[i]
while st and t >= temperatures[st[-1]]:
st.pop()
if st:
ans[i] = st[-1] - i
st.append(i)
return ans
复杂度分析
时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
空间复杂度:O(min(n,U)),其中 U=max(temperatures)−min(temperatures)+1。返回值不计入,仅考虑栈的最大空间消耗。

写法二:从左到右
栈中记录还没算出下一个更大元素的那些数的下标。

相当于栈是一个 todolist,在循环的过程中,现在还不知道答案是多少,在后面的循环中会算出答案。

class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
ans = [0] * n
st = [] # todolist
for i, t in enumerate(temperatures):
//栈不为空并且该天温度值大于栈顶温度
while st and t > temperatures[st[-1]]:
j = st.pop()
ans[j] = i - j
st.append(i)
return ans
复杂度分析
时间复杂度:O(n),其中 n 为 temperatures 的长度。虽然我们写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此循环次数之和是 O(n),所以时间复杂度是 O(n)。
空间复杂度:O(n)。注意这种写法栈中可以有重复元素。

接雨水:
面的方法相当于「竖着」计算面积,单调栈的做法相当于「横着」计算面积。

这个方法可以总结成 16 个字:找上一个更大元素,在找的过程中填坑。

注意 while 中加了等号,这可以让栈中没有重复元素,从而在有很多重复元素的情况下,使用更少的空间。

看复杂度的话,单调栈不如双指针的做法。但如果输入的 height 是一个流(stream),只能从左到右遍历,那么单调栈(在这种场景下)就是不错的方法了。

class Solution:
def trap(self, height: List[int]) -> int:
ans = 0
st = []
for i, h in enumerate(height):
while st and height[st[-1]] <= h:
bottom_h = height[st.pop()]
if not st: # 栈是空的
break
left = st[-1]
dh = min(height[left], h) - bottom_h # 面积的高
ans += dh * (i - left - 1)
st.append(i)
return ans

#define MIN(a, b) ((b) < (a) ? (b) : (a))

int trap(int* height, int heightSize) {
int ans = 0;

// 用数组模拟栈,栈大小最多为 heightSize
int* st = malloc(sizeof(int) * heightSize);
int top = -1; // 栈顶指针,初始为空栈

for (int i = 0; i < heightSize; i++) {
    int h = height[i];
    while (top >= 0 && height[st[top]] <= h) {
        int bottom_h = height[st[top]];
        top--; // 出栈
        if (top < 0) {
            break;
        }
        int left = st[top];
        int dh = MIN(height[left], height[i]) - bottom_h; // 面积的高
        ans += dh * (i - left - 1);
    }
    st[++top] = i; // 入栈
}

free(st);
return ans;