基础问题
- 找到这个数
- 找到第一个/最后一个 <= target 的数字/所在位置【<= <= <= <= > > >】
- 找到第一个/最后一个 >= target 的数字/所在位置【< < < >= >= >=】
什么情况可以二分
题目 满足 0 / 1 性质(关于条件单调) ,或者函数满足单调性 都可以进行二分。
二分模板 来自 acwing
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[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)) r = mid; // check()判断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)) l = mid;
else r = mid - 1;
}
return l;
}
实战
剑指 Offer 53 - I. 在排序数组中查找数字 I
public int search(int[] nums, int target) {
if(nums.length == 0) return 0;
int l = 0;
int r = nums.length - 1;
// 找到第一个 >= target 的数字 根据 01 性质来分析,找到·第一个·大于等于target ,既然是第一个 那么就要 逼近 小端点 r = mid; mid 可能是答案。 因为有等于号了。 mid 是否加一问题 : l 不甘心自己 不是 l = mid + 1, 缺少 · +1 · 就补在 mid 计算上。
while (l < r) {
int mid = l + r >> 1;
if (check(nums,mid,target)) r = mid;
else l = mid + 1;
}
int lAns = l;
if (nums[lAns] != target) return 0;
l = 0 ;
r = nums.length - 1;
while (l < r) {
int mid = l + r + 1>> 1;
if (check1(nums,mid,target)) l = mid;
else r = mid - 1;
}
int rAns = l;
return rAns - lAns + 1;
}
boolean check(int[] nums , int mid , int target) {
return nums[mid] >= target;
}
boolean check1(int[] nums , int mid , int target) {
return nums[mid] <= target;
}
}
x 的平方根
class Solution {
public int mySqrt(int x) {
int l = 0 ;
int r = x;
// 2 * 2 = 4 < 8 ;3 * 3 = 9 > 8
// 小于等于 x 的最后一个数字
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(mid , x)) l = mid;
else r = mid - 1;
}
return (int)l;
}
boolean check(long mid , int x) {
return mid <= x / mid;
}
}
剑指 Offer 11. 旋转数组的最小数字
输入: numbers = [3,4,5,1,2]
输出: 1
分段 3,4,5 | 1, 2 : n[r] < 左 n[r] >= 右 可能重复的情况 : 3 , 3 | 2 ,3 n[r] <= 左 、 n[r] >= 右 == 怎么确定? 很显然 不能确定左右了 只有 n[r] < 左 和 n[r] > 右 满足 0 | 1, 那么 我们n[r] 处于 == 这个状态时,直接抛弃。
class Solution {
public int minArray(int[] numbers) {
if (numbers.length == 0) return -1;
int l = 0,r = numbers.length - 1;
while ( l < r) {
int mid = l + r >> 1;
if (numbers[mid] < numbers[r]) r = mid;
else if (numbers[mid] > numbers[r]) l = mid + 1;
else r --;
}
return numbers[l];
}
}
寻找峰值
分析 : 摘自 leetcod 热评
为什么二分查找大的那一半一定会有峰值呢?(即nums[mid]<nums[mid+1]时,mid+1~N一定存在峰值) 我的理解是,首先已知 nums[mid+1]>nums[mid],那么mid+2只有两种可能,一个是大于mid+1,一个是小于mid+1,小于mid+1的情况,那么mid+1就是峰值,大于mid+1的情况,继续向右推,如果一直到数组的末尾都是大于的,那么可以肯定最后一个元素是峰值,因为nums[nums.length]=负无穷
class Solution {
public int findPeakElement(int[] nums) {
int l = 0 , r = nums.length - 1;
while (l < r) {
int mid = l + r >> 1;
// 峰值一定在最左边
if (nums[mid] >= nums[mid + 1]) r = mid ;
else l = mid + 1;
}
return l;
}
}
二分答案
- 对于最大值/最小值问题/给个方案 是否可行 ? 0 / 1 性质 二段性
- 给他一个解、判断它是否合法。二分+判定实现猜答案。
410. 分割数组的最大值
思路参考 : liweiwei1419
class Solution {
public int splitArray(int[] nums, int k) {
int l = 0 , r = 0; // l 每个分一组 r 全部放一起
for (int i : nums){
r += i;
l = Math.max(l,i);
}
// 答案 一定在 l -- 总和之间
while (l < r) {
int mid = l + r >> 1;
if (check(nums,mid,k)) r = mid;
else l = mid + 1;
}
return r;
}
boolean check(int[] nums , int maxSum ,int m) {
// 刚开始是有一组的
int cnt = 1;
int sum = 0;
for (int i = 0 ; i < nums.length ; i ++) {
if (sum + nums[i] <= maxSum) {
sum += nums[i];
}else {
cnt ++;
sum = nums[i];
}
}
// 需要的分组 如果小于 m 就可行
return cnt <= m;
}
/*
将数组分为 m 个非空连续子数组 使得各自和的最大值最小
[7,2,5,10,8] m = 2; 所有情况如下。
[7 | 2 5 10 8] lsum = 7; rsum = 25; max : 25;
[7 2 | 5 10 8] lsum = 9; rsum = 23; max : 23;
[7 2 5 | 10 8] lsum = 14; rsum = 18; max : 18;
[7 2 5 10 | 8] lsum = 24; rsum = 8; max : 24
18 23 24 25 是合法的解空间 18 之前全是不合法的
*/
}
875. 爱吃香蕉的珂珂
类似于上一题的思路
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int l = 1 , r = 0;
for (int i : piles) r = Math.max(i,r);
while (l < r) {
System.out.println ( " l " + l + " r " + r);
int mid = l + (r - l >> 1);
if (check(piles,mid,h)) {
r = mid;
}
else l = mid + 1;
}
return l;
}
boolean check(int[] piles , int speed , int h) {
int needH = 0;
for (int i = 0; i < piles.length; i ++) {
// 这堆香蕉需要 几小时吃完?
needH += piles[i] % speed == 0 ? piles[i] / speed : piles[i] / speed + 1;
// System.out.println("吃第" + i + "堆香蕉" + "花费" + needH + "小时");
}
// System.out.println("每次吃" + speed + "根香蕉" + "吃全部香蕉花费" + needH + "小时");
return needH <= h;
}
}
/** 警卫在 8 小时回来
[3,6,7,11] h = 8; 每个小时 可以吃 k 根 我每次吃 1 根 达到最慢
速度最快 我一小时吃一堆的最大值。
*/