二分查找
适用于有序数组,思想比较简单,每次折半搜索,时间复杂度为O(logN)。具体实现参考 Search.java
,特别需要注意二分查找的边界问题。
标准写法:
class Solution {
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) { // 退出循环时left=right+1区间内没有任何一个元素,所以要跳出循环
int mid = left + (right - left)/2; // 防止溢出
if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
return mid; // 如果target存在,则一定会进入本分支
}
}
return left; // target不存在,返回插入位置,也可返回-1表示未找到
}
}
复制代码
注意:
-
上述算法正确运行的前提是不存在重复元素,如果存在重复元素,则无法保证最终返回的是重复元素中的哪一个元素的下标。如果
nums
中不存在目标元素,则返回的是target应该插入的位置。 -
为何是
while(left <= right)
?- 算法初始化时
right = nums.length - 1
,搜索下标的区间的应该是左闭右闭的[left, right],因为可能存在更新left = mid + 1 = right
的情况(eg:在[0, 2]中找2,此时mid = right = left = 1没有被搜索就直接认为元素不存在了),所以不能写成while(left < right)
。 - 循环退出条件为
left = right+1
,此时可以保证在未找到元素时返回目标元素的正确插入位置(如果目标元素比原始数组中的任何元素都大的话)。
- 算法初始化时
-
如果写成
while(left != right)
,当数组长度为1时,直接无法找到又有效下标(如果是直接返回-1的话);当数组中没有目标元素时,程序陷入死循环。 -
为何是
mid = left + (right - left)/2
?- 如果写成
mid = (left + right)/2
,左右边界相加可能超过Integer.MAX_VALUE
导致越界。而(first + last) / 2 = (2 * first + last - first) / 2 = first + length / 2
, 其中length = last - first
为区间长度。此时不可能存在越界的情况。
- 如果写成
left从0起始,mid是向下取整,left只在mid遇到 确定 小于目标数时才前进一步,left一直在朝着第一个目标数的位置在逼近。永远记住left是对的!!!
而right的收缩往往是大胆的(如写法二部分),所以right不可取。
写法二
寻找左侧边界的二分查找
记住 标准写法
最后的原则!此时寻找最左侧的目标值将变得很简单:
class Solution {
public int binarySearchFirst(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left)/2;
if (nums[mid] >= target) { // 取等号的时候不能直接返回,因为左边可能还有相同的数字
right = mid - 1;
} else {
left = mid + 1;
}
}
return left; // 如果target不存在则left表示插入位置
/*
if (left != nums.length && nums[left] == target) {
return left;
}
return -1; // 表示未找到
*/
}
}
复制代码
寻找右侧边界的二分查找
和寻找左侧边界解法类似,只是最终需要返回right
而不是left
(因为此时左侧的前进是激进的,右侧是保守的)!如果目标元素不存在,返回的下标没有意义!
class Solution {
public int binarySearchLast(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right;
}
}
复制代码
相关问题
-
基于值的二分查找:参考leetcode.problem378 有序矩阵中第K小的元素
-
while循环条件不一定是
left <= right 复制代码
- leetcode.problem540 有序数组中的单一元素
- leetcode.problem278 有序矩阵中第K小的元素
-
局部有序,旋转数组相关的题目
- leetcode.problem33 寻找旋转数组中的元素 I
-
同时寻找左右边界
- leetcode.problem34 在排序数组中查找元素的第一个和最后一个位置
待补充。。。
总结
while 中到底是 left < right
还是 left <= right
(假定left=0
,right=n-1
分别为左右两端元素的下标值)?
left <= right
的循环退出条件为left = right + 1
, 此时适用于原始数组中午target
值时可以返回target
的正确插入位置(return left
)left < right
的循环退出条件为left = right
, 此时适用于不允许数组越界访问的情形(因为循环体可能可能存在mid+1可能越界的情况), 此时return left/right
均可。
本文正在参加「金石计划 . 瓜分6万现金大奖」