返回 LeetCode 刷题

Markdown File

二分查找

二分查找.md

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寻找峰值的二分不是找某个确定值,而是根据坡度方向,保留一定有峰值的一半。
Rendered Preview

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)。

  1. 前提条件

题目给一个数组:

int* nums;
int numsSize;

峰值定义:
nums[i] > nums[i-1]
并且
nums[i] > nums[i+1]

题目还认为:

nums[-1] = -∞
nums[n] = -∞

所以边界也可能是峰值。

  1. 核心概念

看相邻两个数:

nums[mid] 和 nums[mid+1]

如果:

nums[mid] < nums[mid+1]

说明现在是上坡:

mid -> mid+1

那右边一定存在峰值。

如果:

nums[mid] > nums[mid+1]

说明现在是下坡:

mid -> mid+1

那左边,包括 mid,一定存在峰值。

  1. 为什么上坡右边一定有峰值?

比如:

1 2 3 4

一直上坡,最后一个就是峰值,因为右边是 -∞。

如果中途开始下降:

1 2 5 3

转折点 5 就是峰值。

所以:
只要 nums[mid] < nums[mid+1]
右边一定有峰值。

  1. 为什么下坡左边一定有峰值?

比如:
4 3 2 1

一直下坡,第一个就是峰值,因为左边是 -∞。

如果左边曾经上升又下降:

1 5 3 2

转折点 5 就是峰值。

所以:
只要 nums[mid] > nums[mid+1]
左边一定有峰值。

  1. 二分模板

这里用闭区间:[left, right]

每次比较:

nums[mid] 和 nums[mid+1]

所以 mid+1 不能越界。

因此循环条件写:

while (left < right)

  1. 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,所以返回哪个都无所谓

  2. 例题流程
    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:不是找最大值

峰值不一定是全局最大。

例如:

[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;

  1. 总结
    核心:
    比较 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


每次保留一个一定存在峰值的区间。

寻找峰值的二分不是找某个确定值,而是根据坡度方向,保留一定有峰值的一半。