"每天都在搜索的路上"
之前总结过二分法的套路(重识二分法(上)、重识二分法(中)、重识二分法(下)),那就从二分搜索开始刷起吧!
据说组内2年+前端经验小老弟拿了字节37K+(2-1)的offer,比我这四年的老油条还多(酸酸酸...),励志从今天开启刷题模式。
给定一个排序数组 nums (无重复元素的升序数组) 和一个目标值 target,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
示例1
输入: nums=[1,3,5,6], target=5
输出: 2
解释: target 在数组 nums 中存在,即返回其下标。
示例2
输入: nums=[1,3,5,6], target=2
输出: 1
解释: target 在数组 nums 中不存在,即返回其应该存在的位置下标
分析
本题属于二分搜索入门,重点考察二分法细节处理,即,当 target 元素不存在与 nums 数组中,如果确定返回的 left、right、left+1、right+1指针。
由于数组元素是严格递增且无重复的,自然要想到二分法。因此我们套用二分法的三个区间模型对应的计算模版,即左闭右闭 [left,right]、左闭右开[left,right)、左开右闭(left,right]。
target 元素存在两种情况:1、 ,2、 target 元素不在 nums 数中。
-
如果 target 元素在 nums 数组中,一定存在 nums[mid]==target 情况,所以最终结果一定会在二分的过程中通过 mid 指针返回
-
如果 target 元素不在 nums 数组中,一定在通过 left 或 right 指针(亦或需要额外计算)返回。故我们着重讨论一下这种情况。
如果 target 元素不在 nums 数组中,那么即存在三种情况,即,
- target 在数组左边界外,即 target<nums[0]
- target 在数组右边界外,即 target>nums[len−1]
- target 在数组中某个位置,即 nums[0]<target<nums[len−1]
其中,len=nums.length−1
接下来,我们就开始讨论这种情况( target 不在数组内),确定在不同区间模型下的最终返回值。
左闭右闭
区间模型为 [left,right],mid 计算方式选择 (left+right)/2、(left+right+1)/2、(left+right−1)/2,循环条件为 left≤right。(参考 重识二分法(上))
1️⃣ target 在数组左外边界
【最终结果: 0】 这种情况下 left 指针会一直指向 0,而 right 指针会不断向 left 靠拢,并在最后一次循环前指向 left=right=0。此时计算出 mid=0 ❓,nums[mid] 依旧大于 target,故 right=mid−1=−1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=0,right=−1(故应返回 left 或返回 right+1)。
细心的同学会发现:当 left=right=0 时,mid=(left+right)/2=0 是没有问题的,但是为什么经(left+right+1)/2、(left+right−1)/2 计算得到的值也是 0 呢?
在实际编程中,会采用差值方式计算 mid 指针,即 mid=left+((right−left+1)>>1)=0,mid=left+((right−left−1)>>1)=0,而且位运算中有一个向 0 取整的概念,即当实数大于0时,向下取整,实数小于0时,向上取整。JS语法中可通过">> 1(右移一位)",亦或"~~"符号,处理(0.5)以及(-0.5),最后计算出来的值均为0。(详见参考代码)
2️⃣ target 在数组右外边界
【最终结果:nums.length 】。讨论:这种情况下 right 指针会一直指向 nums.length−1,而 left 指针会不断向 right 靠拢,并在最后一次循环前指向,left=right=nums.length−1。此时,计算出的 mid=nums.length−1❓,nums[mid] 依旧小于 target,故 left=mid+1=len+1=nums.length 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=nums.length,right=nums.length−1(应返回 left 或返回 right+1)
细心的同学又会发现,如果 mid=(left+right−1)/2 计算方式时,mid=nums.length−2 (向下取整), nums[mid] 依旧小于 target,故 left=mid+1=nums.length−1,貌似进入了死循环 🤔...这里依然使用差值计算方式,即 mid=left+((right−left−1)>>1)
3️⃣ target 在数组中间
讨论: 假设存在索引 k,其中 k∈[0,n−1),n 是数组的长度,即 n=nums.length,使得 nums[k]<target<nums[k+1]。先说结论:target 应插入到 k+1 这个位置,故返回 k+1。
在最后一次循环前,left 和 right 指向有两种情况,指向 k 或 k+1。我们对这两种情况分开讨论。
-
当 left 和 right 同时指向 k 时,进入最后一次循环,此时 mid=k,nums[mid] 小于 target,因此 left=mid+1=k+1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=k+1,right=k(应返回 left 或返回 right+1)。
-
当 left 和 right 同时指向 k+1 时,进入最后一次循环,此时 mid=k+1,nums[mid] 大于 target,因此 right=mid−1=k 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=k+1,right=k(应返回 left 或返回 right+1)。
综上所述,在左闭右闭区间模型下,mid 计算方式为 mid=(left+right)/2、mid=(left+right−1)/2、mid=(left+right+1)/2,且当元素不在 nums 数组中时,当退出循环时直接返回 left,亦或返回 right+1。
4️⃣ 参考代码
function searchInsert(nums: number[], target: number): number {
let left = 0, right = nums.length - 1;
while(left <= right) {
const mid = left + ~~((right - left) / 2);
if (nums[mid] === target) {
return mid;
}
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
};
左闭右开
区间模型为 [left,right),mid 计算方式选择 (left+right)/2、(left+right−1)/2,循环条件为 left<right。参考 重识二分法(中)
1️⃣ target 在数组左外边界
【最终结果: 0】 讨论:这种情况下 left 指针会一直指向 0,而 right 指针会不断向 left 靠拢,并在最后一次循环前指向 left=0,right=1。此时计算出 mid=0,nums[mid] 依旧大于 target,故 right=mid=0 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=0(应返回 left 或返回 right)。
2️⃣ target 在数组右外边界
【最终结果:nums.length 】。讨论:这种情况下 right 指针会一直指向 num.length,而 left 指针会不断向 right 靠拢,并在最后一次循环前指向,left=nums.length−1,right=nums.length。此时,计算出的 mid=nums.length−1,nums[mid] 依旧小于 target,故 left=mid+1=nums.length 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=nums.length(应返回 left 或返回 right)
3️⃣ target 在数组中间
讨论: 假设存在索引 k,其中 k∈[0,n−1),n 是数组的长度,即 n=nums.length,使得 nums[k]<target<nums[k+1]。先说结论:target 应插入到 k+1 这个位置,故返回 k+1。
在最后一次循环前,left 指向 k,right 指向 k+1。进入最后一次循环,此时 mid=k,nums[mid] 小于 target,因此 left=mid+1=k+1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=k+1(应返回 left 或返回 right)。
综上所述,在左闭右闭区间模型下,mid 计算方式为 mid=(left+right)/2 或 mid=(left+right−1)/2,且当元素不在 nums 数组中时,当退出循环时直接返回 left,亦或返回 right。
4️⃣ 参考代码
function searchInsert(nums: number[], target: number): number {
let left = 0, right = nums.length;
while(left < right) {
const mid = left + ~~((right - left) / 2);
if (nums[mid] === target) {
return mid;
}
if (nums[mid] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
左开右闭(left,right]
区间模型为 [left,right),mid 计算方式选择 (left+right+1)/2,循环条件选择 left<right。参考 重识二分法(下)
1️⃣ target 在数组左外边界
【最终结果: 0】 讨论:这种情况下 left 指针会一直指向 −1,而 right 指针会不断向 left 靠拢,并在最后一次循环前指向 left=−1,right=0。此时计算出 mid=0,nums[mid] 依旧大于 target,故 right=mid−1=−1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=0,right=−1(应返回 left+1 或返回 right+1)。
2️⃣ target 在数组右外边界
【最终结果:nums.length 】。讨论:这种情况下 right 指针会一直指向 nums.length−1,而 left 指针会不断向 right 靠拢,并在最后一次循环前指向,left=nums.length−2,right=nums.length−1。此时,计算出的 mid=nums.length−1,nums[mid] 依旧小于 target,故 left=mid=nums.length−1 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=nums.length−1(应返回 left+1 或返回 right+1)
3️⃣ target 在数组中间
讨论: 假设存在索引 k,其中 k∈[0,n−1),n 是数组的长度,即 n=nums.length,使得 nums[k]<target<nums[k+1]。先说结论:target 应插入到 k+1 这个位置,故返回 k+1。
在最后一次循环前,left 和 right 指向有两种情况,指向 k 或 k+1。我们对这两种情况分开讨论。
在最后一次循环前,left 指向 k,right 指向 k+1。进入最后一次循环,此时 mid=k+1,nums[mid] 大于 target,因此 right=mid−1=k 进而退出循环。故这种情况,二分搜索退出循环时的指针为 left=right=k(应返回 left+1 或返回 right+1)。
综上所述,在左闭右闭区间模型下,mid 计算方式为 mid=(left+right+1)/2,且当元素不在 nums 数组中时,当退出循环时直接返回 left+1,亦或返回 right+1。
4️⃣ 参考代码
function searchInsert(nums: number[], target: number): number {
let left = -1, right = nums.length - 1;
while(left < right) {
const mid = left + ~~((right - left + 1) / 2);
if (nums[mid] === target) {
return mid;
}
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid;
}
}
return left + 1;
};
总结
| 区间模型 | 循环条件 | mid计算方式 | 二分指针赋值 | 返回值 |
|---|
| [left,right] | left≤right | mid=(lef+right)/2、mid=(lef+right−1)/2、mid=(lef+right+1)/2 | 当 nums[mid]>target 时 right=mid−1,否则 left=mid+1 | left 亦或 right+1 |
| [left,right) | left<right | mid=(lef+right)/2、mid=(lef+right−1)/2 | 当 nums[mid]>target 时 right=mid,否则 left=mid+1 | left 亦或 right |
| (left,right] | left<right | mid=(lef+right+1)/2 | 当 nums[mid]>target 时 right=mid−1,否则 left=mid | left+1 亦或 right+1 |