Skip to content

Commit

Permalink
滑动窗口update
Browse files Browse the repository at this point in the history
  • Loading branch information
yk committed Feb 22, 2024
1 parent a4bdc4e commit 5b3458d
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 101 deletions.
202 changes: 102 additions & 100 deletions content/posts/algorithm/trick/二分法.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: '二分法'
title: '二分搜索'
date: 2023-01-03T11:24:54+08:00
---

Expand All @@ -11,22 +11,22 @@ date: 2023-01-03T11:24:54+08:00

### 循环不变式

- `while low <= high`
- `while low <= high`

1. 终止条件,一定是 `low + 1 == high`,也即 low > right
2. 意味着,整个区间 `[low..high]` 里的每个元素都被遍历过了
1. 终止条件,一定是 `low + 1 == high`,也即 low > right
2. 意味着,整个区间 `[low..high]` 里的每个元素都被遍历过了

- `while low < high`
- `while low < high`

1. 终止条件,一定是 `low == high`
2. 意味着,整个区间 `[low..high]` 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁
1. 终止条件,一定是 `low == high`
2. 意味着,整个区间 `[low..high]` 里当 low == high 时的元素可能会没有被遍历到,需要打个补丁

### 可行解区间

对于二分搜索过程中的每一次循环,它的可行解区间都应当一致,结合对于循环不变式的理解:

- `[low...high]`,左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 `low = mid + 1` 还是 `high = mid - 1`
- `[low...high)`,左闭右开区间,维持可行解区间,下一个搜索区间左边是 `low = mid + 1`,右边是 `high = mid`
- `[low...high]`,左右都闭区间,一般根据 mid 就能判断下一个搜索区间是 `low = mid + 1` 还是 `high = mid - 1`
- `[low...high)`,左闭右开区间,维持可行解区间,下一个搜索区间左边是 `low = mid + 1`,右边是 `high = mid`

因为数组的特性,常用的就是这两种区间。当然也有左右都开的区间 `(low...high)`,对应的循环不变式为 `while low + 1 < high`,不过比较少见。

Expand All @@ -50,27 +50,27 @@ date: 2023-01-03T11:24:54+08:00
var findMin = function (nums) {
const n = nums.length
let left = 0
let right = n - 1 // 区间 [left, right] 如果用右开区间则不方便判断右值
let right = n - 1 // 区间 [left, right] 如果用右开区间则不方便判断右值
// 循环不变量对应区间为 [left, right)
while(left < right) {
while (left < right) {
const mid = left + ((right - left) >> 1) // 向下取整
if(nums[mid] > nums[right]) {
if (nums[mid] > nums[right]) {
left = mid + 1 // left 可能和mid相等
}else {
} else {
right = mid // right
}
}
return nums[left]
}
```

- 无重复、升序(单调递增)和 O(log n) 都在提示我们可以使用二分法来解决
- 思考旋转,导致的结果
1. 左<中,中<右(min 在左,收缩右)
2. 左<中,中>右(min 在右,收缩左)
3. 左>中,中<右(min 在左,收缩右),所以只比较中与右的关系即可
- 求的 mid,因为向下取整后偏向 left 一侧,所以 `left <= mid``mid < right`,为了保证循环不变量的恒等,left 将收缩为 mid + 1,right 将收缩为 mid
- 如果用 mid 和左比,那么 mid 需要先加一再向下取整,这样 mid 就更靠近右边的 right 了,然后先找出最大值,再向右偏一位即可
- 无重复、升序(单调递增)和 O(log n) 都在提示我们可以使用二分法来解决
- 思考旋转,导致的结果
1. 左<中,中<右(min 在左,收缩右)
2. 左<中,中>右(min 在右,收缩左)
3. 左>中,中<右(min 在左,收缩右),所以只比较中与右的关系即可
- 求的 mid,因为向下取整后偏向 left 一侧,所以 `left <= mid``mid < right`,为了保证循环不变量的恒等,left 将收缩为 mid + 1,right 将收缩为 mid
- 如果用 mid 和左比,那么 mid 需要先加一再向下取整,这样 mid 就更靠近右边的 right 了,然后先找出最大值,再向右偏一位即可

第二种做法:`while(left <= right)`

Expand All @@ -84,11 +84,11 @@ var findMin = function (nums) {
let left = 0
let right = n - 1
// 循环不变式对应区间为 [left, right]
while(left <= right) {
while (left <= right) {
const mid = left + ((right - left) >> 1) // 向下取整
if(nums[mid] >= nums[right]) {
if (nums[mid] >= nums[right]) {
left = mid + 1
}else {
} else {
right = mid
}
}
Expand All @@ -107,26 +107,26 @@ var findMin = function (nums) {
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
var findMin = function (nums) {
let left = 0
let right = nums.length - 1
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] > nums[right]) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] > nums[right]) {
left = mid + 1
}else if(nums[mid] < nums[right]){
} else if (nums[mid] < nums[right]) {
right = mid
}else if (nums[mid] === nums[right]) {
} else if (nums[mid] === nums[right]) {
// 注意 这里的收缩右边界 与 寻找一个数(这个数就是重复的数字) 的收缩右边界情况不一样
right--
}
}
return nums[left]
};
}
```

- 一堆数字中寻找目标数字(这个数字是重复的)的边界,是将 left 或 right 与 mid 相关,逼近关系是 `left = mid + 1``right = mid`
- 这道题是寻找最小值,最小值不一定是重复的,但是可能有重复的数字,所以逼近关系是 `right--`
- 一堆数字中寻找目标数字(这个数字是重复的)的边界,是将 left 或 right 与 mid 相关,逼近关系是 `left = mid + 1``right = mid`
- 这道题是寻找最小值,最小值不一定是重复的,但是可能有重复的数字,所以逼近关系是 `right--`

### [33. 搜索渲染排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)

Expand All @@ -136,31 +136,32 @@ var findMin = function(nums) {
* @param {number} target
* @return {number}
*/
// 毫无疑问 二分法, 就是需要注意二分过程中的细节 搜索区间和循环不变量
var search = function(nums, target) {
// 毫无疑问 二分法, 就是需要注意二分过程中的细节 搜索区间和循环不变量
var search = function (nums, target) {
const n = nums.length
let l = 0, r = n - 1
let l = 0,
r = n - 1
// 搜索区间是 [l..r]
while(l <= r) {
const mid = l + (r - l >> 1)
if(nums[mid] === target) return mid
while (l <= r) {
const mid = l + ((r - l) >> 1)
if (nums[mid] === target) return mid
// 因为mid偏向左半边 所以 确定左半边比较容易
if (nums[0] <= nums[mid] ) {
if(nums[l] <= target && target < nums[mid]) {
if (nums[0] <= nums[mid]) {
if (nums[l] <= target && target < nums[mid]) {
r = mid - 1
}else {
} else {
l = mid + 1
}
}else {
if(nums[mid] < target && target <= nums[r]) {
} else {
if (nums[mid] < target && target <= nums[r]) {
l = mid + 1
}else {
} else {
r = mid - 1
}
}
}
return -1
};
}
```

> 这道题还真的只有用 小于等于才好做, 想一想为什么呢?
Expand All @@ -175,49 +176,49 @@ var search = function(nums, target) {
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
var search = function (nums, target) {
let left = 0
// 其实这里初始化成 nums.length 也是能OK的,当收缩边界的时候就需要注意了, 见下一题
let right = nums.length - 1
// 循环不变量对应区间 [left, right)
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
return mid
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
right = mid -1
} else {
right = mid - 1
}
}
return nums[left] === target ? left : -1
};
}
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
var search = function (nums, target) {
let left = 0
let right = nums.length - 1
while(left <= right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left <= right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
return mid
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
right = mid -1
} else {
right = mid - 1
}
}
return -1
};
}
```

如上,第一种和第二种仅仅是 循环不变式 发生了变化:

- 第一种:循环不变量对应区间为 `[left, right)`,结束条件是 `left === right`,如[2,2)就停止了,这意味着区间内可能会有元素没有遍历过,所以需要加一层兜底,比如数组只有一个数字的情况
- 第二种:循环不变量对应区间为 `[left, right]`,结束条件是 `left === right + 1`,如[3,2]才会停止,这意味着,区间内所有元素都被搜索过了
- 第一种:循环不变量对应区间为 `[left, right)`,结束条件是 `left === right`,如[2,2)就停止了,这意味着区间内可能会有元素没有遍历过,所以需要加一层兜底,比如数组只有一个数字的情况
- 第二种:循环不变量对应区间为 `[left, right]`,结束条件是 `left === right + 1`,如[3,2]才会停止,这意味着,区间内所有元素都被搜索过了

### [剑指 Offer 53 - I. 在排序数组中查找数字 I](https://leetcode.cn/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/description/)

Expand All @@ -229,39 +230,40 @@ var search = function(nums, target) {
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
var search = function (nums, target) {
// 寻找出左右边界,然后相减即是区间了
let left = 0
let right = nums.length - 1 // 实际上这里是 nums.length 也ok, 因为是求左边界 [left, right) 对右边界无所谓
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
right = mid // 寻找左边界, 按照[left, right), 所以 right = mid
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
} else {
right = mid
}
}
const l = left // 此处收缩右边界 right = mid 不会发生越界
left = 0
right = nums.length // 注意这里~~~ 因为是收缩左边界去找右边界了,由于循环不变量为[left,right), 所以初始区间右边界应该与之保持一致为 nums.length
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
left = mid + 1 // 寻找右边界,收缩左边界,按照[left, right),left = mid + 1
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
} else {
right = mid
}
}
const r = left - 1 < 0 ? 0 : left - 1 // mid是在搜索区间内的,而+1后的left不一定,结束条件是 left === right,由于是向右逼近,所以left应该-1
if(l <= r && nums[l] === target && nums[r] === target) { // 防止压根没有
if (l <= r && nums[l] === target && nums[r] === target) {
// 防止压根没有
return r - l + 1
}
return 0
};
}
```

怎么样,细节是不是拉满~~~
Expand All @@ -277,38 +279,38 @@ var search = function(nums, target) {
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
var searchRange = function (nums, target) {
let left = 0
let right = nums.length - 1
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
right = mid
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
} else {
right = mid
}
}
const l = left
left = 0
right = nums.length
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] === target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
left = mid + 1
}else if(nums[mid] < target) {
} else if (nums[mid] < target) {
left = mid + 1
}else {
} else {
right = mid
}
}
const r = left - 1 < 0 ? 0 : left - 1
if(l <= r && nums[l] === target && nums[r] === target) {
if (l <= r && nums[l] === target && nums[r] === target) {
return [l, r]
}
return [-1, -1]
};
}
```

### [35. 搜索插入位置](https://leetcode.cn/problems/search-insert-position/)
Expand All @@ -321,22 +323,22 @@ var searchRange = function(nums, target) {
* @param {number} target
* @return {number}
*/
var searchInsert = function(nums, target) {
var searchInsert = function (nums, target) {
let left = 0
let right = nums.length // 这道题因为拆入位置可能为左边界或右边界,所以必须初始化为 nums.length 才行
// 如下,如果对右边界做了提前兜底,那么初始化right为nums.length-1也是没问题的, 这下更理解了吧?
// let right = nums.length - 1
// if(target > nums[nums.length - 1]) return nums.length
while(left < right) {
const mid = left + (right - left >> 1)
if(nums[mid] >= target) {
while (left < right) {
const mid = left + ((right - left) >> 1)
if (nums[mid] >= target) {
right = mid
}else if(nums[mid] < target){
} else if (nums[mid] < target) {
left = mid + 1
}
}
return right
};
}
```

## 衍生变形题
Expand All @@ -353,15 +355,15 @@ TODO

第一点,其实和二分没多大关系,就是在数组问题上,一定要对索引敏感,比如:

- 区间 `[a, b]`,那么 a~b (包含 a 和 b) 的长度为 `a - b + 1`
- 区间 `(a, b)`,那么 a~b (不包含 a 和 b) 的长度为 `a - b - 1`
- 区间 `[a, b) || (a, b]`,那么 a~b (不包含其中一个)的长度为 `a - b`
- 区间 `[a, b]`,那么 a~b (包含 a 和 b) 的长度为 `a - b + 1`
- 区间 `(a, b)`,那么 a~b (不包含 a 和 b) 的长度为 `a - b - 1`
- 区间 `[a, b) || (a, b]`,那么 a~b (不包含其中一个)的长度为 `a - b`

同样的,求索引也可以利用上述方法。

第二点,`while([condition])` 这里的 `condition``小于还是小于等于` 一定要搞清楚,有时候要从实际问题出发。

- 如果是 `<`,结束条件是 left === right,没啥好争议的,结果取 left 或 right 都一样
- 如果是 `<=`,结束条件是 left > right,注意 📢:这里就会有问题了,是 left 先越过 right 还是 right 先越过 left,这个是要搞清楚的,题目不同,也就不一样,但是会影响我们的结果,有时候会产生越界问题
- 如果是 `<`,结束条件是 left === right,没啥好争议的,结果取 left 或 right 都一样
- 如果是 `<=`,结束条件是 left > right,注意 📢:这里就会有问题了,是 left 先越过 right 还是 right 先越过 left,这个是要搞清楚的,题目不同,也就不一样,但是会影响我们的结果,有时候会产生越界问题

第三点,在二分搜索中,while 循环时的循环条件称为**循环不变式**,它对应的区间称之为**循环不变量****_就是在 while 寻找中每一次边界的处理都要坚持根据区间的定义来操作_**,这直接决定了我们的逼近策略,very important!
Loading

0 comments on commit 5b3458d

Please sign in to comment.