前言
二分法(Binary search)是一种常用的查找算法,适用于有序数组或有序列表的查找操作。它通过将目标值与数组(或列表)中间位置的元素进行比较,从而确定目标值可能出现的位置范围。如果目标值等于中间位置的元素,则查找成功;如果目标值小于中间位置的元素,则在数组的前半部分进行继续查找;如果目标值大于中间位置的元素,则在数组的后半部分进行继续查找。通过不断地缩小查找范围,最终可以找到目标值或确定目标值不存在于数组(或列表)中。
所以在此分享一下二分查找的几种形式和对应的代码
题目描述
这个是leetcode上的[34. 在排序数组中查找元素的第一个和最后一个位置],难度为中等。
给你一个按照非递减顺序排列的整数数组 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]
思路一
当我们看到这题后,最简单的想法是直接线性遍历,直接遍历数组找第一个和最后一个。但是题目明确要求了要使用O(log n)的时间复杂度解决就不能考虑线性的。所以我们只能选择考虑其他性质。
思路二
我们注意到其他的性质是非递减顺序排列的整数数组,这个性质我们是没有使用的。所以我们就可以考虑前言中二分查找,但是这个查找并不是定位于mid这个数而是聚焦于最后一个出现的mid和第一个出现的mid。我们就以第一个出现的mid为例,来讨论下二分查找的不同写法。注意这些写法主要是开闭区间的不一样。
左开右开区间
在这个区间下,我们选择初始化left = -1,right = n(n是数组的长度)设置mid=left+(right-left)/2(这样写是为了防止溢出整数)。然后不断缩减的范围。我们针对这个写法举一个例子
int nums[] = [1,2,3,3,3,4,5] 找3第一个出现
这个时候mid是 -1+(7-(-1))/2 = 3 (向下取整)
我们的原则就是确保r+1的范围是大于等于target
left-1的范围是小于target的(注意这个区间的定义是比较重要的)
然后我们看nums[mid] = 3是大于等于target,所以我们将right更新到mid
即right=mid //有的善于思考的同学就会问,为什么不是right = mid-1,这种情况我们先不谈,我们可以看最后的结果知道答案。
mid = -1+(3+1)/2 = 1
nums[mid] = 2<3
left = mid
这个时候只有下标为2的3还没有确定大小我们继续这个过程
mid = 2
nums[mid] = 3=3
right = 2
继续执行,我们这个时候范围就是(1,2)理论上是没有数,但是我们的遍历条件是left+1<right停止循环结束返回left+1或者right
这个时候我们可以说为什么不用mid+1的原因了,主要因为我们初始状态和区间定义。首先我要说明为什么初始状态left =--1,right=n。这是因为区间的定义,我们是开区间left下标的所在地方是判断不了的,所以选择不在数组范围的初始化。包括后面为什么不是mid+1,这也是开区间的原因,假设我们是mid+1,mid就没有办法可以被放入大于等于target的区间和小于target的区间。
具体代码如下
public int binary_search(int nums[],int target,int left,int right)
{
// 先判断递归条件,这个是左开右开区间
if (left+1>=right)
{
return left;
}
int mid = (right-left)/2;
if (nums[mid]<target)
{
return binary_search(nums, target, mid, right);
}else
return binary_search(nums, target, left, mid);
}
左闭右开区间
这个区间对应left就是变化为left = mid +1,right=mid。道理都是和前一个一样。取得到的话就可以加一。判断条件为left<right。当区间为[left,left)就没有数字参与了。
public int binarySearch(int nums[],int target,int left,int right) {
// 先判断递归条件,这个是左闭右开区间
if (left >= right) {
return right;
}
int mid = (right + left) / 2;
if (nums[mid] < target) {
return binarySearch(nums, target, mid + 1, right);
} else {
return binarySearch(nums, target, left, mid);
}
}
左闭右闭区间
这个初始化为left=0,right=n-1;这个left = mid+1,right = mid-1。区间只有[left+1,left]的区间才会没有数可以选。所以left>right才结束递归
public int binarySearch(int nums[],int target,int left,int right)
{
// 先判断递归条件,这个是左开右开区间
if (left>right)
{
return left;
}
int mid = (right+left)/2;
if (nums[mid]<target)
{
return binarySearch(nums, target, mid+1, right);
}else
{
return binarySearch(nums, target, left, mid-1);
}
}
其他条件的转化
好的,我们回到这道题目中来。我们这样找的下标是第一个下标为大于等于target的下标,但是最后一个大于等于target的坐标要怎么求呢?这在整数数组同时是可以转化的。我们可以求target加1的第一个数的左边(前提得先求到target,因为第一个target存在,所以最后一个也一定存在)。然后小于等于的情况就是转化为大于等于target-1的情况,小于也是同理。
题目具体代码
class Solution {
public int[] searchRange(int[] nums, int target) {
int ans = binarySearch(nums,target,0,nums.length-1);
int res[] =new int[2];
if(ans==nums.length||nums[ans]!=target)
{
res[0] = -1;
res[1] = -1;
return res;
}
res[0] =ans;
res[1] = binarySearch(nums,target+1,0,nums.length-1)-1;
return res;
}
public int binarySearch(int nums[],int target,int left,int right)
{
// 先判断递归条件,这个是左开右开区间
if (left>right)
{
return left;
}
int mid = (right+left)/2;
if (nums[mid]<target)
{
return binarySearch(nums, target, mid+1, right);
}else
{
return binarySearch(nums, target, left, mid-1);
}
}
}
总结
对于二分查找,我们只要能区分二分区间外的性质就可以正确的做出大多数题目。区间内即没还有确定性质的则不是很重要。所以要牢记区间的重要性。