算法解释:
二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取 一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复 杂度为 O(log n)。
举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次 折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一 半。于是我们的查找区间变成了 {3,4,5}。(注意,根据具体情况和您的刷题习惯,这里的 5 可以 保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4,正好是我们 需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍 历数组,最坏的情况则需要查找 5 次。
具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此 有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍:
第一是尝试熟练使用 一种写法,比如左闭右开或左闭右闭(便于处理边界条件), 尽量只保持这一种写法;
第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写 法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。
二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题, 指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。
二分查找模板:
图示:
常规代码模板:
public static int binarySearch(int[] array,int left,int right,int target){
//这里需要注意,循环条件
while (left <= right) {
//这里需要注意,计算mid
int mid = left + ((right - left)/2);
if (nums[mid] == target) {
return mid;
}else if (nums[mid] < target) {
//这里需要注意,左移动
left = mid + 1;
}else if (nums[mid] > target) {
//这里需要注意,右移动
right = mid - 1;
}
}
//没有找到该元素,返回 -1
return -1;
}
注意:
1、计算 mid 时 ,不能使用 (left + right )/ 2,否则有可能会导致(left + right)的值int/long溢出
2、while (left < = right) { } 注意括号内为 left <= right ,而不是 left < right ,如果我们设置条件为 left < right 则当我们执行到最后一步时,则我们的 left 和 right 重叠时,则会跳出循环,返回 -1,区间内不存在该元素,但是不是这样的,我们的 left 和 right 此时指向的就是我们的目标元素 ,但是此时 left = right 跳出循环;(上面动图)
3、left = mid + 1,right = mid - 1 而不是 left = mid 和 right = mid。我们思考一下这种情况,见下图,当我们的target 元素为 16 时,然后我们此时 left = 7 ,right = 8,
mid = left + (right - left)/2 = 7 + (8-7) /2= 7,那如果设置 left = mid 的话,则会进入死循环,mid 值一直为7 。
递归模板写法:
public static int binarySearch(int[] nums,int target,int left, int right) {
if (left <= right) {
int mid = left + ((right - left)/2);
if (nums[mid] == target) {
//查找成功
return mid;
}else if (nums[mid] > target) {
//新的区间,左半区间
return binarySearch(nums,target,left,mid-1);
}else if (nums[mid] < target) {
//新的区间,右半区间
return binarySearch(nums,target,mid+1,right);
}
}
//不存在返回-1
return -1;
}
第一题快速导航:69. x 的平方根
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4 输出: 2
示例 2:
输入: 8 输出: 2 说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
代码两种写法
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
int sqrt = x/mid;
if(sqrt==mid){
return mid;
}else if (sqrt < mid) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return r;
}
}
class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
}
第二种需要考虑 mid * mid越界情况long强转一下。
查找区间快速导航: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]
思路:先找到目标值,因为数组是排好序的,所以找到目标值后需要往两边找相同值
代码:
class Solution {
public int[] searchRange(int[] nums, int target) {
int l = 0;
int r = nums.length - 1;
while(l<=r){
//模板
int mid = l + (r - l)/2;
if(nums[mid] < target){
l = mid +1;
}else if(nums[mid] > target){
r = mid - 1;
}else{
//对找到值之后的逻辑处理,向两边找相同的值,注意可能会存在[8,8,8,8,8]这种情况出现,所以越界处理也要注意到
l = mid;
r = mid;
while(l > 0 && nums[l-1] == target){
l--;
}
while(r < (nums.length - 1) && nums[r+1] == target){
r++;
}
return new int[]{l,r};
}
}
return new int[]{-1,-1};
}
}
旋转数组查找数字 快速导航:81. 搜索旋转排序数组 II
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
示例 1:
输入: nums = [2,5,6,0,0,1,2], target = 0 输出: true
示例 2:
输入: nums = [2,5,6,0,0,1,2], target = 3 输出: false
进阶:
- 这是 搜索旋转排序数组 的延伸题目,本题中的
nums
可能包含重复元素。 - 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
题解:
即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点, 如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。 如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区 间继续二分查找。
注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部 相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行 二分查找。
public boolean search(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return false;
}
int start = 0;
int end = nums.length - 1;
int mid;
while (start <= end) {
mid = start + (end - start) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[start] == nums[mid]) {
start++;
continue;
}
//前半部分有序
if (nums[start] < nums[mid]) {
//target在前半部分
if (nums[mid] > target && nums[start] <= target) {
end = mid - 1;
} else { //否则,去后半部分找
start = mid + 1;
}
} else {
//后半部分有序
//target在后半部分
if (nums[mid] < target && nums[end] >= target) {
start = mid + 1;
} else { //否则,去后半部分找
end = mid - 1;
}
}
}
//一直没找到,返回false
return false;
}
【遇事不决,可问春风!】