二分查找通常用于一个有序数组中。
x的平方根
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sqrtx
在没有使用sqrt()方法的时候,虽然可以遍历[1,n - 1]进行获取最后的结果,但是耗时,因此我们需要利用二分查找,恰好[1,n - 1]的数字是有序的,可以通过二分查找来实现。
基本思路:
1、在[1,x ]中进行二分查找
- 初始值为left = 1,right = x,定义中间变量为mid,sqrt(基于下面的mid方法求解,需要将right初始值为x).
- 在left <= right的时候,不断进行循环,并且mid = left + (right - left) / 2
- 获取sqrt,即sqrt = x / mid,然后依据sqrt和mid的大小关心,更新left、right:
①sqrt == mid,说明sqrt的平方刚好等于x,直接返回sqrt
②sqrt > mid,说明mid太小,所以为了使得sqrt、mid两者趋于相等,我们需要将mid变大,所以需要移动left,使得left = mid + 1
③sqrt < mid,说明mid太大,所以为了让sqrt、mid两者趋于相等,需要将mid变小,所以需要将right变小,使得right = mid - 1
2、当left > right的时候,直接返回right,才会保证向下取整。即比如8,当退出循环的时候,left为3,right为2,因为8的平方根是2.82842…,向下取整就是2,即right.同时如果x等于0的时候,刚好可以返回0.
为什么mid = left + (right - left) / 2
我们需要取[left,right](这个区间公差为1,所以每一个数字相差1)中的中间值,那么这时候(right - left)表示这一个区间中一共有right - left数字,除以一半,在加上left就是[left,right]区间的中间数字。
对应的代码:
class Solution {
public int mySqrt(int x) {
int l = 1,r = x,mid,sqrt;
while(l <= r){
mid = l + (r - l) / 2;
sqrt = x / mid;
if(sqrt == mid)
return sqrt;
else if(mid > sqrt)
r = mid - 1;
else
l = mid + 1;
}
return r;
}
}
运行结果:
在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 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]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array
利用两次二分查找寻找target的左右两个边界。同时在进行第二个二分查找之前,可以利用第一个二分查找判断target是否存在于数组中,如果不存在,那么就返回[-1,-1],否则进行第二个二分查找。
对应的代码:
class Solution {
public int[] searchRange(int[] nums, int target) {
int right = upperBound(nums,0,nums.length - 1,target);
/*
如果right等于-1,说明target不存在于数组中或者数组长度为0,直接返回
[-1,-1],无需进行第二次二分查找
*/
if(right == -1)
return new int[]{-1,-1};
int left = lowerBound(nums,0,right,target);
return new int[]{left,right};
}
//寻找右边界
public int upperBound(int[] nums,int low,int high,int target){
int mid;
while(low <= high){
mid = (low + high) / 2;
if(nums[mid] <= target)//求解右边界,所以需要不断往右移动
low++;
else
high = mid - 1;
}
/*
如果high在合理的范围(即大于等于0,说明数组的长度不是0)
如果nums[high] == target,说明target存在于数组中,所以返回
high,否则返回-1,表示数组的长度为0或者target没有在数组中
*/
return high >= 0 && nums[high] == target ? high : -1;
}
//寻找左边界
public int lowerBound(int[] nums,int low,int high,int target){
int mid;
while(low <= high){
mid = (low + high) / 2;
if(nums[mid] >= target)//不断向左边移动,从而得到左边界
high = mid - 1;
else
low = mid + 1;
}
return low;
}
}
运行结果:
搜索旋转排序数组II
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 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
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104
进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii
由于进行旋转之前,数组是有序的,并且是非降序,所以逐渐递增的,进行旋转之后,就会分成了两个子数组,此时每一个子数组都是递增的。而我们利用二分查找的前提是在一个有序数组中进行的。所以划分局部有序子数组是本题的关键。
基本步骤:
①定义变量,left、right,mid,并且left = 0,right = nums.length - 1.
②在left小于等于right的时候,不断进行循环。
- mid = (left + high) / 2,获取数组中的中间值,如果这个中间值等于target,那么直接返回true,否则进行下面的步骤。
- 如果nums[mid]的值大于nums[high]数字,说明[low,high]这个区间中,[low,mid]是局部有序,并且都是大于nums[high]的值,因此这时候我们需要判断target是否在[nums[low],nums[mid]]这一个区间中,如果在,就在[low,mid]这一个区间中查找target,否则,说明target可能在[mid + 1,high]区间,这时候更新low,使得low = mid + 1.
- nums[mid] 小于nums[high]的值,表明[mid + 1,high]是局部有序的,所以需要判断target是否在[mid + 1,high]区间中,如果在,就二分查找进行查找,否则更新high,使得high = mid
- nums[mid]等于nums[high]的时候,并且当前的nums[mid]不等于target,此时我们并不能得出[mid,high]的所有数字相等且都不等于target的结论,因为存在[1,1,1,1,1,1,1,1,2,1,1,1,1,1],target = 2,这时候nums[mid] = nums[7] = 1 = nums[high] = nums[13] = 1,但是此时[mid,high]的所有数字并没有相等,所以**需要将high–,**而不是将high = mid。
对应的代码:
class Solution {
public boolean search(int[] nums, int target) {
int low = 0,high = nums.length - 1,mid,pivot,tmp;
//找到中间枢纽
while(low <= high){
mid = (low + high) / 2;
if(nums[mid] == target)
return true;
if(nums[mid] > nums[high]){
/*
如果中间值大于最后一个数字,说明[low,mid]是单调的,并且
target的值在这个区间中,利用二分查找进行判断
*/
if(target >= nums[low] && target < nums[mid])
return bounarySearch(nums,low,mid,target);
//target不在这个区间中,那么说明在右边的区间中,所以low = mid + 1
low = mid + 1;
}
else if(nums[mid] < nums[high]){
/*
如果中间值小于nums[high],说明[mid + 1,high]都是大于
nums[mid]的值,并且是逐渐递增的,所以可以进行二分查找
*/
if(target >= nums[mid + 1] && target <= nums[high])
return bounarySearch(nums,mid + 1,high,target);
//如果没有找到,说明需要将缩小范围为[low,mid]
high = mid;
}else{
/*
中间值等于nums[high],表示[low,high]不是单调的,在某一处是
变化的,所以需要缩小范围,执行high--
而不是认为[mid,high]的所有数字都是相等的(事实并不一
定),然后执行high = mid - 1或者high = mid
*/
high--;
}
}
return false;
}
//二分查找target
public boolean bounarySearch(int[] nums,int low,int high,int target){
int mid;
while(low <= high){
mid = (low + high) / 2;
if(nums[mid] == target)
return true;
else if(nums[mid] > target)
high = mid - 1;
else
low = mid + 1;
}
return false;
}
}
运行结果:
有序数组中的单一元素
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
提示:
1 <= nums.length <= 105
0 <= nums[i] <= 105
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/single-element-in-a-sorted-array
由于数组有序,所以考虑二分查找,并且每一个数字出现两次,只有一个单一数字,表示数组的总个数必然是奇数,所以单一数字所在的那一侧数字总数必然是奇数。基于这个原理进行更新left、right。
①获取中间值下标mid = (left + right) / 2
②这时候中间值有可能是单一数字,也有可能不是,并且如果中间值不是单一数字的时候,要么和左边相邻数字相等,要么和右边相邻数字相等,因此
可以分为几种情况进行考虑:
- 中间值等于左边相邻的数字,即nums[mid] == nums[mid - 1],那么左边不等于中间值的数字个数为(mid - 1 - left),那么我们需要考虑左边不等于中间值的数字个数的奇偶性,当(mid - 1 - left) % 2 == 0的时候,左边不等于中间值的数字个数为偶数,表明单一数字在中间值的右侧,所以更新left,使其等于mid + 1,否则,(mid - 1 - left) % 2 != 0的时候,表示左边不等于中间值数字个数为奇数,表示单一数字在中间值的左侧,更新right为mid - 2(因为mid - 1下标对应的值和当前mid的值相等,所以不是mid - 1)
- 中间值等于右边相邻的数字,即nums[mid] == nums[mid + 1],那么右边不等于中间值的数字个数为(right - mid - 1),这时候需要考虑右边不等于中间值的数字个数的奇偶性,从而判断单一数字在中间值的哪一侧。如果(right - mid - 1) % 2 == 0,表示中间值右边不等于中间值的数字个数为偶数,这时候单一数字在中间值左侧,所以更新right为mid - 1,否则,(right - mid - 1)% 2 != 0,表示中间值右边不等于中间值的数字个数为奇数,这时候表示单一数字在中间值的右侧,所以更新left为mid + 2
- 中间值不等于左右两个相邻数字,表示它就是单一数字,直接返回nums[mid]
对应的代码:
class Solution {
public int singleNonDuplicate(int[] nums) {
int left = 0,right = nums.length - 1,mid;
while(left <= right){
mid = (left + right) / 2;
if(mid - 1 >= 0 && nums[mid] == nums[mid - 1]){
/*
如果当前中间下标的值等于左边相邻的数字,并不能断定单一数字
一定在右侧,例如[1,1,2,3,3,4,4,8,8],此时中间下标为4,和下
标为3的数字相等,但是单一数字却在左侧。所以当中间数字等于
左边相邻数字的时候,则左边数字不等于中间值的个数为mid - 1
- left,如果是奇数,表示单一数字在左侧,所以需要更新right
使其等于mid - 2,否则,如果是偶数,表示在右侧,更新left,
使其等于mid + 1
*/
if((mid - 1 - left) % 2 == 0) //如果左侧不等于中间值的数字个数是偶数,那么说明单一数字在中间数字的右侧
left = mid + 1;
else//左侧为不等于中间值的数字个数为奇数,表示在中间值的左侧
right = mid -2;
}else if(mid + 1 < nums.length && nums[mid + 1] == nums[mid]){
/*
如果中间下标的值等于右边相邻的数字的时候,需要知道右边不等
于中间值数字的个数,即right - mid - 1,这时候如果是偶数,
说明单一数字出现中间值的左侧,所以更新right为mid - 1
否则,如果是奇数,表示单一数字出现右侧,更新left为mid + 2
*/
if((right - mid - 1) % 2 == 0)
right = mid - 1;
else
left = mid + 2;
}else //左右两个数字都不想等,表示中间值就是单一数字
return nums[mid];
}
return -1;
}
}
运行结果:
长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
方法一:暴力解决
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int res = 0,i,j = 0,sum = 0;
for(i = 0; i < nums.length; ++i){
sum += nums[i];
while(sum >= target){
if(res == 0 || i - j + 1 < res)
res = i - j + 1;
sum -= nums[j++];
}
}
return res;
}
}
运行结果:
方法二:前缀和+二分查找
定义一个sum数组,表示[0,i]之间的元素的和。由于nums数组中的元素是正整数,所以sum数组必定是递增的数组,因此是有序的,符合二分查找的条件。
假设[ j, i]是一个连续子数组,并且这个连续子数组的和大于等于target,其中[ j, i]的连续子数组等于nums[ j ] + nums[ j + 1] + nums[ j + 2] + ...... + nums[ i ],也就等于了sum[ i ] - sum[ j - 1],所以满足sum[ i ] - sum[ j - 1] >= target,这时候在知道 i 的情况下,需要求解出满足条件的sum[ j - 1],并且使其距离 i 最近即可
所以遍历sum,每次循环进行二分查找:
1、定义res,初始值为0,表示符合条件的最小连续子数组的长度
2、如果当前的sum[ low ] 小于等于target,这时候需要讨论sum是小于target,还是等于target,如果等于target,则表示[0 , low]的连续子数组的和刚好等于target,这时候需要将low - 0 + 1赋值给res
3、否则,sum[ i ]大于target,那么需要进行二分查找,寻找距离 low 最近的sum[ j - 1] ,使得sum[ i ] - sum[ j - 1] >= targert并且长度最短,也即sum[j - 1] <= sum[ i ] - target,因此只要sum[j - 1]在满足条件下,sum[ j - 1]尽可能地大,就会使得连续子数组个数越小,此时表示[ j , i]这个连续子数组的和大于等于target,需要将j - i + 1赋值给res.
- 在findIndex方法中执行二分查找的方法,查找最大地sum[ j ],使得sum[ j ] >= sum[low] - target,因此该二分查找和寻找sum[low] - target的右边界的道理类似
- 当sum[mid] 小于等于 target的时候,需要更新low,使sum[mid]变大,所以low = mid + 1
- 否则,sum[mid]大于target的时候,需要将sum[mid]减小,所以更新right,使得right = mid - 1
- 当left > right得时候,退出循环,然后返回left
对应的代码:
class Solution {
public int findIndex(int[] sum,int left,int right,int target){
int mid;
while(left <= right){
mid = (left + right) / 2;
if(sum[mid] > target){
//target小于sum[mid]得时候,表示target在sum[mid]得左边,所以更新right,使其等于mid - 1
right = mid - 1;
}else{
/*
当sum[mid]小于target,表示target在mid得右边,所以使left =
mid + 1,同时当sum 等于target得时候,为了将连续子数组长度
尽可能小,需要将left右移,再次进行left = mid + 1,所以将在
sum[mid]小于等于target得时候更新left
*/
left = mid + 1;
}
}
return left;
}
public int minSubArrayLen(int target, int[] nums) {
if(nums.length <= 0)
return 0;
int[] sum = new int[nums.length + 1];
int i,low = 0,res = 0,index;
sum[0] = nums[0];//[0,0]的前缀和为nums[0]
for(i = 1; i < nums.length; ++i){
sum[i] = nums[i] + sum[i - 1];
}
//利用二分查找进行查找j - 1的下标
for(;low < nums.length; ++low){
if(sum[low] <= target){
if(sum[low] == target && (res == 0 ||low + 1< res))
res = low + 1;
}else{
//如果当前[0,i]的连续和大于target,进行二分查找
index = findIndex(sum,0,low - 1,sum[low] - target);//由于该方法参数right的值使low - 1,所以导致left不会发生越界的情况
if(res == 0 || low - index + 1 < res)
res = low - index + 1;
}
}
return res;
}
}
运行结果: