162 寻找峰值
2题意解读
3看一下灵神的思路:
4
5首先读懂题目,为什么要规定 nums[−1]=nums[n]=−∞?也就是假设 nums[0] 的左边还有一个 −∞,nums[n−1] 的右边还有一个 −∞。
6
7因为这可以保证数组一定有峰值。比如数组是严格递减的,那么 nums[0] 就是(唯一的)峰值。为什么?因为此时 nums[−1]<nums[0]>nums[1]。注意 −∞ 可以保证 nums[0] 一定比它左边的数大。同理,如果数组是严格递增的,那么 nums[n−1] 就是(唯一的)峰值。
8
9性质分析
10定理:如果 i<n−1 且 nums[i]<nums[i+1],那么在下标 [i+1,n−1] 中一定存在峰值。
11
12证明:反证法,假设下标 [i+1,n−1] 中没有峰值。
13
14由于 i+1 不是峰值且 nums[i]<nums[i+1],所以一定有 nums[i+1]<nums[i+2] 成立,否则 i+1 就是峰值了。注意题目保证相邻元素不同,不存在相邻元素相等的情况。
15由于 i+2 不是峰值且 nums[i+1]<nums[i+2],所以一定有 nums[i+2]<nums[i+3] 成立,否则 i+2 就是峰值了。
16
17依此类推,得
18nums[i]<nums[i+1]<nums[i+2]<⋯<nums[n−1]>nums[n]=−∞
19这意味着 nums[n−1] 是峰值,矛盾,所以原命题成立。
20同理可得,如果 i<n−1 且 nums[i]>nums[i+1],那么在 [0,i] 中一定存在峰值。
21
22所以,通过比较 nums[i] 和 nums[i+1] 的大小关系,可以二分找到峰值。
23
24二分定义
25下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的,喜欢哪种写法就用哪种。
26
27循环不变量:
28
29>left 的下标中存在峰值下标。
30≤right 的下标中存在峰值下标。
31换句话说,任意时刻均满足 (left,right] 中存在峰值下标。
32在二分过程中,我们能确定的是区间 (left,right] 里面一定有峰值。区间外面有没有峰值?可能有,也可能没有。
33
34常见误区:很多同学会把二分范围与答案范围混为一谈。请注意,二分范围 (left,right) 与答案范围 (left,right] 是不一样的。比如二分循环结束的时候,二分范围是空的,你总不能说答案在空区间里面吧!
35
36不是因为开区间只能找到一个峰值
37
38而是因为二分不断缩小范围
39
40最后答案区间只剩一个位置
41
42这个位置恰好是某个峰值
43
44所以返回 right(或者对应模板里的 left+1)
45
46只能保证找到一个峰值,所以最后要返回left+1=right的时候,就是返回right
47
48开区间二分循环结束后 left+1=right,由于 (left,right] 中只有 right 一个数,所以答案是 right。
49
50常见误区:如果有多个峰值,我们无法在一开始、以及二分过程中就确定哪个峰值最终会成为答案。二分的思路只是不断地缩小范围,并最终找到其中的一个峰值。尤其在二分过程中,nums[i]<nums[i+1] 并不意味着 i 右边的第一个峰值一定会是最终答案。
51
52细节
53二分范围(注意是二分范围不是答案范围)是开区间 (−1,n−1),也就是闭区间 [0,n−2]。
54
55为什么二分范围不用包含 n−1?
56
57这是因为,如果有且仅有一个峰值,且其下标是 n−1,那么一定有
58
59nums[0]<nums[1]<nums[2]<⋯<nums[n−1]
60这意味着每次二分更新的都是 left,最终答案自然就是 n−1。
61
62注意本题答案是存在的,即 [0,n−1] 中一定存在峰值,这可以用上文中的反证法证明。
63
64class Solution:
65 def findPeakElement(self, nums: List[int]) -> int:
66 left, right = -1, len(nums) - 1 # 开区间 (-1, n-1)
67 while left + 1 < right: # 开区间不为空
68 mid = (left + right) // 2
69 if nums[mid] > nums[mid + 1]: # 下坡,峰顶位置 <= mid
70 right = mid
71 else: # 上坡,峰顶位置 > mid
72 left = mid
73 return right
74复杂度分析
75时间复杂度:O(logn),其中 n 是 nums 的长度。
76空间复杂度:O(1)。
77
781. 前提条件
79
80题目给一个数组:
81
82int* nums;
83int numsSize;
84
85峰值定义:
86nums[i] > nums[i-1]
87并且
88nums[i] > nums[i+1]
89
90题目还认为:
91
92nums[-1] = -∞
93nums[n] = -∞
94
95所以边界也可能是峰值。
96
972. 核心概念
98
99看相邻两个数:
100
101nums[mid] 和 nums[mid+1]
102
103如果:
104
105nums[mid] < nums[mid+1]
106
107说明现在是上坡:
108
109mid -> mid+1
110
111那右边一定存在峰值。
112
113如果:
114
115nums[mid] > nums[mid+1]
116
117说明现在是下坡:
118
119mid -> mid+1
120
121那左边,包括 mid,一定存在峰值。
122
1233. 为什么上坡右边一定有峰值?
124
125比如:
126
1271 2 3 4
128
129一直上坡,最后一个就是峰值,因为右边是 -∞。
130
131如果中途开始下降:
132
1331 2 5 3
134
135转折点 5 就是峰值。
136
137所以:
138只要 nums[mid] < nums[mid+1]
139右边一定有峰值。
140
1414. 为什么下坡左边一定有峰值?
142
143比如:
1444 3 2 1
145
146一直下坡,第一个就是峰值,因为左边是 -∞。
147
148如果左边曾经上升又下降:
149
1501 5 3 2
151
152转折点 5 就是峰值。
153
154所以:
155只要 nums[mid] > nums[mid+1]
156左边一定有峰值。
157
1585. 二分模板
159
160这里用闭区间:[left, right]
161
162每次比较:
163
164nums[mid] 和 nums[mid+1]
165
166所以 mid+1 不能越界。
167
168因此循环条件写:
169
170while (left < right)
171
1726. C 代码
173int findPeakElement(int* nums, int numsSize) {
174 int left = 0;
175 int right = numsSize - 1;
176
177 while (left < right) {
178 int mid = left + (right - left) / 2;
179
180 if (nums[mid] < nums[mid + 1]) {
181 left = mid + 1;
182 } else {
183 right = mid;
184 }
185 }
186
187 return left;
188}
189这里是保证最后的取等条件是left=right,所以返回哪个都无所谓
190
1917. 例题流程
192nums = [1,2,1,3,5,6,4]
193
194初始:
195
196left = 0
197right = 6
198第一轮
199mid = 3
200nums[3] = 3
201nums[4] = 5
202
203因为:
204
2053 < 5
206
207说明右边有峰值。
208
209所以:
210
211left = mid + 1 = 4
212
213范围变成:
214
215[4,6]
216第二轮
217mid = 5
218nums[5] = 6
219nums[6] = 4
220
221因为:
222
2236 > 4
224
225说明左边有峰值,包括 mid。
226
227所以:
228
229right = mid = 5
230
231范围变成:
232
233[4,5]
234第三轮
235mid = 4
236nums[4] = 5
237nums[5] = 6
238
239因为:
240
2415 < 6
242
243右边有峰值。
244
245所以:
246
247left = 5
248
249结束:
250
251left = right = 5
252
253返回:
254
2555
256
257对应:
258
259nums[5] = 6
260
2618. 易错点
262易错点 1:不是找最大值
263
264峰值不一定是全局最大。
265
266例如:
267
268[1,2,1,3,5,6,4]
269
270峰值可以是:
271
2722 或 6
273
274返回任意一个峰值都行。
275
276易错点 2:比较的是 mid 和 mid+1
277
278不是直接判断:
279
280nums[mid] > nums[mid-1] && nums[mid] > nums[mid+1]
281
282二分的核心是判断哪边一定有峰值。
283
284易错点 3:right = mid,不是 mid - 1
285
286当:
287
288nums[mid] > nums[mid+1]
289
290mid 自己就可能是峰值。
291
292所以不能丢掉 mid。
293
294正确:right = mid;
295
2969. 总结
297核心:
298比较 nums[mid] 和 nums[mid+1]
299
300如果 nums[mid] < nums[mid+1]:
301
302说明右边是上坡方向
303
304右边一定有峰值
305
306left = mid + 1
307
308如果 nums[mid] > nums[mid+1]:
309
310说明左边是下坡方向
311
312左边一定有峰值
313
314right = mid
315
316--------------------------------
317
318循环:
319
320while(left < right)
321
322--------------------------------
323
324答案:left 或 right
325
326--------------------------------
327
328每次保留一个一定存在峰值的区间。
329
330寻找峰值的二分不是找某个确定值,而是根据坡度方向,保留一定有峰值的一半。
62 寻找峰值
题意解读
看一下灵神的思路:
首先读懂题目,为什么要规定 nums[−1]=nums[n]=−∞?也就是假设 nums[0] 的左边还有一个 −∞,nums[n−1] 的右边还有一个 −∞。
因为这可以保证数组一定有峰值。比如数组是严格递减的,那么 nums[0] 就是(唯一的)峰值。为什么?因为此时 nums[−1]<nums[0]>nums[1]。注意 −∞ 可以保证 nums[0] 一定比它左边的数大。同理,如果数组是严格递增的,那么 nums[n−1] 就是(唯一的)峰值。
性质分析
定理:如果 i<n−1 且 nums[i]<nums[i+1],那么在下标 [i+1,n−1] 中一定存在峰值。
证明:反证法,假设下标 [i+1,n−1] 中没有峰值。
由于 i+1 不是峰值且 nums[i]<nums[i+1],所以一定有 nums[i+1]<nums[i+2] 成立,否则 i+1 就是峰值了。注意题目保证相邻元素不同,不存在相邻元素相等的情况。
由于 i+2 不是峰值且 nums[i+1]<nums[i+2],所以一定有 nums[i+2]<nums[i+3] 成立,否则 i+2 就是峰值了。
依此类推,得
nums[i]<nums[i+1]<nums[i+2]<⋯<nums[n−1]>nums[n]=−∞
这意味着 nums[n−1] 是峰值,矛盾,所以原命题成立。
同理可得,如果 i<n−1 且 nums[i]>nums[i+1],那么在 [0,i] 中一定存在峰值。
所以,通过比较 nums[i] 和 nums[i+1] 的大小关系,可以二分找到峰值。
二分定义
下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的,喜欢哪种写法就用哪种。
循环不变量:
left 的下标中存在峰值下标。
≤right 的下标中存在峰值下标。
换句话说,任意时刻均满足 (left,right] 中存在峰值下标。
在二分过程中,我们能确定的是区间 (left,right] 里面一定有峰值。区间外面有没有峰值?可能有,也可能没有。
常见误区:很多同学会把二分范围与答案范围混为一谈。请注意,二分范围 (left,right) 与答案范围 (left,right] 是不一样的。比如二分循环结束的时候,二分范围是空的,你总不能说答案在空区间里面吧!
不是因为开区间只能找到一个峰值
而是因为二分不断缩小范围
最后答案区间只剩一个位置
这个位置恰好是某个峰值
所以返回 right(或者对应模板里的 left+1)
只能保证找到一个峰值,所以最后要返回left+1=right的时候,就是返回right
开区间二分循环结束后 left+1=right,由于 (left,right] 中只有 right 一个数,所以答案是 right。
常见误区:如果有多个峰值,我们无法在一开始、以及二分过程中就确定哪个峰值最终会成为答案。二分的思路只是不断地缩小范围,并最终找到其中的一个峰值。尤其在二分过程中,nums[i]<nums[i+1] 并不意味着 i 右边的第一个峰值一定会是最终答案。
细节
二分范围(注意是二分范围不是答案范围)是开区间 (−1,n−1),也就是闭区间 [0,n−2]。
为什么二分范围不用包含 n−1?
这是因为,如果有且仅有一个峰值,且其下标是 n−1,那么一定有
nums[0]<nums[1]<nums[2]<⋯<nums[n−1]
这意味着每次二分更新的都是 left,最终答案自然就是 n−1。
注意本题答案是存在的,即 [0,n−1] 中一定存在峰值,这可以用上文中的反证法证明。
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left, right = -1, len(nums) - 1 # 开区间 (-1, n-1)
while left + 1 < right: # 开区间不为空
mid = (left + right) // 2
if nums[mid] > nums[mid + 1]: # 下坡,峰顶位置 <= mid
right = mid
else: # 上坡,峰顶位置 > mid
left = mid
return right
复杂度分析
时间复杂度:O(logn),其中 n 是 nums 的长度。
空间复杂度:O(1)。
- 前提条件
题目给一个数组:
int* nums;
int numsSize;
峰值定义:
nums[i] > nums[i-1]
并且
nums[i] > nums[i+1]
题目还认为:
nums[-1] = -∞
nums[n] = -∞
所以边界也可能是峰值。
- 核心概念
看相邻两个数:
nums[mid] 和 nums[mid+1]
如果:
nums[mid] < nums[mid+1]
说明现在是上坡:
mid -> mid+1
那右边一定存在峰值。
如果:
nums[mid] > nums[mid+1]
说明现在是下坡:
mid -> mid+1
那左边,包括 mid,一定存在峰值。
- 为什么上坡右边一定有峰值?
比如:
1 2 3 4
一直上坡,最后一个就是峰值,因为右边是 -∞。
如果中途开始下降:
1 2 5 3
转折点 5 就是峰值。
所以:
只要 nums[mid] < nums[mid+1]
右边一定有峰值。
- 为什么下坡左边一定有峰值?
比如:
4 3 2 1
一直下坡,第一个就是峰值,因为左边是 -∞。
如果左边曾经上升又下降:
1 5 3 2
转折点 5 就是峰值。
所以:
只要 nums[mid] > nums[mid+1]
左边一定有峰值。
- 二分模板
这里用闭区间:[left, right]
每次比较:
nums[mid] 和 nums[mid+1]
所以 mid+1 不能越界。
因此循环条件写:
while (left < right)
-
C 代码
int findPeakElement(int* nums, int numsSize) {
int left = 0;
int right = numsSize - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < nums[mid + 1]) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
这里是保证最后的取等条件是left=right,所以返回哪个都无所谓
-
例题流程
nums = [1,2,1,3,5,6,4]
初始:
left = 0
right = 6
第一轮
mid = 3
nums[3] = 3
nums[4] = 5
因为:
3 < 5
说明右边有峰值。
所以:
left = mid + 1 = 4
范围变成:
[4,6]
第二轮
mid = 5
nums[5] = 6
nums[6] = 4
因为:
6 > 4
说明左边有峰值,包括 mid。
所以:
right = mid = 5
范围变成:
[4,5]
第三轮
mid = 4
nums[4] = 5
nums[5] = 6
因为:
5 < 6
右边有峰值。
所以:
left = 5
结束:
left = right = 5
返回:
5
对应:
nums[5] = 6
- 易错点
易错点 1:不是找最大值
峰值不一定是全局最大。
例如:
[1,2,1,3,5,6,4]
峰值可以是:
2 或 6
返回任意一个峰值都行。
易错点 2:比较的是 mid 和 mid+1
不是直接判断:
nums[mid] > nums[mid-1] && nums[mid] > nums[mid+1]
二分的核心是判断哪边一定有峰值。
易错点 3:right = mid,不是 mid - 1
当:
nums[mid] > nums[mid+1]
mid 自己就可能是峰值。
所以不能丢掉 mid。
正确:right = mid;
- 总结
核心:
比较 nums[mid] 和 nums[mid+1]
如果 nums[mid] < nums[mid+1]:
说明右边是上坡方向
右边一定有峰值
left = mid + 1
如果 nums[mid] > nums[mid+1]:
说明左边是下坡方向
左边一定有峰值
right = mid
循环:
while(left < right)
答案:left 或 right
每次保留一个一定存在峰值的区间。
寻找峰值的二分不是找某个确定值,而是根据坡度方向,保留一定有峰值的一半。