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
调栈(Monotonic Stack)
- 前提条件
单调栈适用于这一类题,对于数组中每个元素:
寻找
左边第一个比它大
左边第一个比它小
右边第一个比它大
右边第一个比它小
例如:
739 每日温度
496 下一个更大元素
503 下一个更大元素 II
84 柱状图最大矩形
这些题都有一个共同特点:找最近满足条件的元素
- 为什么需要单调栈?
例如:
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)
- 核心思想
栈里面保存还没有找到答案的元素
例如数组:
73 74 75
开始:73
右边不知道有没有更大的。
于是:73入栈。
继续:74
发现:74>73
说明:73终于找到答案。
于是:73出栈。
然后:74自己还不知道未来有没有更大的。
继续入栈。
这就是单调栈。
- 为什么叫"单调"?
因为:栈里面一直保持一种顺序。
例如每日温度:
维护从栈底到栈顶单调递减
例如:
80
75
73
一直越来越小。
为什么?
因为来了76
以后:
80
75
73
73:没用了。
弹。
得到:
80
75
76:
继续比75大。
75:也弹。
得到:80
最后:76:
入栈。
得到:
80
76
还是:单调递减。
所以叫单调递减栈。
- 栈里面放什么?
这是最容易错的地方。
不是温度。而是下标
例如:
73 74 75
栈:
放:
0
1
2
为什么?
因为最后答案要求距离。
例如2-0=2
如果只存73
不知道它在哪里。
所以必须存下标。
-
固定模板
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;
}
以后看到右边第一个更大基本就是这个。
-
每日温度代码(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;
}
-
详细举例
数组:
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
和题目:一致。
- 为什么是 while?
例如:
80
75
73
来了:
90
如果:
if(...)
只能:
弹:
73
但是:
90:
还:
大于:
75。
还:
大于:
80。
所以:
必须:
一直弹。
因此:
必须:
while(...)
这和希尔排序那个 while 的思想有点像:满足条件时要连续处理,而不是只处理一次。
- 时间复杂度为什么 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;