二分算法超详细解析(适合算法新手)
二分搜索算法初级解析
1 需要考虑的几个点
提供的待搜索集合一般是非递减排列
二分搜索算法需要考虑的几个点:
a) 搜索区间的左右闭合问题:
[0,nums.length-1]即左闭右闭区间,[0,nums.length)即左闭右开区间
b) 对于迭代结束条件问题:
- [0,nums.length-1]区间迭代结束条件为left>right
- [0.nums.length)区间迭代结束条件为left>=right,准确来说是left==right
c) 对于迭代时左右指针变换问题:
- [0,nums.length-1]区间左指针变换为mid+1,右指针变换为mid-1
- [0,nums.length)区间左指针变换为mid+1,右指针变换为mid
(因为mid已经在该迭代轮次时搜索了,变换时闭合区间不要再次包含mid,开放区间要包含mid)
d) 二分查找特定值一般使用左闭右闭区间,找到等于该值的位置即可
(有重复元素存在时无所谓该位置是不是对应第一个元素)
e) 二分查找特定值的左/右边界,也就是该元素第一次/最后一次出现的位置,一般使用左闭右开区间
(查找边界问题需要考虑该元素是否真正存在,以及迭代结束时指针越界问题,见下文详解)
2 二分查找特定值
a) 这是最典型最简单的二分搜索算法题,记录下其通用的算法框架:
int binarySearch(int[] nums, int target) {
int left = 0;
// 左闭右闭区间
int right = nums.length - 1;
// 注意迭代结束条件
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
// 注意
left = mid + 1;
else if (nums[mid] > target)
// 注意
right = mid - 1;
}
return -1;
}
3 二分查找左右边界值
a) 该类型算法最大的难点在于:
- 判断期望返回的元素在迭代结束后的准确位置
- 判断需要考虑该元素是否真正存在于集合
- 若元素存在,需要考虑该元素在集合首尾边界位置的情况
- 若元素不存在,需要考虑迭代结束时元素和target大小关系,进而作返回值的处理
- 若元素不存在,需要考虑迭代结束时,左右指针越界导致返回时出错
b) 先给出算法基本框架
【查找左边界】
int left = 0, right = nums.length;
while(left<right){
mid = left + (right-left)/2 ;
// 如果搜索到target相等的元素,意味着至少需要向mid左部搜索
if(nums[mid] == target){
right = mid;
}
// 如果搜索到target小的元素,意味着至少需要向mid右部搜索
else if(nums[mid] < target){
left = mid+1;
}
else if(nums[mid] > target){
right = mid;
}
}
【查找右边界】
int left = 0, right = nums.length;
while(left<right){
mid = left + (right-left)/2 ;
// 如果搜索到target相等的元素,意味着至少需要向mid右部搜索
if(nums[mid] == target){
left = mid+1;
}
// 如果搜索到target小的元素,意味着至少需要向mid右部搜索
else if(nums[mid] < target){
left = mid+1;
}
else if(nums[mid] > target){
right = mid;
}
}
c) 逐个考虑上面列出的难点问题
-
返回值:
对于左边界问题,若在迭代结束时找到target的左边界,有可能是最后一轮left== right =mid+1
得到,也有可能是left== right =mid
只有可能最后迭代是走这两个分支后结束,即返回left或者right就是target(这里默认返回左指针)
return nums[left];
对于右边界问题,若在迭代结束时找到target的右边界,有可能是最后一轮left == right = mid
得到,也有可能是left == right = mid+1
得到,但这里的right不可能指向target,所以left向右搜索一步后和right相等
只有可能最后迭代是走这两个分支后结束,与target相同的元素只有可能是left-1或者right-1(这里默认返回左指针)
return nums[left-1];
-
若元素存在,考虑其位于集合首位边界的情况
由于左边界问题返回的是left,若元素存在,left==right找到target时不会越界; -
若元素不存在时,有两种情况:
情况一:迭代结束时,两指针停在集合中。由于左边界问题迭代结束时可能是left == right = mid+1
,或者是left == right = mid
,由判断分支知道这两种情况指向的都是刚好大于target的元素,即指向大于target的第一个元素;由于右边界问题迭代结束时可能是left == right = mid+1
,或者是left == right = mid
,由判断分支知道这两种情况left-1指向的都是小于target的最大元素,即指向小于target的最后一个元素。
情况二:迭代结束时,左边界问题中的left == right = nums.length
,也就是left一直在找right前进,逼近右边区域,过程中搜索到的元素均小于target,该情况也属于搜索失败了;右边界问题中的left == right = 0
,也就是right一直在找left后退,过程中搜索到的元素均大于target,属于搜索失败
综上所述 -
搜索失败有两种可能:
第一种可能:两指针停在集合中
return nums[left]==target?left:-1; //左边界
return nums[left-1]==target?left-1:-1; //右边界
第二种可能:两指针停在集合首尾边界位置
if(left==nums.length) return -1; //左边界
if(left==0) return -1; //右边界
提供二分搜索左右边界的代码框架:
【左边界】
private int leftBoundSearch(int[] nums, int target){
int left = 0, right = nums.length;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] == target) right = mid;
else if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid;
}
if (left == nums.length) {
return -1;
}
return nums[left] == target ? left : -1;
}
【右边界】
private int rightBoundSearch(int[] nums, int target){
int left = 0, right = nums.length;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] == target) left = mid + 1;
else if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid;
}
if (left == 0 ) {
return -1;
}
return nums[left-1] == target ? left-1 : -1 ;
}