二分查找
题目一:在排序数组中查找元素的第⼀个和最后⼀个位置
1. 题⽬链接:
https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/
2. 题⽬描述:
给你⼀个按照⾮递减顺序排列的整数数组 nums,和⼀个⽬标值 target。请你找出给定⽬标值在数组
中的开始位置和结束位置。
如果数组中不存在⽬标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 ⽰例 1:
输⼊:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
⽰例 2:
输⼊:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
⽰例 3:
输⼊:nums = [], target = 0
输出:[-1,-1]
提⽰:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是⼀个⾮递减数组
-109 <= target <= 109
3. 算法思路:
⽤的是⼆分思想,就是根据数据的性质,在某种判断条件下将区间⼀分为⼆,然后舍去其中⼀个
区间,然后再另⼀个区间内查找;
⽅便叙述,⽤ x 表⽰该元素, resLeft 表⽰左边界, resRight 表⽰右边界。
寻找左边界思路:
寻找左边界:
我们注意到以左边界划分的两个区间的特点:
▪左边区间
[left, resLeft - 1]
都是⼩于 x 的;▪ 右边区间(包括左边界)
[resLeft, right]
都是⼤于等于 x 的;• 因此,关于 mid 的落点,我们可以分为下⾯两种情况:
当我们的 mid 落在
[left, resLeft - 1]
区间的时候,也就是arr[mid] < target
。说明
[left, mid]
都是可以舍去的,此时更新 left 到 mid + 1 的位置, 继续在[mid + 1, right]
上寻找左边界;◦ 当 mid 落在
[resLeft, right]
的区间的时候,也就是arr[mid] >= target
。 说明
[mid + 1, right]
(因为 mid 可能是最终结果,不能舍去)是可以舍去的,此时 更新 right 到 mid
的位置,继续在 [left, mid] 上寻找左边界; • 由此,就可以通过⼆分,来快速寻找左边界; 注意:这⾥找中间元素需要向下取整。
因为后续移动左右指针的时候:• 左指针:
left = mid + 1
,是会向后移动的,因此区间是会缩⼩的;• 右指针: right = mid ,可能会原地踏步(⽐如:如果向上取整的话,如果剩下 1,2 两个元 素,
left == 1 , right == 2 , mid == 2
。更新区间之后, left,right,mid 的值没有改变,就会陷⼊死循环)。
因此⼀定要注意,当 right = mid 的时候,要向下取整。 寻找右边界思路:• 寻右左边界:
◦ ⽤
resRight
表⽰右边界;◦ 我们注意到右边界的特点:
▪ 左边区间 (包括右边界)
[left, resRight]
都是⼩于等于 x 的;▪ 右边区间
[resRight+ 1, right]
都是⼤于 x 的;• 因此,关于 mid 的落点,我们可以分为下⾯两种情况:
◦ 当我们的 mid 落在
[left, resRight]
区间的时候,说明[left, mid - 1]
( mid 不可以舍去,因为有可能是最终结果) 都是可以舍去的,此时更新 left 到 mid
的位置; ◦ 当
mid
落在[resRight+ 1, right]
的区间的时候,说明 [mid, right] 内的元素
是可以舍去的,此时更新right
到mid - 1
的位置;• 由此,就可以通过⼆分,来快速寻找右边界; 注意:这⾥找中间元素需要向上取整。 因为后续移动左右指针的时候:
• 左指针:
left = mid
,可能会原地踏步(⽐如:如果向下取整的话,如果剩下 1,2 两个元 素,left == 1, right == 2,mid == 1
。更新区间之后, left,right,mid 的值 没有改变,就会陷⼊死循环)。• 右指针: right = mid - 1 ,是会向前移动的,因此区间是会缩⼩的; 因此⼀定要注意,当 right = mid
的时候,要向下取整。 ⼆分查找算法总结:请⼤家⼀定不要觉得背下模板就能解决所有⼆分问题。⼆分问题最重要的就是要分析题意,然后确定
要搜索的区间,根据分析问题来写出⼆分查找算法的代码。模板记忆技巧:
- 关于什么时候⽤三段式,还是⼆段式中的某⼀个,⼀定不要强⾏去⽤,⽽是通过具体的问题分析情 况,根据查找区间的变化确定指针的转移过程,从⽽选择⼀个模板。
- 当选择两段式的模板时:
在求 mid 的时候,只有 right - 1 的情况下,才会向上取整(也就是 +1 取中间数)
4.代码
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] ret = new int[2];
ret[0]=-1;
ret[1]=-1;
if(nums.length==0) return ret;
int n = nums.length;
int left = 0;
int right = n-1;
//左端点
while(left < right){
int mid = left+(right-left)/2;
if(nums[mid] < target){
left = mid+1;
}else if(nums[mid] >= target){
right = mid;
}
}
if(nums[left] == target)
ret[0] = left;
//右端点
right = n-1;
while(left < right){
int mid=left+(right-left+1)/2;
if(nums[mid] <= target){
left = mid;
}else if(nums[mid] > target){
right=mid-1;
}
}
if(nums[left]==target)
ret[1] = left;
return ret;
}
}
题目二:搜索插⼊位置
1. 题⽬链接:
https://leetcode.cn/problems/search-insert-position/description/
2. 题⽬描述:
给定⼀个排序数组和⼀个⽬标值,在数组中找到⽬标值,并返回其索引。如果⽬标值不存在于数组
中,返回它将会被按顺序插⼊的位置。
请必须使⽤时间复杂度为 O(log n) 的算法。
⽰例 1:
输⼊: nums = [1,3,5,6], target = 5
输出: 2
⽰例 2:
输⼊: nums = [1,3,5,6], target = 2
输出: 1
⽰例 3:
输⼊: nums = [1,3,5,6], target = 7
输出: 4
3. 解法(⼆分查找算法):
算法思路:
a. 分析插⼊位置左右两侧区间上元素的特点:设插⼊位置的坐标为 index ,根据插⼊位置的特点可以知道:
•
[left, index - 1]
内的所有元素均是⼩于target
的;•
[index, right]
内的所有元素均是⼤于等于target
的。b. 设
left
为本轮查询的左边界,right
为本轮查询的右边界。根据mid
位置元素的信 息,分析下⼀轮查询的区间:▪ 当
nums[mid] >= target
时,说明 mid 落在了[index, right]
区间上,mid
左边包括
mid 本⾝,可能是最终结果,所以我们接下来查找的区间在[left, mid]
上。因此,更新 right 到 mid
位置,继续查找。▪ 当
nums[mid] < target
时,说明 mid 落在了[left, index -1]
区间上,mid 右边但不包括 mid 本⾝,可能是最终结果,所以我们接下来查找的区间在
[mid + 1, right]
上。因此,更新
left 到 mid + 1 的位置,继续查找。c. 直到我们的查找区间的⻓度变为 1 ,也就是
left == right
的时候, left 或者 right
所在的位置就是我们要找的结果。
代码
class Solution
{
public int searchInsert(int[] nums, int target)
{
int left = 0, right = nums.length - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
// 特判⼀下第三种情况
if(nums[right] < target) return right + 1;
return right;
}
}
题目三: x 的平⽅根
1. 题⽬链接:
https://leetcode.cn/problems/sqrtx/description/
2. 题⽬描述:
给你⼀个⾮负整数 x ,计算并返回 x 的 算术平⽅根 。
由于返回类型是整数,结果只保留 整数部分 ,⼩数部分将被 舍去 。
注意:不允许使⽤任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
⽰例 1:
输⼊: x = 4
输出: 2
⽰例 2: 输⼊: x = 8
输出: 2
解释:
8 的算术平⽅根是 2.82842… , 由于返回类型是整数,⼩数部分将被舍去。
3. 解法⼀(暴⼒查找):
算法思路:
依次枚举 [0, x] 之间的所有数 i : (这⾥没有必要研究是否枚举到 x / 2 还是 x / 2 + 1
。因为我们找到结果之后直接就返回 了,往后的情况就不会再判断。反⽽研究枚举区间,既耽误时间,⼜可能出错)▪ 如果 i * i == x ,直接返回 x ;
▪ 如果 i * i > x ,说明之前的⼀个数是结果,返回 i - 1 。 由于 i * i 可能超过 int 的最⼤值,因此使⽤
long long 类型。
算法代码:
class Solution {
public:
int mySqrt(int x) {
// 由于两个较⼤的数相乘可能会超过 int 最⼤范围
// 因此⽤ long long
long long i = 0;
for (i = 0; i <= x; i++)
{
// 如果两个数相乘正好等于 x,直接返回 i
if (i * i == x) return i;
// 如果第⼀次出现两个数相乘⼤于 x,说明结果是前⼀个数
if (i * i > x) return i - 1;
}
// 为了处理oj题需要控制所有路径都有返回值
return -1;
}
};
4. 解法⼆(⼆分查找算法):
算法思路:
设 x 的平⽅根的最终结果为 index :
a. 分析 index 左右两次数据的特点:
▪ [0, index] 之间的元素,平⽅之后都是⼩于等于 x 的;
▪ [index + 1, x] 之间的元素,平⽅之后都是⼤于 x 的。 因此可以使⽤⼆分查找算法。
算法代码
class Solution {
public int mySqrt(int x) {
if(x <1)return 0;
long left = 1;
long right = x;
while(left<right){
long mid = left+(right-left+1)/2;
if(mid*mid<=x){
left=mid;
}else if(mid*mid>x){
right=mid-1;
}
}
return (int)left;
}
}