前言
渐进刷题记录第一天,原本以为零零散散刷了很多题,面对一个基础的二分算法,难道还有什么难度吗?没想到现实给我当头一棒。1. 关于何时 left <= right , 何时left < right, 我尽然找不到一个合理的解释,2. 当数组出现重复的元素时,又怎么用二分法来确定区间?3.……经历了半天的搜寻与刷题,我对此才有了答案。
题目解答
二分法的使用前提:该数组为有序数组(一般题目不涉及重复元素)
1.1 二分法
这题是一个标准的二分法题目,代码很简单,我列出这题是为了说明三个问题
本题代码:
解释:
问题1:取决于区间的定义。二分法本质是将一个区间不断平分,最后确定到一个数或找不到。因此需要每一次划分后的区间应该满足区间的定义。
注:区间的定义:一个区间的形式有4种情况,题目一般都是前面两种。
- 前闭后闭 [left , right]
- 前闭后开 [left , right)
- 前开后闭 (left , right]
- 前开后开 (left , right)
本题很明显是属于第一种情况(因为target可能是里面的任何数)
在第一种情况循环while(left <= right)
里面就是 <=,因为要满足区间的定义,在前闭后闭的情况下,存在left = right的情况,比如: [1,1] 即1,是存在的,故while循环要包括二者相等的情况。
问题3:正是在前闭后闭的情况下,当nums[middle] <或 > target时 这是不是表明下标为middle的元素肯定不是我们要找的,这时对left或right进行调整,如果改为left = middle
由于是前闭后闭则middle将被包括在折半后的区间里,而middle明显不是我们需要的
故left = middle + 1
这样便将middle排除在外.
所以当区间为[left, right)时while(left < right),left = middle+1,right = middle
此时right不被包括在其中。
问题2:middle,left,right
都是int类型,如果写成middle = (left+right)/2
则可能left+right的值超过int类型,造成溢出,写成middle = left + (right-left)/2
可以解决溢出问题。
1.2 搜索插入位置
本题在基础的二分法上加上返回应当插入的位置。
代码如下
int searchInsert (int* nums, int numsSize, int target){
int left = 0;
int right = numsSize - 1;
while(left <= right) {
int middle = left + (right-left)/2;
if(nums[middle] > target)
right = middle - 1;
else if(nums[middle] < target)
left = middle + 1;
else
return middle;
}
return left;
}
关于返回插入的位置为什么为 left?这可以通过画图来解释。
如果target不在该数组内,查找的结果总是right最后为最小区间的左端,left最后为最小区间的右端
如:最后right指向3,left指向5,【3,5】为能找的最小区间
1.3 x的平方根
本题也是用二分法求解,相比于前面给出了明确的数组,本题的数组则是隐藏的。
根据x的范围,x的平方根是在 [0, x] (为闭区间是因为x=0或1的情况)。所以相当于是在一个元素为[0,1,2,3……x]的有序且连续的数组寻找满足条件的值,这思路就相当于把上面一题搜索插入位置中返回插入位置,改为返回插入位置的左相邻位置
注意:本题middle*middle存在溢出,因此改为了long long类型
,当然还有另一种写法middle < x/middle
但我写的代码不能这样改,因为存在 /0(除0的情况).
int mySqrt(int x) {
int left = 0;
int right = x;
while(left <= right)
{
long long middle = left + (right - left)/2;
if(middle*middle < x) {
left = middle + 1;
}
else if(middle*middle > x) {
right = middle - 1;
}
else {
return middle;
}
}
return right;
}
评论区有一个更简洁的代码,可以使用 middle > x/middle,
int mySqrt(int x)
{
if(x == 1)
return 1;
int left = 0;
int right = x;
while(right-left>1)
{
int middle = left + (right-left)/2;
if(x/middle < middle)
right = middle;
else
left = middle;
}
return left;
}
1.4 在排序数组中查找元素的第一个和最后一个位置
在排序数组中查找元素的第一个和最后一个位置
本题难度就比前几题大了很多。
本题依然用二分法求解,难点在于涉及重复元素。
解释在代码下方:
int GetLeftBorder(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
int flag = 1;
while(left <= right) {
flag = 0;
int mid = left + (right-left)/2;
if (nums[mid] >= target) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return flag ? -1 : left;
}
int GetRightBorder(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
int flag = 1;
while(left <= right) {
flag = 0;
int mid = left + (right-left)/2;
if(nums[mid] <= target) {
left = mid+1;
}
else {
right = mid-1;
}
}
return flag ? -1 : right;
}
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
int LeftTarget = GetLeftBorder(nums,numsSize,target);
int RightTarget = GetRightBorder(nums,numsSize,target);
int* ans = (int*)malloc(sizeof(int)*2);
ans[0] = -1;
ans[1] = -1;
*returnSize = 2;
if(LeftTarget != -1 && RightTarget != -1
&& LeftTarget <= RightTarget) {
ans[0] = LeftTarget;
ans[1] = RightTarget;
}
return ans;
}
思路:用二分法分别确定最左边的target和最右边的target
首先:确定区间的定义:前闭后闭。因为要查询整个数组,且存在数组只有一个元素的情况,即结果为[0,0]。
然后:分析有多少种情况:
- target在该数组中如示例1,则最后RightTarget >= LeftTarget
- target数组有元素,但不存在target,如示例2,则最后LeftTarget > RightTarget
- 数组为空,则不会进行查找,如示例3,则LeftTatget/RightTarget为-1
注:1中为什么是 >=,因为存在 {1},1;结果为[0,0],可以相等。