二分查找的一些总结
二分查找
二分查找真的是个很玄妙的算法,因为它的思路很简单,在一个有序的序列中,想要找到某个数,先判断中间的数与这个目标数的大小,如果中间的数大于目标数,那么则认为,目标数在这个序列的左半边,反之则在右半边。
但是思路容易,实现可是极其麻烦,因为总是分不清边界,l 和 r 什么时候是 < 什么时候是 <= ,还有边界的更新,一会是l = mid,一会是r = mid+1啥的,总是让人头晕。
经过在算法基础课的学习,记下了这个模版,在刷题中使用也很方便。
在二分查找中,最主要的还是要确定好边界的处理,防止产生死循环。
模版
- 当mid属于左边时
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1
}
return l;
- 当mid属于右边时
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
例题及思路
Leetcode 704 二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
1.你可以假设 nums 中的所有元素是不重复的。
2.n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
这个题目就是一个标准的二分题了,因为数组是有序的了,所以也不用在排序,直接比较mid的值与target的关系,我们可以发现,当nums[mid] > target时,代表着我们的我们的target在左半边,则r需要缩小。
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0,r = nums.size();
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] == target) return mid;
else if (nums[mid] > target) r = mid;
else l = mid + 1;
}
return -1;
}
};
Leetcode 1283. 使结果不超过阈值的最小除数
给你一个整数数组 nums 和一个正整数 threshold ,你需要选择一个正整数作为除数,然后将数组里每个数都除以它,并对除法结果求和。
请你找出能够使上述结果小于等于阈值 threshold 的除数中 最小 的那个。
每个数除以除数后都向上取整,比方说 7/3 = 3 , 10/2 = 5 。
题目保证一定有解。
示例 1:
输入:nums = [1,2,5,9], threshold = 6
输出:5
解释:如果除数为 1 ,我们可以得到和为 17 (1+2+5+9)。
如果除数为 4 ,我们可以得到和为 7 (1+1+2+3) 。如果除数为 5 ,和为 5 (1+1+1+2)。
示例 2:
输入:nums = [2,3,5,7,11], threshold = 11
输出:3
示例 3:
输入:nums = [19], threshold = 5
输出:4
提示:
- 1 <= nums.length <= 5 * 10^4
- 1 <= nums[i] <= 10^6
- nums.length <= threshold <= 10^6
首先要理解题目的意思,我们需要找到最小的除数,使得元素和除以除数小于target,当看到最小值的时候,就得想到贪心做法,也就是说我们除以5都小于target了,那我们一定不会考虑除以3的事了。
第一步确定边界,最小除数定位1,因为不能除以0,最大的除数定位数组中最大的数,否则你怎么加都不会为target。
之后就按照上面的模版来解决,我们可以发现check函数的性质是,当我们的mid < target时,就代表着我们要找的数在左边,则更新右边。
class Solution {
public:
bool check(vector<int>& nums,int mid,int threshold) {
int sum = 0;
for (auto x : nums) {
sum += x % mid == 0 ? x / mid : x / mid + 1;
if (sum > threshold) return false;
}
return true;
}
int smallestDivisor(vector<int>& nums, int threshold) {
int maxn = nums[0];
for (auto x : nums) {
maxn = max(maxn,x);
}
int l = 1,r = maxn;
while (l < r) {
int mid = l + r >> 1;
if (check(nums,mid,threshold)) r = mid;
else l = mid + 1;
}
return r;
}
};
Leetcode 857.爱吃香蕉的珂珂
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
示例 2:
输入: piles = [30,11,23,4,20], H = 5
输出: 30
示例 3:
输入: piles = [30,11,23,4,20], H = 6
输出: 23
提示:
1 <= piles.length <= 10^4
piles.length <= H <= 10^9
1 <= piles[i] <= 10^9
首先来确定边界,我们左边界定位1,因为猴子珂珂至少吃一个(1个都不吃纯属nt,不过这个题目后面的用例都上万了,也是实属nt),然后右边界定为这个数组中最大的数,因为就算取1个亿,珂珂吃完这一堆香蕉后照样休息不继续了。
然后是我们的check函数,我们可以发现,当我们的mid < target成立时,根据贪心的思想,珂珂还能再吃慢一点,,代表着我们要找的值在左边,所以更新右边
class Solution {
public:
bool check(vector<int>& piles,int v,int H) {
int sum = 0;
for (auto x : piles) {
sum += (x / v) + (x % v == 0 ? 0 : 1);
}
return sum <= H;
}
int minEatingSpeed(vector<int>& piles, int H) {
int maxn = 0;
for (auto x : piles) {
maxn = max(x, maxn);
}
int l = 1;
int r = maxn;
while (l < r) {
int mid = l + r >> 1;
if (check(piles,mid,H)) r = mid;
else l = mid + 1;
}
return l;
}
};
Leetcode 1011 在D天内送达包裹的能力
传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。
传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。
返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力
示例 1:
输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 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) 是不允许的。
示例 2:
输入:weights = [3,2,2,4,1,4], D = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:
输入:weights = [1,2,3,1,1], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
提示:
-
1 <= D <= weights.length <= 50000
-
1 <= weights[i] <= 500
这个题目与上面那两个题目是一个套路,都是标准的套模版。
第一步我们还是来确定边界,我们船的运载能力最小要是这堆货物里的最大的,要不然我一次送一个货物,最大的还送不走,最大的为货物的总和,我一船就送完了。
第二步我们确定check函数,我们发现,当我们可以用小于D的天数就能完成任务时,根据贪心的思想,就代表我如果减少船的容量也是有希望的,代表我们的值在左侧,则更新右边界。
class Solution {
public:
bool check(vector<int>& weights,int capc,int D) {
int sum = 0;
for (auto x : weights) {
if (sum + x > capc) {
sum = 0;
D--;
}
sum += x;
}
return D > 0;
}
int shipWithinDays(vector<int>& weights, int D) {
int sum = 0,maxn = 0;
for (auto x : weights) {
sum += x;
maxn = max(maxn,x);
}
int l = max(sum / D,maxn);
int r = sum;
while (l < r) {
int mid = l + r >> 1;
if (check(weights,mid,D)) r = mid;
else l = mid + 1;
}
return l;
}
};
Leetcode 34 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
题目要求我们时间复杂度为O(log n) 级别,则我们就该想到使用二分查找,这道题目充分考查了我们对check函数的理解,还是老样子
第一步,我们确定边界,我们左边界取0,右边界取最后一个元素的下标。
第二部来确定check函数,第一个元素,则代表着他有如下的性质,它右边所有的数都比他大,所以当我们的mid >= target 时,代表着我们的数在左侧,则我们更新右边界。最后一个元素,则代表着他有着如下的性质,它左边所有的数都比它小,所以当我们的mid <= target 时,代表着我们的数在右侧,则更新左边界。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> res;
int n = nums.size();
if(n == 0 ) return vector<int>({-1,-1});
int l = 0, r = n - 1;
while(l < r){
long long mid = l + r >> 1;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
if(nums[r] != target) return vector<int>({-1,-1});
res.push_back(r);
l = 0,r = n-1;
while(l < r){
long long mid = l + r + 1 >> 1;
if(nums[mid] <= target) l = mid;
else r = mid - 1;
}
res.push_back(r);
return res;
}
};
总结
关于二分查找,总是需要考虑许多细节的地方,尤其是区间的开闭。
举个例子
//1
while (l <= r) {
.....
}
//2
while (l < r) {
.....
}
我们看函数1,l <= r 代表着我们的区间是闭合的,[4,4]是存在的。
我们再看函数2,l < r 代表我们是开区间,[4,4]是不存在的,所以在check里容易直接退出去了,从而扫描不到4
还有要注意的就是左右边界的更新,我们可以记住那两个模版
- 当我们的mid在左边时,我们使r = mid, l = mid + 1,此时mid 就是 (l + r) / 2
- 当我们的mid在右边时,我们使l = mid, r = mid - 1,此时mid为(l + r + 1) / 2
在计算mid的时候,容易爆int,我们可以写为(l + (r - l)) / 2
最后总结一下二分查找的做题思路吧
- 确定二分的边界
- 编写框架并完成特判
- 设计一个check函数,确定性质(我个人喜欢把mid放在左边)
- 判断左右边界如何更新
希望大家可以在二分查找中注意处理各种琐碎的问题。