题目: 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1],OJ链接
二分查找的三种写法
二分查找通常有以下几种写法,区别主要在于 while 里面:
形式 | 结论与建议 |
---|---|
while (left <= right) | 简单问题用,在循环体里能找到答案以后退出。 |
while (left < right) | 复杂问题用,把答案留到退出循环以后,再判断。是解决二分问题的利器,尤其在边界问题用,这种方式考虑细节最少,但是需要一定练习才能灵活运用。 |
while (left + 1 < right) | 不建议,本质上和 while (left <= right) 写法一样,盲目套这个所谓的最无脑模板,反而学不会二分 |
LeetCode 第 34 题采用 while(left <= right)
本篇题解采用 while(left <= right) 这种二分查找的写法,并分析如何在循环体里设置 left 和 right,和应该返回 left 和 right,即怎样思考边界问题。只要逻辑是完备,并且足够细心,写对二分查找问题并不困难。
第 1 部分:查找 target 出现的第 1 个位置
二分查找的基本用法是在一个有序数组里查找目标元素,具体是看区间中间元素的值 nums[mid] 与 target 的大小关系。
- 如果等于,就可以直接返回;
- 如果严格大于,就往右边查找;
- 如果严格小于,就往左边查找。
- 如果当前看到的元素 恰好等于 target,那么当前元素有可能是 target 出现的第 11 个位置,由于我们要找第 11 个位置,此时我们应该向左边继续查找;
- 如果当前看到的元素 严格大于 target,那么当前元素一定不是要找的 target 出现的第 11 个位置,第 11 个位置肯定出现在 mid 的 左边 ,因此就需要在 [left, mid] 区间里继续查找;
- 如果当前看到的元素 严格小于 target,那么当前元素一定不是要找的 target 出现的第 11 个位置,第 11 个位置肯定出现在 mid 的 右边 ,因此就需要在 [mid + 1, right] 区间里继续查找。
代码 1:
private int findFirstPosition(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) {
// ① 不可以直接返回,应该继续向左边找,即 [left..mid - 1] 区间里找
right = mid - 1;
} else if (nums[mid] < target) {
// 应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else {
// 此时 nums[mid] > target,应该继续向左边找,即 [left..mid - 1] 区间里找
right = mid - 1;
}
}
// 此时 left 和 right 的位置关系是 [right, left],注意上面的 ①,此时 left 才是第 1 次元素出现的位置
// 因此还需要特别做一次判断
if (left != nums.length && nums[left] == target) {
return left;
}
return -1;
}
解释:
- 第 11 次出现的位置和最后 11 次出现的位置肯定都在数组里。因此,初始化的时候 left = 0 、 right = nums.length - 1;
- nums[mid] == target 的时候,在 [left, mid - 1] 区间里找,有没有可能 nums[mid] 就是第 11 次出现的位置,有可能,但不要紧,退出循环的时候 right 指针在左,left 在右。如果数组里存在 target,那么 left 一定位于 target 出现的第 11 个位置,请看下图。
还有一种特殊情况,当要查找的目标元素不存在的时,分两种情况:(1)target 很大,(2)target 很小:
target 很大,还是上面的例子:
target 很小,还是上面的例子:
以上特殊例子,解释了为什么在 while (left <= right) 退出循环以后,需要单独判断 left 是否越界,以及判断 nums[left] 是不是目标元素的原因。
第 2 部分:查找 target 出现的最后 1 个位置
可以直接看注释。代码 2:
private int findLastPosition(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) {
// 只有这里不一样:不可以直接返回,应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else if (nums[mid] < target) {
// 应该继续向右边找,即 [mid + 1, right] 区间里找
left = mid + 1;
} else {
// 此时 nums[mid] > target,应该继续向左边找,即 [left, mid - 1] 区间里找
right = mid