二分搜索算法
二分的本质并不是单调性,但是具有单调性一定可以使用二分,可以使用二分不一定具有单调性。二分的要点是"边界问题"
二分算法看似很容易实现,但是对于处理二分的边界上的细节时却十分复杂。
所以使用二分算法之前需要根据实际问题进行建模,根据不同的性质设置判断条件,划分红蓝
区域,最终定位边界
。
请看下图:
找出蓝色的边界的数字和红色边界的数字。
可以将图中的一串数字根据颜色的不同,而划分成红蓝两个区域,可以使用通过二分法
来确定红蓝区域的边界
。
两头分别设立两个指针,l
和 r
,l
是从左相向右扫描蓝色的数字,r
是从右向左扫描红色的部分。
根据 l
和 r
可以确认mid
的位置:
使用二分法时,mid
指向的位置是蓝色的边界位置,并且,根据以上数字的颜色不同的性质,我们可以知道,在mid
指向的位置开始,左面的都是蓝色的,右面的都是红色的。
想要使用二分找到蓝色的边界,就需要不断的挪动 l 、 r
、mid
这三个指针所指向的位置,在 l
和 mid
还有 r
所指向的位置相同的时候,就代表找到了这个边界。
要找到左边部分也就是蓝色部分的边界,就需要将边界划分为这样的两个部分:
[ l , mid]
和 [ mid+1, r ]
并且需要这样处理 mid: mid = (l + r)/ 2
同理,要找到红色部分的边界时就需要将边界改变 : [ l, mid - 1 ]
和 [ mid, r ]
,因为红色部分的边界是蓝色部分边界的下一个位置,所以mid
需要这样处理:mid = (l + r + 1) / 2
,加一个 1
就正好可以将mid
指向蓝色边界的下一个位置也就是红色边界的位置。
根据以上的操作就可以找到红蓝颜色的边界。
整数二分
二分法查找需要注意边界问题,根据边界的实际情况来选择使用那种模板。其实就是看mid属于左边还是右边。
模板一:
// 区间[l, r]被划分成 [l, mid] 和 [mid+1, r]时使用
int bsearch_1(int l, int r) {
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) // check() 判断 mid是否满足性质
r = mid;
else
l = mid + 1;
}
return l;
}
模板二:
// 区间[l, r]被划分成 [l, mid-1] 和 [mid, r]时使用
int bsearch_2(int l, int r) {
while (l < r) {
int mid = l + r + 1 >> 1; // 注意
if (check(mid)) // check() 判断 mid是否满足性质
l = mid;
else
r = mid - 1;
}
return l;
}
经典问题:
如果给定这个数组 [5,7,7,8,8,10] ,而 target 是 8
根据这个数组可以将问题分成两个部分,<= 8 的部分,和 >= 8 的部分,而这个两个部分的边界值就是我们要寻找的答案
先来找到 <= 8 的部分的边界,8 出现的起始位置:
<= 8
的部分是蓝色的,8
包含在蓝色的范围内。所以需要采用模板一,区间[l, r]
被划分成 [l, mid]
和 [mid+1, r]
时的情况。
在看 >= 8
的部分,也就是 8
的结束位置:
也就是红色部分,此时 8 属于红色部分 ,要找右边部分的边界需要使用模板二,也就是区间[l, r]
被划分成 [l, mid-1]
和 [mid, r]
时的情况。
可以将 mid
指向的数字理解为我们要寻找的答案。
这样,通过两次二分法查找,一次查找左边界,一次查找右边界,就可以找到答案。
class Solution {
public int[] searchRange(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return new int[]{-1, -1};
}
int l = 0, r = nums.length - 1;
int[] res = new int[2];
while (l < r) {
int mid = l + r >> 1;
if (target <= nums[mid]) {
r = mid;
} else {
l = mid + 1;
}
}
if (nums[l] != target) {
res[0] = -1;
res[1] = -1;
return res;
}
res[0] = l;
l = 0;
r = nums.length - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (target >= nums[mid]) {
l = mid;
}else {
r = mid - 1;
}
}
res[1] = l;
return res;
}
}
浮点数二分
对于面向浮点数二分的算法相较于整数二分更简单。
浮点数二分不需要考虑边界值的问题,因为浮点型数字在精度允许的情况下可以不断的二分下去。
浮点数二分模板:
public double bsearch(double n) {
final double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
例题:
- 给定一个浮点数 n,求它的三次方根
- −10000≤n≤10000结果保留 6 位小数
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double n = sc.nextDouble();
double l = -10000, r = 10000;
while (r - l > 1e-8) {
double mid = (l + r)/2;
if(mid * mid * mid > n) r = mid;
else l = mid;
}
System.out.printf("%.6f", l);
}
}