代码框架
int binarySearch(int[] nums, int target) {int left = 0, right = ...;while(...) {int mid = left + (right - left) / 2;if (nums[mid] == target) {...} else if (nums[mid] < target) {left = ...} else if (nums[mid] > target) {right = ...}}return ...;
}
另外提前说明一下,计算 mid
时需要防止溢出,代码中 left + (right - left) / 2
就和 (left + right) / 2
的结果相同,但是有效防止了 left
和 right
太大,直接相加导致溢出的情况。
一、寻找一个数(基本的二分搜索)
class Solution {// 标准的二分搜索框架,搜索目标元素的索引,若不存在则返回 -1public int search(int[] nums, int target) {int left = 0;// 注意int 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 if (nums[mid] > target) {// 注意right = mid - 1;}}return -1;}
}
核心思路: [left, right]
两端都闭的区间。这个区间其实就是每次进行搜索的区间。
如果 target
不存在,搜索左侧边界的二分搜索返回的索引是大于 target
的最小索引。
此算法有什么缺陷?
答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。
比如说给你有序数组 nums = [1,2,2,2,3]
,target
为 2,此算法返回的索引是 2,没错。但是如果我想得到 target
的左侧边界,即索引 1,或者我想得到 target
的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见,你也许会说,找到一个 target
,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
二、寻找左侧边界的二分搜索
int left_bound(int[] nums, int target) {int left = 0;// 注意int right = nums.length;// 注意while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {right = mid;} else if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {// 注意right = mid;}}return left;
}
为什么是 left = mid + 1
和 right = mid
?
为什么 left = mid + 1
,right = mid
?和之前的算法不一样?
答:这个很好解释,因为我们的「搜索区间」是 [left, right)
左闭右开,所以当 nums[mid]
被检测之后,下一步应该去 mid
的左侧或者右侧区间搜索,即 [left, mid)
或 [mid + 1, right)
。
为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target
这种情况的处理:
if (nums[mid] == target)right = mid;
可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right
,在区间 [left, mid)
中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
左闭右闭写法
int left_bound(int[] nums, int target) {int left = 0, right = nums.length - 1;// 搜索区间为 [left, right]while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {// 搜索区间变为 [mid+1, right]left = mid + 1;} else if (nums[mid] > target) {// 搜索区间变为 [left, mid-1]right = mid - 1;} else if (nums[mid] == target) {// 收缩右侧边界right = mid - 1;}}// 判断 target 是否存在于 nums 中// 如果越界,target 肯定不存在,返回 -1if (left < 0 || left >= nums.length) {return -1;}// 判断一下 nums[left] 是不是 targetreturn nums[left] == target ? left : -1;
}
三、寻找右侧边界的二分查找
类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:
int right_bound(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 if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid;}}// 注意return left - 1;
}
当 target
不存在时,right_bound
返回值的含义
直接说结论,和前面讲的 left_bound
相反:如果 target
不存在,搜索右侧边界的二分搜索返回的索引是小于 target
的最大索引。
int right_bound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] == target) {// 这里改成收缩左侧边界即可left = mid + 1;}}// 最后改成返回 left - 1if (left - 1 < 0 || left - 1 >= nums.length) {return -1;}return nums[left - 1] == target ? (left - 1) : -1;
}
875. 爱吃香蕉的珂珂 - 力扣(LeetCode)
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。示例 1:输入:piles = [3,6,7,11], h = 8
输出:4
示例 2:输入:piles = [30,11,23,4,20], h = 5
输出:30
示例 3:输入:piles = [30,11,23,4,20], h = 6
输出:23// 完整可运行版本
class Solution {public int minEatingSpeed(int[] piles, int h) {int left = 1;int right = 1_000_000_000;while (left <= right) {int mid = left + (right - left) / 2;if (f(piles, mid) == h) {// 搜索左侧边界,则需要收缩右侧边界right = mid - 1;} else if (f(piles, mid) < h) {// 需要让 f(x) 的返回值大一些right = mid - 1;} else if (f(piles, mid) > h) {// 需要让 f(x) 的返回值小一些left = mid + 1;}}return left;}long f(int[] piles, int x) {long hour = 0;for (int i = 0; i < piles.length; i++) {hour += piles[i] / x;if (piles[i] % x > 0) {hour++;}}return hour;}
}
难点是如何抽象成二分搜索
还有坑点在int会出现数字越界的case,要用long
1011. 在 D 天内送达包裹的能力 - 力扣(LeetCode)
传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。示例 1:输入:weights = [1,2,3,4,5,6,7,8,9,10], days = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
class Solution {public int shipWithinDays(int[] weights, int days) {int left = 0;// 注意,right 是开区间,所以额外加一int right = 1;for (int w : weights) {left = Math.max(left, w);right += w;}while (left < right) {int mid = left + (right - left) / 2;int cap = timeByLoad(weights, mid);if (cap > days) {left = mid + 1;} else if (cap < days) {right = mid;} else {right = mid;}}return left;}public int timeByLoad(int[] weights, int load) {int days = 0;for (int i = 0; i < weights.length; ) {int cap = load;while (i < weights.length) {if (cap < weights[i])break;elsecap -= weights[i];i++;}days++;}return days;}
}
重点注意需要对left和right进行剪纸不然会判超时.