参考:
问题列表
| 序号 | 题目 | 完成 |
|---|---|---|
| 704. 二分查找 | ✅ | |
| 35. 搜索插入位置 | ✅ | |
| 34. 在排序数组中查找元素的第一个和最后一个位置 | ✅ | |
| 1011. 在 D 天内送达包裹的能力 | ✅ | |
| 410. 分割数组的最大值 | ✅ | |
| 875. 爱吃香蕉的珂珂 | ✅ | |
| 287. 寻找重复数 | ||
| 658. 找到 K 个最接近的元素 | ||
| 793. 阶乘函数后 K 个零 | ||
| 剑指 Offer 53 - I. 在排序数组中查找数字 I | ✅ | |
| 剑指 Offer II 073. 狒狒吃香蕉 | ✅ |
题解
基础二分算法
二分查找算法的示例,只能处理无重复字段的集合的查找。
// 递归写法
class Solution {
public int search(int[] nums, int target) {
return search(nums, 0, nums.length - 1, target);
}
public int search(int[] nums, int start, int end, int target) {
if (start > end) {
return -1;
}
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
return search(nums, mid + 1, end, target);
} else {
return search(nums, start, mid - 1, target);
}
}
}
// 迭代写法
class Solution {
public 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;
}
}
class Solution {
public int searchInsert(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 {
right = mid - 1;
}
}
return left;
}
}
含有重复元素的左右边界
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] ans = new int[2];
int len = nums.length - 1;
ans[0] = searchLeft(nums, 0, len, target);
ans[1] = ans[0] == -1 ? -1 : searchRight(nums, 0, len, target);
return ans;
}
public int searchLeft(int[] nums, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 分两种情况,只有一个目标,有多个目标
// 只有一个时,right = ans - 1, left 不断增大直到最后一个left = mid + 1,left就是最终的结果
// 有多个时,每遇到一次相等,right就减1,最后变成只有一个的情况
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
if (left == nums.length) {
return -1;
}
return nums[left] == target ? left : -1;
}
public int searchRight(int[] nums, int left, int right, int target) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 分两种情况,只有一个目标,有多个目标
// 只有一个时,left = ans + 1, right不断的缩小,知道最后一个right = mid -1,right就是最终的结果
// 有多个时,每遇到一次相等,left就加1,最后变成只有一个的情况
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
if (right < 0) {
// 因为right一直在减少,所以判断right的下限
return -1;
}
return nums[right] == target ? right : -1;
}
}
// 左开右闭的写法,更简单些
class Solution {
public int search(int[] nums, int target) {
int leftIdx = findLeft(nums, target);
if (leftIdx == -1) {
return 0;
}
int rightIdx = findRight(nums, target);
return rightIdx - leftIdx + 1;
}
public int findLeft(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 {
left = mid + 1;
}
}
return left;
}
public int findRight(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
// 当找到 target 时,收缩左侧边界
left = mid + 1;
} else {
right = mid;
}
}
return left - 1;
}
}
二分查找的应用
首先想到暴力解法,时间复杂度不是很理想。
执行耗时:284 ms,击败了5.86% 的Java用户
内存消耗:46.8 MB,击败了15.82% 的Java用户
于是想怎么优化,其实容量和天数是线性关系,确定了容量之后,所花费的天数其实是一个非递增数组,这种情况必然要想到用二分查找。
// 暴力解法
class Solution {
public int shipWithinDays(int[] weights, int days) {
int max = 0;
int sum = 0;
for (int num : weights) {
max = Math.max(max, num);
sum += num;
}
// 如果一天运完,直接返回总数
if (days == 1) {
return sum;
}
int len = weights.length;
// 随着容量的增大,需要的天数会变小
for (int capacity = max; capacity <= sum; capacity++) {
int right = 0;
int total = 0;
int currentDays = 1;
while (right < len && currentDays <= days) {
total += weights[right];
// 超过容量的时候,重新计数,day+1
if (total > capacity) {
// 重新计数
total = weights[right];
right++;
currentDays++;
continue;
}
// 没超过容量,再计算下一个
right++;
}
if (currentDays > days) {
continue;
}
return capacity;
}
return sum;
}
}
//二分查找
class Solution {
public int shipWithinDays(int[] weights, int days) {
int left = 0;
int right = 0;
for (int num : weights) {
left = Math.max(left, num);
right += num;
}
// 如果需要一天运完,直接返回总数,无需遍历
if (days == 1) {
return right;
}
// 随着容量的增大,需要的天数会变小
// left = max, right = sum, currentDays会慢慢减少,currentDays是一个非递增数组,找左边界
while (left <= right) {
int mid = left + (right - left) / 2;
if (getDays(weights, mid) <= days) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
// 根据容量得到运完需要的天数
public int getDays(int[] weights, int capacity) {
int right = 0;
int total = 0;
int days = 1;
while (right < weights.length) {
total += weights[right];
// 超过容量的时候,重新计数,day+1
if (total > capacity) {
// 重新计数
total = weights[right];
days++;
}
// 没超过容量,再计算下一个
right++;
}
return days;
}
}
//leetcode submit region end(Prohibit modification and deletion)
//方法一:二分查找
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length;
int l = 1, r = n - 1, ans = -1;
// cnt[i] 表示nums数组中小于等于i的数有多少个
// i的范围为[1,n]
// 4, 3, 2, 7, 8, 6, 5, 3, 1
// i | 1 2 3 4 5 6 7 8 9
// cnt | 1 2 4 5 6 7 8 9 9
// 假设重复的数是target
// [1, target-1]里所有的cnt[i]都小于等于i
// [target, n]里所有的cnt[i]都大于i
while (l <= r) {
int mid = (l + r) >> 1;
int cnt = 0;
// 计算cnt
for (int i = 0; i < n; ++i) {
if (nums[i] <= mid) {
cnt++;
}
}
// 找cnt,cnt是大于i的第一个数,可以理解为找右边界
if (cnt <= mid) {
l = mid + 1;
} else {
r = mid - 1;
ans = mid;
}
}
return ans;
}
}
class Solution {
public int minEatingSpeed(int[] piles, int h) {
// 显然速度约大,h越小,那么h就是一个非递增数组,求最小的k,即求左边界
// 最少要吃一个,否则无意义
int left = 1;
// 最多不超过max,否则无意义
int right = 1;
for (int pile : piles) {
right = Math.max(right, pile);
}
while (left < right) {
int mid = left + (right - left) / 2;
if (getHours(piles, mid) <= h) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
public int getHours(int[] piles, int k) {
int hours = 0;
for (int pile : piles) {
hours += pile / k;
if (pile % k > 0) {
hours++;
}
}
return hours;
}
}
对于这种问题,我们首先应该想到对结果进行分析:
- 对各个子数组的和的范围:
- 最小值:因为m<=nums.length,这就是说最极限的情况是每一个子数组里都只有一个元素,那么此时是元素中的最大值;
- 最大值:m=1时,即是所有元素的和。
- 子数组的和(以下用target代替)与m的关系:
- 随着target的增加,m会减少,因为target是最大子数组和,target越大说明元素越多,分的组自然就越小。
- 由target得到的cnt(实时子数组数量)< m,说明target有点大了,可以减少
- 由target得到的cnt(实时子数组数量)> m,说明target有点小了,可以增加
- 由target得到的cnt(实时子数组数量)== m,说明正好,但是这个时候,我们需要求左边界,所以继续缩小。
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int splitArray(int[] nums, int k) {
int right = 0;
int left = 0;
for (int num : nums) {
right += num;
left = Math.max(left, num);
}
while (left < right) {
int mid = left + (right - left) / 2;
if (getSum(nums, mid) <= k) {
// 小于说明还有优化的空间,target可以继续减少
// 等于说明target是满足的,因为我们要寻找左边界,所以左移
right = mid;
} else {
// 大于,说明target不够,需要增大
left = mid + 1;
}
}
return left;
}
public int getSum(int[] nums, int target) {
int sum = 0;
int cnt = 1;
for (int i = 0; i < nums.length; i++) {
if (sum + nums[i] > target) {
cnt++;
sum = nums[i];
} else {
sum += nums[i];
}
}
return cnt;
}
}
//leetcode submit region end(Prohibit modification and deletion)