1、说明
二分是一个理解起来比较简单,但是实际做起来边界情况非常复杂的问题。大量的题目可以使用二分来做,但是细节问题非常复杂
在没有详细了解二分之前,以下几个问题我是没有概念的:
-
是
while(left <= right)
还是while(left < right)
? -
是
mid = (left + right) / 2
还是mid = (left + right + 1) / 2
? -
为什么有时候直接
return left
就可以,为什么有时候就直接在循环内返回了?
2、解释
本文并不想对二分做一个详细的介绍,而是重点想回答一下上面的几个一直以来没有理解的几个问题
<
还是<=
,实际上最主要的区别就在于是想把数据分为两个部分还是想分为三个部分,对于同样一道题目,两种思路应该都可以做到,详见leetcode34,理论上来说分为两个部分代码更简单,但是分为三个部分理解起来更简单。
之所以理解起来更简单,是因为在判断的时候我们有三种判断,并且对于大于和小于我们只要针对l和r进行改变即可:
针对leetcode34第二部分:
while(l <= r) {
int mid = l + r >> 1; // 分为三部分,我们就不用考虑mid的上下取整问题
if(nums[mid] == target) {
if(mid + 1 == nums.length || nums[mid+1] > nums[mid]) {
return mid;
} else {
l = mid + 1;
}
} else if(nums[mid] > target) {
r = mid - 1; // 分为三部分,我们就不用考虑l和r的边界问题
} else {
l = mid + 1;
}
}
当然,如果是while(l < r)
代表分为两个部分,对于分为两个部分来说,稍微复杂一点:我们需要判断mid属于左半部分还是属于右半部分
- 如果mid属于左半部分,那么判断时应该为
left = mid + 1
、right = mid
的搭配; - 如果mid属于右半部分,那么判断时候应该为
left = mid
、right = mid - 1
的搭配;
如leetcode34第一部分
// 情况1
while(l < r) {
int mid = l + r >> 1; // 此时mid为向下取整
if(nums[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
但是当mid属于右半部分时候,在计算mid的时候我们应该向上取整,原因就是当两个元素时候的特殊情况,会造成死循环
// 找到数最后一个为target索引值
private int findLastIndex(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while(l < r) {
int mid = l + r + 1 >> 1; // 这里应该向上取整
if(nums[mid] > target) {
r = mid - 1;
} else {
l = mid;
}
}
return l;
}
经过以上讨论,我们就可以做到回答上面的问题:
1、是while(left <= right)
还是while(left < right)
一般来说都可以,但是有些时候我们要具体问题
不过前者是将数据分为三个部分,后者是分为两个部分,前者不用考虑上下取整,理解起来简单。
后者需要考虑上下取整问题,理解起来稍微复杂
2、是mid = (left + right) / 2
还是mid = (left + right + 1) / 2
如上当分为两个部分时候,并且当mid属于右半部分时候,在计算mid的时候我们应该向上取整。
3、为什么有时候直接return l
就可以
这是因为当while(left < right)
时候,退出条件为l == r
,此时我们认为返回任意即可;
但是当数据分为三个部分时候,我们一般认为在循环当中就直接return
,循环外一般是返回异常或者-1
参考资料: