Skip to content

Commit 091eab9

Browse files
author
lucifer
committed
feat: 二分讲义优化
1 parent 34118c8 commit 091eab9

File tree

1 file changed

+18
-55
lines changed

1 file changed

+18
-55
lines changed

91/binary-search.md

+18-55
Original file line numberDiff line numberDiff line change
@@ -382,47 +382,19 @@ function binarySearchRight(nums, target) {
382382

383383
#### 思维框架
384384

385-
不管是寻找最左插入位置还是后面的寻找最右插入位置,我们的更新指针代码都是一样的。即:
386-
387-
```
388-
l = mid + 1
389-
# or
390-
r = mid
391-
```
392-
393-
> 当然也有别的方式(比如 mid 不是向下取整,而是向上取整), 但是这样可以最小化记忆成本。
394-
395-
有的人有疑问,这样设置结束条件会不会漏过正确的解,必然是不会的,如果更新区间代码是 r = mid - 1 还有可能错过,而 r = mid 这种代码根本就没有摒弃 r。
396-
397-
这样描述可能不够清晰。为了探究这个问题, 我们继续往下看。
398-
399-
以 nums = [1,3,4], target = 2 为例。我们要找的其实就是 [1,x,3,4] 中 x 所在的位置,换句话说**我们的目的就是不断收缩搜索区间到 x 位置**
400-
401-
- 如果 nums[mid] < x,由于 nums 是单调增的,因此 mid 一定在 x 左侧,小于等于 mid 部分一定不是最终解,我们将 mid + 1 作为新的搜索区间的左区间。
402-
- 如果 nums[mid] >= x,由于 nums 是单调增的,因此 mid 一定在 x 的位置或者在 x 的右侧,由于 mid 可能就在 x 的位置,所以 r = mid - 1 就可能错过了这种解,使用 r = mid 这种更新方式可避免这种情况。
403-
404-
说得再直白一点。假设使用最左插入位置的方式查找的索引 为 i,那么应该满足不等式 nums[i] >= target。
405-
406-
- nums = [1,3,4], target = 2 的情况 i = 1,nums[i] = 3 > 2
407-
- nums = [1,2,2,2,3,4], target = 2 的情况 i = 1,nums[i] = 2 = 2
408-
409-
因此
410-
411-
- 如果 nums[mid] 已经小于 target 了,就必然是不可能的情况,直接通过 l = mid + 1 排除
412-
- 否则无法排除, 使用 r = mid 即可, mid 就是我们找到的一个备胎。
385+
如果你将**寻找最左插入位置**看成是**寻找最左满足**大于等于 x 的值,那就可以和前面的知识产生联系,使得代码更加统一。唯一的区别点在于**前面是最左满足等于 x**,这里是**最左满足大于等于 x**
413386

414387
具体算法:
415388

416389
- 首先定义搜索区间为 [left, right],注意是左右都闭合,之后会用到这个点。
417390

418391
> 你可以定义别的搜索区间形式,不过后面的代码也相应要调整,感兴趣的可以试试别的搜索区间。
419392
420-
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 但由于上面提到了更新条件有一个`r = mid`,因此如果结束条件是 left <= right 则会死循环。因此结束条件是 left < right。
393+
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 也就是说我们的终止搜索条件为 left <= right。
421394

422-
- 循环体内,我们不断计算 mid ,并将 nums[mid] 与 目标值比对。
423-
- 如果 nums[mid] 大于等于目标值(mid 在 x 位置或者 x 的右侧), r 也可能是目标解(等于的情况),r - 1 可能会错过解,因此我们使用 r = mid。
424-
- 如果 nums[mid] 小于目标值(mid 在 x 的左侧), mid 以及 mid 左侧都不可能是解,至少是 mid + 1 才行,因此我们使用 l = mid + 1。
425-
- 最后直接返回 l 或者 r 即可。(并且不需要像`最左满足条件的值`那样判断了)
395+
- 当 A[mid] >= x,说明找到一个备胎,我们令 r = mid - 1 将 mid 从搜索区间排除,继续看看有没有更好的备胎。
396+
- 当 A[mid] < x,说明 mid 根本就不是答案,直接更新 l = mid + 1,从而将 mid 从搜索区间排除。
397+
- 最后搜索区间的 l 就是最好的备胎,备胎转正。
426398

427399
#### 代码模板
428400

@@ -433,14 +405,11 @@ def bisect_left(nums, x):
433405
# 内置 api
434406
bisect.bisect_left(nums, x)
435407
# 手写
436-
l, r = 0, len(nums) - 1
437-
while l < r:
408+
l, r = 0, len(A) - 1
409+
while l <= r:
438410
mid = (l + r) // 2
439-
if nums[mid] < x:
440-
l = mid + 1
441-
else:
442-
r = mid
443-
# 由于 l 和 r 相等,因此返回谁都无所谓。
411+
if A[mid] >= x: r = mid - 1
412+
else: l = mid + 1
444413
return l
445414
```
446415

@@ -450,22 +419,19 @@ def bisect_left(nums, x):
450419

451420
#### 思维框架
452421

453-
`寻找最左插入位置`类似。不同的地方在于:如果有多个满足条件的值,我们返回最右侧的。 比如一个数组 nums: [1,2,2,2,3,4],target 是 2,我们应该插入的位置是 4
422+
如果你将**寻找最右插入位置**看成是**寻找最右满足**大于 x 的值,那就可以和前面的知识产生联系,使得代码更加统一。唯一的区别点在于**前面是最左满足等于 x**,这里是**最左满足大于 x**
454423

455424
具体算法:
456425

457426
- 首先定义搜索区间为 [left, right],注意是左右都闭合,之后会用到这个点。
458427

459428
> 你可以定义别的搜索区间形式,不过后面的代码也相应要调整,感兴趣的可以试试别的搜索区间。
460429
461-
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 但由于上面提到了更新条件有一个`r = mid`,因此如果结束条件是 left <= right 则会死循环。因此结束条件是 left < right。
462-
463-
> 有的人有疑问,这样设置结束条件会不会漏过正确的解,其实不会。举个例子容易明白一点。 比如对于区间 [4,4],其包含了一个元素 4,搜索区间不为空。如果我们的答案恰好是 4,会被错过么?不会,因为我们直接返回了 4。
430+
- 由于我们定义的搜索区间为 [left, right],因此当 left <= right 的时候,搜索区间都不为空。 也就是说我们的终止搜索条件为 left <= right。
464431

465-
- 循环体内,我们不断计算 mid ,并将 nums[mid] 与 目标值比对。
466-
- 如果 nums[mid] 小于等于目标值, mid 以及 mid 左侧都不可能是解,因此我们使用 l = mid + 1。
467-
- 如果 nums[mid] 大于目标值, r 也可能是目标解,r - 1 可能会错过解,因此我们使用 r = mid。
468-
- 最后直接返回 l 或者 r 即可。(并且不需要像`最左满足条件的值`那样判断了)
432+
- 当 A[mid] > x,说明找到一个备胎,我们令 r = mid - 1 将 mid 从搜索区间排除,继续看看有没有更好的备胎。
433+
- 当 A[mid] <= x,说明 mid 根本就不是答案,直接更新 l = mid + 1,从而将 mid 从搜索区间排除。
434+
- 最后搜索区间的 l 就是最好的备胎,备胎转正。
469435

470436
#### 代码模板
471437

@@ -477,14 +443,11 @@ def bisect_right(nums, x):
477443
# 内置 api
478444
bisect.bisect_right(nums, x)
479445
# 手写
480-
l, r = 0, len(nums) - 1
481-
while l < r:
446+
l, r = 0, len(A) - 1
447+
while l <= r:
482448
mid = (l + r) // 2
483-
if nums[mid] > x:
484-
r = mid
485-
else:
486-
l = mid + 1
487-
# 由于 l 和 r 相等,因此返回谁都无所谓。
449+
if A[mid] <= x: l = mid + 1
450+
else: r = mid - 1
488451
return l
489452
```
490453

0 commit comments

Comments
 (0)