1
1
# 二分搜索
2
2
3
- ## 二分搜索模板
3
+ 给一个 ** 有序数组 ** 和目标值,找第一次/最后一次/任何一次出现的索引,时间复杂度 O(logN)。
4
4
5
- 给一个 ** 有序数组 ** 和目标值,找第一次/最后一次/任何一次出现的索引,如果没有出现返回-1
5
+ ## 模板
6
6
7
- 模板四点要素
7
+ 常用的二分搜索模板有如下三种形式:
8
8
9
- - 1、初始化:start=0、end=len-1
10
- - 2、循环退出条件:start + 1 < end
11
- - 3、比较中点和目标值:A[ mid] ==、 <、> target
12
- - 4、判断最后两个元素是否符合:A[ start] 、A[ end] ? target
13
-
14
- 时间复杂度 O(logn),使用场景一般是有序数组的查找
9
+ ![ binary_search_template] ( https://img.fuiboom.com/img/binary_search_template.png )
15
10
16
- 典型示例
11
+ 其中,模板 1 和 3 是最常用的,几乎所有二分查找问题都可以用其中之一轻松实现。模板 2 更高级一些,用于解决某些类型的问题。详细的对比可以参考 Leetcode 上的文章: [ 二分搜索模板 ] ( https://leetcode-cn.com/explore/learn/card/binary-search/212/template-analysis/847/ ) 。
17
12
18
13
### [ binary-search] ( https://leetcode-cn.com/problems/binary-search/ )
19
14
20
15
> 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
21
16
17
+ - 模板 3 的实现
18
+
22
19
``` Python
23
20
class Solution :
24
21
def search (self , nums : List[int ], target : int ) -> int :
25
22
26
23
l, r = 0 , len (nums) - 1
27
24
28
25
while l + 1 < r:
29
- mid = ( l + r ) // 2
26
+ mid = l + (r - l ) // 2
30
27
if nums[mid] < target:
31
28
l = mid
32
29
else :
@@ -40,25 +37,16 @@ class Solution:
40
37
return - 1
41
38
```
42
39
43
- 大部分二分查找类的题目都可以用这个模板,然后做一点特殊逻辑即可
44
-
45
- 另外二分查找还有一些其他模板如下图,大部分场景模板 3 都能解决问题,而且还能找第一次/最后一次出现的位置,应用更加广泛
46
-
47
- ![ binary_search_template] ( https://img.fuiboom.com/img/binary_search_template.png )
48
-
49
- 所以用模板#3 就对了,详细的对比可以这边文章介绍:[ 二分搜索模板] ( https://leetcode-cn.com/explore/learn/card/binary-search/212/template-analysis/847/ )
50
-
51
- - 如果是最简单的二分搜索,不需要找第一个、最后一个位置、或者是没有重复元素,可以使用模板 1,代码更简洁
40
+ - 如果是最简单的二分搜索,不需要找第一个、最后一个位置,或者是没有重复元素,可以使用模板 1,代码更简洁。同时,如果搜索失败,left 是第一个大于 target 的索引,right 是最后一个小于 target 的索引。
52
41
53
42
``` Python
54
- # 无重复元素搜索时,更方便
55
43
class Solution :
56
44
def search (self , nums : List[int ], target : int ) -> int :
57
45
58
46
l, r = 0 , len (nums) - 1
59
47
60
48
while l <= r:
61
- mid = ( l + r ) // 2
49
+ mid = l + (r - l ) // 2
62
50
if nums[mid] == target:
63
51
return mid
64
52
elif nums[mid] > target:
@@ -67,12 +55,9 @@ class Solution:
67
55
l = mid + 1
68
56
69
57
return - 1
70
- # 如果找不到,start 是第一个大于target的索引
71
- # 如果在B+树结构里面二分搜索,可以return start
72
- # 这样可以继续向子节点搜索,如:node:=node.Children[start]
73
58
```
74
59
75
- - 模板 2:
60
+ - 模板 2 的实现
76
61
77
62
``` Python
78
63
class Solution :
@@ -81,7 +66,7 @@ class Solution:
81
66
l, r = 0 , len (nums) - 1
82
67
83
68
while l < r:
84
- mid = ( l + r ) // 2
69
+ mid = l + (r - l ) // 2
85
70
if nums[mid] < target:
86
71
l = mid + 1
87
72
else :
@@ -93,18 +78,15 @@ class Solution:
93
78
return - 1
94
79
```
95
80
96
-
97
-
98
81
## 常见题目
99
82
100
83
### [ find-first-and-last-position-of-element-in-sorted-array] ( https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/ )
101
84
102
85
> 给定一个包含 n 个整数的排序数组,找出给定目标值 target 的起始和结束位置。如果目标值不在数组中,则返回` [-1, -1] `
103
86
104
- - 思路:核心点就是找第一个 target 的索引,和最后一个 target 的索引,所以用两次二分搜索分别找第一次和最后一次的位置
87
+ - 思路:核心点就是找第一个 target 的索引,和最后一个 target 的索引,所以用两次二分搜索分别找第一次和最后一次的位置,下面是使用模板 3 的解法
105
88
106
89
``` Python
107
- # 使用模板3的解法
108
90
class Solution :
109
91
def searchRange (self , nums , target ):
110
92
Range = [- 1 , - 1 ]
@@ -113,7 +95,7 @@ class Solution:
113
95
114
96
l, r = 0 , len (nums) - 1
115
97
while l + 1 < r:
116
- mid = ( l + r ) // 2
98
+ mid = l + (r - l ) // 2
117
99
if nums[mid] < target:
118
100
l = mid
119
101
else :
@@ -128,7 +110,7 @@ class Solution:
128
110
129
111
l, r = 0 , len (nums) - 1
130
112
while l + 1 < r:
131
- mid = ( l + r ) // 2
113
+ mid = l + (r - l ) // 2
132
114
if nums[mid] <= target:
133
115
l = mid
134
116
else :
@@ -153,7 +135,7 @@ class Solution:
153
135
154
136
l, r = 0 , len (nums) - 1
155
137
while l < r:
156
- mid = ( l + r ) // 2
138
+ mid = l + (r - l ) // 2
157
139
if nums[mid] < target:
158
140
l = mid + 1
159
141
else :
@@ -166,7 +148,7 @@ class Solution:
166
148
167
149
l, r = 0 , len (nums) - 1
168
150
while l < r:
169
- mid = ( l + r + 1 ) // 2
151
+ mid = l + (r - l + 1 ) // 2
170
152
if nums[mid] > target:
171
153
r = mid - 1
172
154
else :
@@ -180,7 +162,7 @@ class Solution:
180
162
181
163
> 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
182
164
183
- 思路: 使用模板 1,若不存在,随后的左边界为第一个大于目标值的索引 (插入位置),右边界为最后一个小于目标值的索引
165
+ - 使用模板 1,若不存在,左边界为第一个大于目标值的索引 (插入位置),右边界为最后一个小于目标值的索引
184
166
185
167
``` Python
186
168
class Solution :
@@ -189,7 +171,7 @@ class Solution:
189
171
l, r = 0 , len (nums) - 1
190
172
191
173
while l <= r:
192
- mid = ( l + r ) // 2
174
+ mid = l + (r - l ) // 2
193
175
if nums[mid] == target:
194
176
return mid
195
177
elif nums[mid] > target:
@@ -204,10 +186,11 @@ class Solution:
204
186
205
187
> 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
206
188
>
207
- > - 每行中的整数从左到右按升序排列。
208
- > - 每行的第一个整数大于前一行的最后一个整数。
189
+ > 1 . 每行中的整数从左到右按升序排列。
190
+ >
191
+ > 2 . 每行的第一个整数大于前一行的最后一个整数。
209
192
210
- 思路: 两次二分,首先定位行数,接着定位列数
193
+ - 两次二分,首先定位行数,接着定位列数
211
194
212
195
``` Python
213
196
class Solution :
@@ -219,7 +202,7 @@ class Solution:
219
202
l, r = 0 , len (matrix) - 1
220
203
221
204
while l <= r:
222
- mid = ( l + r ) // 2
205
+ mid = l + (r - l ) // 2
223
206
if matrix[mid][0 ] == target:
224
207
return True
225
208
elif matrix[mid][0 ] < target:
@@ -230,7 +213,7 @@ class Solution:
230
213
row = r
231
214
l, r = 0 , len (matrix[0 ]) - 1
232
215
while l <= r:
233
- mid = ( l + r ) // 2
216
+ mid = l + (r - l ) // 2
234
217
if matrix[row][mid] == target:
235
218
return True
236
219
elif matrix[row][mid] < target:
@@ -241,36 +224,11 @@ class Solution:
241
224
return False
242
225
```
243
226
244
- ### [ first-bad-version] ( https://leetcode-cn.com/problems/first-bad-version/ )
245
-
246
- > 假设你有 n 个版本 [ 1, 2, ..., n] ,你想找出导致之后所有版本出错的第一个错误的版本。
247
- > 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
248
-
249
- ``` Python
250
- class Solution :
251
- def firstBadVersion (self , n ):
252
-
253
- l, r = 1 , n
254
-
255
- while l + 1 < r:
256
- mid = (l + r) // 2
257
- if isBadVersion(mid):
258
- r = mid
259
- else :
260
- l = mid
261
-
262
- if isBadVersion(l):
263
- return l
264
- else :
265
- return r
266
- ```
267
-
268
227
### [ find-minimum-in-rotated-sorted-array] ( https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/ )
269
228
270
- > 假设按照升序排序的数组在预先未知的某个点上进行了旋转( 例如,数组 [ 0,1,2,4,5,6,7] 可能变为 [ 4,5,6,7,0,1,2] )。
271
- > 请找出其中最小的元素。假设数组中无重复元素。
229
+ > 假设按照升序排序的数组在预先未知的某个点上进行了旋转,例如,数组 [ 0, 1, 2, 4, 5, 6, 7] 可能变为 [ 4, 5, 6, 7, 0, 1, 2] 。请找出其中最小的元素。假设数组中无重复元素。
272
230
273
- 思路: 使用二分搜索,当中间元素大于右侧元素时意味着拐点即最小元素在右侧,否则在左侧
231
+ - 使用二分搜索,当中间元素大于右侧元素时意味着拐点即最小元素在右侧,否则在左侧
274
232
275
233
``` Python
276
234
class Solution :
@@ -279,7 +237,7 @@ class Solution:
279
237
l , r = 0 , len (nums) - 1
280
238
281
239
while l < r:
282
- mid = ( l + r ) // 2
240
+ mid = l + (r - l ) // 2
283
241
if nums[mid] > nums[r]: # 数组有重复时,若 nums[l] == nums[mid] == nums[r],无法判断移动方向
284
242
l = mid + 1
285
243
else :
@@ -290,9 +248,7 @@ class Solution:
290
248
291
249
### [ find-minimum-in-rotated-sorted-array-ii] ( https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/ )
292
250
293
- > 假设按照升序排序的数组在预先未知的某个点上进行了旋转
294
- > ( 例如,数组 [ 0,1,2,4,5,6,7] 可能变为 [ 4,5,6,7,0,1,2] )。
295
- > 请找出其中最小的元素。(包含重复元素)
251
+ > 假设按照升序排序的数组在预先未知的某个点上进行了旋转,例如,数组 [ 0, 1, 2, 4, 5, 6, 7] 可能变为 [ 4, 5, 6, 7, 0, 1, 2] 。请找出其中最小的元素。数组中可能包含重复元素。
296
252
297
253
``` Python
298
254
class Solution :
@@ -301,7 +257,7 @@ class Solution:
301
257
l , r = 0 , len (nums) - 1
302
258
303
259
while l < r:
304
- mid = ( l + r ) // 2
260
+ mid = l + (r - l ) // 2
305
261
if nums[mid] > nums[r]:
306
262
l = mid + 1
307
263
elif nums[mid] < nums[r] or nums[mid] != nums[l]:
@@ -314,10 +270,7 @@ class Solution:
314
270
315
271
### [ search-in-rotated-sorted-array] ( https://leetcode-cn.com/problems/search-in-rotated-sorted-array/ )
316
272
317
- > 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
318
- > ( 例如,数组 [ 0,1,2,4,5,6,7] 可能变为 [ 4,5,6,7,0,1,2] )。
319
- > 搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
320
- > 你可以假设数组中不存在重复的元素。
273
+ > 假设按照升序排序的数组在预先未知的某个点上进行了旋转,例如,数组 [ 0, 1, 2, 4, 5, 6, 7] 可能变为 [ 4, 5, 6, 7, 0, 1, 2] 。搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1。假设数组中不存在重复的元素。
321
274
322
275
``` Python
323
276
class Solution :
@@ -326,7 +279,7 @@ class Solution:
326
279
l , r = 0 , len (nums) - 1
327
280
328
281
while l <= r:
329
- mid = ( l + r ) // 2
282
+ mid = l + (r - l ) // 2
330
283
if nums[mid] == target:
331
284
return mid
332
285
elif nums[mid] > target:
@@ -342,15 +295,9 @@ class Solution:
342
295
return - 1
343
296
```
344
297
345
- 注意点
346
-
347
- > 面试时,可以直接画图进行辅助说明,空讲很容易让大家都比较蒙圈
348
-
349
298
### [ search-in-rotated-sorted-array-ii] ( https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/ )
350
299
351
- > 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
352
- > ( 例如,数组 [ 0,0,1,2,2,5,6] 可能变为 [ 2,5,6,0,0,1,2] )。
353
- > 编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。(包含重复元素)
300
+ > 假设按照升序排序的数组在预先未知的某个点上进行了旋转,例如,数组 [ 0, 0, 1, 2, 2, 5, 6] 可能变为 [ 2, 5, 6, 0, 0, 1, 2] 。编写一个函数来判断给定的目标值是否存在于数组中,若存在返回 true,否则返回 false。数组中可能包含重复元素。
354
301
355
302
``` Python
356
303
class Solution :
@@ -363,7 +310,7 @@ class Solution:
363
310
l += 1
364
311
r -= 1
365
312
continue
366
- mid = ( l + r ) // 2
313
+ mid = l + (r - l ) // 2
367
314
if nums[mid] == target:
368
315
return True
369
316
elif nums[mid] > target:
@@ -379,6 +326,28 @@ class Solution:
379
326
return False
380
327
```
381
328
329
+ ## 隐含的二分搜索
330
+
331
+ 有时用到二分搜索的题目并不会直接给你一个有序数组,它隐含在题目中,需要你去发现或者构造。一类常见的隐含的二分搜索的问题是求某个有界数据的最值,以最小值为例,当数据比最小值大时都符合条件,比最小值小时都不符合条件,那么符合/不符合条件就构成了一种有序关系,再加上数据有界,我们就可以使用二分搜索来找数据的最小值。注意,数据的界一般也不会在题目中明确提示你,需要你自己去发现。
332
+
333
+ ### [ koko-eating-bananas] ( https://leetcode-cn.com/problems/koko-eating-bananas/ )
334
+
335
+ ``` Python
336
+ class Solution :
337
+ def minEatingSpeed (self , piles : List[int ], H : int ) -> int :
338
+
339
+ l, r = 1 , max (piles)
340
+
341
+ while l < r:
342
+ mid = l + (r - l) // 2
343
+ if sum ([- pile // mid for pile in piles]) < - H:
344
+ l = mid + 1
345
+ else :
346
+ r = mid
347
+
348
+ return l
349
+ ```
350
+
382
351
## 总结
383
352
384
353
二分搜索核心四点要素(必背&理解)
0 commit comments