二分查找的思想就是通过判断中间值与目标值的大小来逐步缩短目标区间
将大规模问题转换为若干个子问题,解在其中一个子问题里
- 查找算法
- 数组有序或是有序的某种变体
- 时间复杂度为O(logn)
二分查找的框架
public int binarySearch(int[] nums, int target) {
int left = 0;
int rigth = ...;
while(...)
{
int mid = right + (right - left) / 2;
if(nums[mid] == target)
...;
else if(nums[mid] < target)
left = ...;
else if(nums[mid] > target)/
right = ...;
}
return ...;
}
- 取中间数:mid = right + (right - left) / 2,而不是 (left + right) / 2,left + right可能大于整型范围
- 使用else if 将所有情况都列出,展示所有细节
- …为需要填入的变化的细节
基本二分查找——寻找一个数
这是最简单的二分查找,寻找一个数,存在则返回其索引,否则返回 -1
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; //细节1
while(left <= right) //细节2
{
int mid = right + (right - left) / 2;
if(nums[mid] == target)
return mid; //细节3
else if(nums[mid] < target)
left = mid + 1; //细节4
else if(nums[mid] > target)/
right = mid - 1; //细节5
}
return -1; //细节6
}
细节1、2:为什么循环条件是left <= right 而不是left < right ?
因为初始化right的赋值为nums.length - 1, 而不是nums.length
这两者出现在功能不同的二分查找中,区别:前者搜索区间为[left, right],后者搜索区间为[left, right),因为nums.length角标越界
当nums[mid] == target 时,循环终止,若没找到该数,那么应该循环终止,返回 -1,终止的时候搜索区间应当为空,表示没有元素可以找了,就是没找到
while(left <= right)的终止条件是 left == right + 1,即[right + 1, right],这个区间是不存在的,即搜索区间为空, 这时直接返回 - 1是正确的
while(left < right)的终止条件是 left == right, [right, right],这时搜索区间非空,但循环终止了,这个区间被漏掉并没有搜索,这时直接返回 - 1就会出现错误
如果非要使用while(left < right),我们加个判断即可
return nums[left] == target ? left : -1;
细节4、5:为什么要 left = mid + 1,right = mid - 1?
当我们发现mid并不是要找的target, 那么当然是去搜索[left, mid - 1]或[mid + 1, right],mid我们已经判断过了, 不需要加入搜索区间了
但该算法无法得到target的左侧和右侧边界,向左向右线性查找会破坏二分的对数时间复杂度
寻找左侧边界的二分查找
public int binarySearch(int[] nums, int target) {
int left = 0;
int rigth = nums.length; //细节1
while(left < right) //细节2
{
int mid = right + (right - left) / 2;
if(nums[mid] == target)
right == mid; //细节3
else if(nums[mid] < target)
left == mid + 1; //细节4
else if(nums[mid] > target)/
right == mid; //细节5
}
if(left == nums.length)
return -1;
return nums[left] == target ? left : -1; //细节6
}
细节1、2:为什么循环条件是left < right 而不是left <= right ?
此代码的循环区间为[left, right), while(left < right) 终止的条件是 left == right,此时搜索区间 [left, left) 恰巧为空,所以可以正确终止
为什么right 初始赋值为nums.length?
请看细节6解析
细节3:为什么nums[mid] == target不直接返回而是right == mid ?
我们在找到target后不是立即返回,而是缩小right, 在区间[left, mid)中继续搜索,不断向左搜索达到锁定左侧边界的目的
细节4、5:为什么 left = mid + 1,right = mid ?,和之前不一样?
此代码的循环区间为[left, right),当mid被检测后应当分为[left, mid) 和[mid + 1, right)
细节6:为什么没有返回-1的操作,如果数组中没有target值怎么办
【左侧边界的理解】
对于上述数组,返回值为1,可以理解为:数组中小于2的元素有1个
如果target == 1, 返回值为0,数组中小于1的元素有0个
如果target == 5,返回值为4, 数组中小于5的元素有4个
那么return 的区间为[0, nums.length],我们添加return判断即可正确返回 - 1
这也就是为什么right 初始赋值为nums.length,因为我们有可能left == rigth == nums.length
while(...)
{...}
if(left == nums.length)
return -1;
return nums[left] == target ? left : -1;
为什么返回left?
循环终止时left == right,返回哪个都行
寻找右侧边界的二分查找
整体思路与寻找左侧边界类似,只是有两处不同
public int binarySearch(int[] nums, int target) {
int left = 0;
int rigth = nums.length;
while(left < right)
{
int mid = right + (right - left) / 2;
if(nums[mid] == target)
left == mid + 1; //细节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;; //细节2
}
细节1 nums[mid] == target 时 left == mid + 1?
我们在找到target后不是立即返回,而是缩小left, 在区间[mid + 1, rigth)中继续搜索,不断向右搜索达到锁定右侧边界的目的
细节2为什么返回left - 1,而不是right?
while 循环的终止条件是 left == right,所以 left 和 right 是一样的,如果非要体现右侧的特点,返回 right - 1 即可
减一是右侧判断的特殊之处,因为我们对 left 的更新必须是 left = mid + 1,即mid = left - 1,就是说 while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left - 1]可能是target。
不存在该值时也要进行判断
left 的取值范围是 [0, nums.length]
当left== 0时,0 是目标的右侧,因此该目标不存在