title: 算法篇-二分搜索
date: 2023-4-10 20:12:57
categories:
- 算法
tags: - Java
- 计算机基础
- 数据结构
- 算法
- 二分搜索
参考:
- 个人网站:www.huangrd.top
- 力扣windliang:https://leetcode.cn/problems/search-a-2d-matrix-ii/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-5-4/
- 力扣Krahets:https://leetcode.cn/leetbook/read/illustration-of-algorithm/5dj09d/
- 代码随想录:https://programmercarl.com/
二分搜索
基础概述
Binary Search
对于已经有序的数组,使用二分搜索加快搜索速度
时间复杂度O(log N)
-
有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,查找成功需要比较的次数
-
使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过?次比较
-
在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次
对于前两个题目,记得一个简要判断口诀:奇数二分取中间,偶数二分取中间靠左。对于后一道题目,需要知道公式:
n = log2 N = log_{10}N/log_{10}2
其中 n 为查找次数,N 为元素个数
二分法的两种写法:
- 左闭右闭[left,right]
- 左闭右开[left,right)
左闭右闭
target 是在一个在左闭右闭的区间里,[left, right] 。
对于左闭右闭的写法:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
/**
* 左闭右闭
*/
public int binarySearch1(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;//区间[left,right]要有意义
while (left <= right) {//注意点1
int mid = left + ((right - left) / 2);//防止溢出
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else return mid;
}
return -1;
}
左闭右开
定义 target 是在一个在左闭右开的区间里,也就是[left, right)。
对于左闭右开的写法:
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
/**
* 左闭右开
* */
public int binarySearch2(int[] nums, int target) {
int left = 0;
int right = nums.length;//区间[left,right)
while (left < right) {
int mid = left + ((right - left) / 2);
if (nums[mid] < target) {//左闭
left = mid + 1;
} else if (nums[mid] > target) {//右开
right = mid;
} else return mid;
}
return -1;
}
二分法查找左右边界
public static int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
public static int binarySearchFindLeft(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
// 不返回,收缩右边界,找到左侧边界 合并 > 情况
right = mid;
}
}
return left != nums.length && nums[left] == target ? left : -1;
}
public static int binarySearchFindRight(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
// 不返回,收缩左边界,找到左右侧边界
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left != 0 && nums[left - 1] == target ? left - 1 : -1;
}
public static void main(String[] args) {
int[] nums = {1, 5, 8, 11, 19, 19, 19, 22, 31, 35, 40, 45, 48, 49, 50};
System.out.println(binarySearchFindLeft(nums, 19));
System.out.println(binarySearch(nums, 19));
System.out.println(binarySearchFindRight(nums, 19));
}
力扣
LC69. x 的平方根
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
利用二分法的思路,在0~x-1中搜索出他的平方根,排除特例。
public int mySqrt(int x) {
/**
* 二分查找
* [left,right)
* */
//因为定义的区间为左闭右开,对于x,当x < 2的时候,x才会取0 or x 即 0 1
if (x < 2) return x;
int left = 1;
int right = x;
while (left < right) {
int mid = left + ((right - left) / 2);
if (mid > x / mid) {
//mid > x / mid防止溢出
right = mid;
} else if (mid < x / mid) {
left = mid + 1;
} else return mid;
}
return right - 1;
}
面试题目要求:求某正整数的平方根,要求误差小于0.01
//二分法进行处理,注意变量类型
public static double sqrt(int target){
double n=1e-2; //在这里可根据精度要求进行调整
double l=0,r=target; //这里若初始化r=target/2的话,则处理较小正整数会出错,如1,2
while(l<=r){ //二分查找
double mid=(l+r)/2;
//这里用除法,而不用mid*mid与target比较,是防止mid过大时,mid*mid产生溢出问题
if(target/mid<mid){
r=mid-n;
}else{
l=mid+n;
}
}
return r;
}
}
LC367. 有效的完全平方数
给定一个 正整数 num
,编写一个函数,如果 num
是一个完全平方数,则返回 true
,否则返回 false
。
进阶:不要 使用任何内置的库函数,如 sqrt
。
防止越界,用long表示。
/**
* 二分法
* [left,right]左闭右闭
* */
public boolean isPerfectSquare(int num) {
int left = 0;
int right = num;
while (left <= right) {
int mid = left + ((right - left) / 2);
long square = (long) mid * mid;
if (square > num) {
right = mid - 1;
} else if (square < num) {
left = mid + 1;
} else{
return true;
}
}
return false;
}
LC1802. 有界数组中指定下标处的最大值
给你三个正整数 n
、index
和 maxSum
。你需要构造一个同时满足下述所有条件的数组 nums
(下标 从 0 开始 计数):
nums.length == n
nums[i]
是 正整数 ,其中0 <= i < n
abs(nums[i] - nums[i+1]) <= 1
,其中0 <= i < n-1
nums
中所有元素之和不超过maxSum
nums[index]
的值被 最大化
返回你所构造的数组中的 nums[index]
。
注意:abs(x)
等于 x
的前提是 x >= 0
;否则,abs(x)
等于 -x
。
示例 1:
输入:n = 4, index = 2, maxSum = 6
输出:2
解释:数组 [1,1,2,1] 和 [1,2,2,1] 满足所有条件。不存在其他在指定下标处具有更大值的有效数组。
示例 2:
输入:n = 6, index = 1, maxSum = 10
输出:3
提示:
1 <= n <= maxSum <= 109
0 <= index < n
Related Topics
贪心
二分查找
👍 81
👎 0
根据题目描述,如果我们确定了 nums[index] 的值为 x,此时我们可以找到一个最小的数组总和。也就是说,在 index 左侧的数组元素从 x-1 每次递减 1,如果减到 1 后还有剩余元素,那么剩余的元素都为 1;同样的,在 index 及右侧的数组元素从 x 也是每次递减 1,如果减到 1 后还有剩余元素,那么剩余的元素也都为 1。
这样我们就可以计算出数组的总和,如果总和小于等于 maxSum,那么此时的 x 是合法的。随着 x 的增大,数组的总和也会增大,因此我们可以使用二分查找的方法,找到一个最大的且符合条件的 x。
为了方便计算数组左侧、右侧的元素之和,我们定义一个函数 sum(x, cnt),表示一共有 cnt 个元素,且最大值为 x 的数组的总和。函数 sum(x, cnt) 可以分为两种情况:
计算[0,idx] 和 [idx, n - 1]区间所有元素的和
设cnt为区间的元素个数,即idx + 1 或 n - idx
1. cnt >= x, 那么会有多余的数全部放置为1: cnt - mid, 剩下的数为1,2,...x, 和为(x+1)*x/2。
总和 = cnt - x + (x+1)*x/2
2. cnt < x, 那么放置的数为 x - cnt + 1, x - cnt, ...., x即[x-cnt+1, x],
总和 = (2x-cnt+1) * cnt / 2
-
如果 x ≥ cnt,那么数组的总和为 [(x+x−cnt+1)×cnt]/2
-
如果 x < cnt,那么数组的总和为 [(x+1)×x]/2+cnt−x
对于满足条件的最大的 x,x+1,x+2,… 一定不满足条件,而 1,2,…,x 这些数都满足条件,我们需要寻找最大的x,由于 n=10^9, 直接循环肯定不行,我们使用二分查找来寻找满足条件的最大x。
接下来,定义二分的左边界 left = 1,右边界 right = maxSum,然后二分查找 nums[index] 的值 mid,如果 sum(mid−1,index) + sum(mid,n−index) ≤ maxSum,那么此时的 mid 是合法的,我们可以将 left 更新为 mid,否则我们将 right 更新为 mid - 1。
最后将 left 作为答案返回即可。
class Solution {
public int maxValue(int n, int index, int maxSum) {
int left = 1, right = maxSum;
while (left < right) {
// 当用到的是r=mid是,就是mid=l+r>>1,当用到的是l=mid时,就是mid=l+r+1>>1,这是别人总结出来的经验,这样就避免了出现各种各样的边界错误。
int mid = (left + right + 1) >>> 1;
if (sum(mid - 1, index) + sum(mid, n - index) <= maxSum) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
}
private long sum(long x, int cnt) {
return x >= cnt ? (x + x - cnt + 1) * cnt / 2 : (x + 1) * x / 2 + cnt - x;
}
}
复杂度分析:
- 时间复杂度 O*(logN)*
- 空间复杂度 O(1)。其中 N = maxSum
LC33. 搜索旋转排序数组
整数数组 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,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums
中的每个值都 独一无二- 题目数据保证
nums
在预先未知的某个下标上进行了旋转 -104 <= target <= 104
Related Topics
数组
二分查找
👍 2442
👎 0
解题思路:
先根据 nums[mid] 与 nums[left] 的关系判断 mid 是在左段还是右段,接下来再判断 target 是在 mid 的左边还是右边,从而来调整左右边界 left 和 right。
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) { // [,]左闭右闭
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 先根据 nums[mid] 与 nums[left] 的关系判断 mid 是在左段还是右段
if (nums[mid] >= nums[left]) {
// 再判断 target 是在 mid 的左边还是右边,从而调整左右边界 left 和 right
if (target < nums[mid] && target >= nums[left]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
复杂度分析:
- 时间复杂度O(log N)
- 空间复杂度O(1)
LC240. 搜索二维矩阵 II
编写一个高效的算法来搜索 *m* x *n*
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
提示:
m == matrix.length
n == matrix[i].length
1 <= n, m <= 300
-109 <= matrix[i][j] <= 109
- 每行的所有元素从左到右升序排列
- 每列的所有元素从上到下升序排列
-109 <= target <= 109
Related Topics
数组
二分查找
分治
矩阵
👍 1251
👎 0
方法一 逐行二分
逐行进行二分搜索,根据有序性:
- 当某一行第一个元素 > target,则没找到 target, 结束。
- 当某一行最后一个元素 < target,则改行没有 target, 下一行继续。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
for (int i = 0; i < matrix.length; i++) {
// 1. 第一个元素大于tar
if (matrix[i][0] > target) {
return false;
}
// 2. 最后一个元素小于tar
if (matrix[i][matrix[i].length - 1] < target) {
continue;
}
if (binarySearch(matrix[i], target) != -1) {
return true;
}
}
return false;
}
private int binarySearch(int[] matrix, int target) {
int left = 0, right = matrix.length - 1;
while (left <= right) {
int mid = left + right >> 1;
if (matrix[mid] == target) {
return mid;
} else if (matrix[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
}
复杂度分析
- 时间复杂度 O(M * log N)
- 空间复杂度 O(1)
方法二 二分查找数
从右上角出发开始遍历,会发现每次都是向左数字会变小,向下数字会变大,有点和 二分查找树
相似。二分查找树的话,是向左数字变小,向右数字变大。
所以我们可以把 target
和当前值比较。
- 如果
target
的值大于当前值,那么就向下走。 - 如果
target
的值小于当前值,那么就向左走。 - 如果相等的话,直接返回
true
。
也可以换个角度思考:
如果 target
的值小于当前值,也就意味着当前值所在的列肯定不会存在 target
了,可以把当前列去掉,从新的右上角的值开始遍历。
同理,如果 target
的值大于当前值,也就意味着当前值所在的行肯定不会存在 target
了,可以把当前行去掉,从新的右上角的值开始遍历。
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
如果 target = 9,如果我们从 15 开始遍历, cur = 15
target < 15, 去掉当前列, cur = 11
[1, 4, 7, 11],
[2, 5, 8, 12],
[3, 6, 9, 16],
[10, 13, 14, 17],
[18, 21, 23, 26]
target < 11, 去掉当前列, cur = 7
[1, 4, 7],
[2, 5, 8],
[3, 6, 9],
[10, 13, 14],
[18, 21, 23]
target > 7, 去掉当前行, cur = 8
[2, 5, 8],
[3, 6, 9],
[10, 13, 14],
[18, 21, 23]
target > 8, 去掉当前行, cur = 9, 遍历结束
[3, 6, 9],
[10, 13, 14],
[18, 21, 23]
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int row = 0, col = matrix[0].length - 1;
while (row < matrix.length && col >= 0) {
if (target == matrix[row][col]) {
return true;
} else if (target < matrix[row][col]) {
col--;
} else {
row++;
}
}
return false;
}
}
复杂度分析:
- 时间复杂度:O(M + N)
- 空间复杂度:O(1)
方法三 二维二分
找到矩阵的中心,然后和目标值比较看能不能丢弃一些元素。
如下图,中心位置是 9
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, /9/,16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
通过中心位置, 我们可以把原矩形分成四个矩形, 左上, 右上, 左下, 右下
[1, 4, 7 [11, 15
2, 5, 8 12, 19
3, 6, /9/] 16, 22]
[10, 13, 14 [17, 24
18, 21, 23] 26, 30]
如果 target = 10,
此时中心值小于目标值,左上角矩形中所有的数都小于目标值,我们可以丢弃左上角的矩形,继续从剩下三个矩形中寻找
如果 target = 5,
此时中心值大于目标值,右下角矩形中所有的数都大于目标值,那么我们可以丢弃右下角的矩形,继续从剩下三个矩形中寻找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
return searchMatrixHelper(matrix, 0, 0, matrix[0].length - 1, matrix.length - 1, matrix[0].length - 1, matrix.length - 1, target);
}
private boolean searchMatrixHelper(int[][] matrix, int x1, int y1, int x2, int y2, int xMax, int yMax, int target) {
//只需要判断左上角坐标即可
if (x1 > xMax || y1 > yMax) {
return false;
}
// x表示列,y表示行
if (x1 == x2 && y1 == y2) {
return target == matrix[y1][x1];
}
int m1 = (x1 + x2) >>> 1;
int m2 = (y1 + y2) >>> 1;
if (matrix[m2][m1] == target) {
return true;
}
if (matrix[m2][m1] < target) {
// 右上矩阵
return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) ||
// 左下矩阵
searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) ||
// 右下矩阵
searchMatrixHelper(matrix, m1 + 1, m2 + 1, x2, y2, x2, y2, target);
} else {
// 右上矩阵
return searchMatrixHelper(matrix, m1 + 1, y1, x2, m2, x2, y2, target) ||
// 左下矩阵
searchMatrixHelper(matrix, x1, m2 + 1, m1, y2, x2, y2, target) ||
// 左上矩阵
searchMatrixHelper(matrix, x1, y1, m1, m2, x2, y2, target);
}
}
}
复杂度分析:
- 时间复杂度:O(n(log_43)),大约n^0.8.
LC34. 在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 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
Related Topics
数组
二分查找
👍 2255
👎 0
先查找左边界,再从左边界向后二分查找右边界。
class Solution {
public int[] searchRange(int[] nums, int target) {
// 先找最左边位置
int left = binarySearchLeft(nums, target);
if (left == -1) {
return new int[]{-1, -1};
}
// 再找最右边的位置
int right = binarySearchRight(nums, left, target);
return new int[]{left, right};
}
private int binarySearchRight(int[] nums, int left, int target) {
int right = nums.length;
while (left < right) {
int mid = left + ((right - left) >>> 1);
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left != 0 && nums[left - 1] == target ? left - 1 : -1;
}
private int binarySearchLeft(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >>> 1);
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left != nums.length && nums[left] == target ? left : -1;
}
}
复杂度分析:
- 时间复杂度:O(log N)
- 空间复杂度:O(1)