二分查找

例题:返回长度为N、按升序排列的整数数组nums中第一个大于等于target的数的位置,如果所有数都小于等于target,返回数组长度。注意算法时间复杂度必须为$O(\log N)$

考虑二分查找(红蓝染色法),指定$L,R$分别为询问的左右边界,考虑开区间的情况即$(L,R)$,$M=\lfloor (L+R)/2\rfloor$为当前正在询问的数字。

如果nums[M] < target,则表明M左边的数组都小于target,需要查询M右边的数字,更新L=M,即将边界更新为(M, R),否则表明nums[M]右侧的数都大于等于target,第一个大于等于target的数一定为M或在M左侧,更新R=M,即将边界更新为(L,M),终止条件为L + 1 >= R,此时R指向的数就一定为第一个给大于等于target的数。

在排序数组中查找元素的第一个和最后一个位置

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

1
2
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

1
2
输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • -10^9 <= target <= 10^9

注意到题目中提到数组元素按非递减顺序排序,则可以采用二分查找法。

首先实现方法int lowerBound(int[] nums, int target):给定一个按照非递减顺序排列的整数数组 nums,和一个目标值 target,找出第一个大于等于target的数的位置。那么可以将其他问题使用该方法解决:

  • 找出第一个大于target的数的位置:lowerBound(nums, target+1)
  • 找出最后一个小于target的数位置:lowerBound(nums, target)-1

  • 找出最后一个小于等于target的数位置:lowerBound(nums, target+1)-1

这里使用两侧开区间的循环不变式,即(left, right),循环不变式中所有在left以及left左边的数都必定严格小于target,所有在right以及right右边的数都必定大于等于target,则代码可写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
/**
* 求解非递减nums数组中第一个大于等于target的数的索引(此处使用开区间)
* @param nums 非递减整数数组
* @param target 目标值
* @return 目标值的索引
*/
public int lowerBound(int [] nums, int target) {
int left = -1, right = nums.length, mid;
while (left + 1 < right) { // (left, right)当left+1 == right时就没有元素了,退出循环
mid = left + (right - left) / 2; // 防止整形溢出
if (nums[mid] < target) { // < 保证left以及left左边的数都是严格小于target
left = mid; // (mid+1, right)
}
else { // 即nums[mid] >= target 保证right右边的数都是大于等于target
right = mid; // (left, mid)
}
}
return right;
}

public int[] searchRange(int[] nums, int target) {
int start = lowerBound(nums, target); // 求第一个大于等于target的数的索引
if (start == nums.length || nums[start] != target) {
return new int[] {-1, -1};
}

// 注意上面start判断存在的话 表明target存在 则end必然存在
// 求第一个大于等于target+1的数的索引前一个索引 即最后一个小于等于target的数的索引
int end = lowerBound(nums, target + 1) - 1;
return new int[] {start, end};
}
}

礼盒的最大甜蜜度

2517. 礼盒的最大甜蜜度 - 力扣(LeetCode)

给你一个正整数数组 price ,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k

商店组合 k不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼盒中任意两种糖果 价格 绝对差的最小值。

返回礼盒的 最大 甜蜜度

示例 1:

1
2
3
4
5
输入:price = [13,5,1,8,21,2], k = 3
输出:8
解释:选出价格分别为 [13,5,21] 的三类糖果。
礼盒的甜蜜度为 min(|13 - 5|, |13 - 21|, |5 - 21|) = min(8, 8, 16) = 8 。
可以证明能够取得的最大甜蜜度就是 8 。

示例 2:

1
2
3
4
5
输入:price = [1,3,1], k = 2
输出:2
解释:选出价格分别为 [1,3] 的两类糖果。
礼盒的甜蜜度为 min(|1 - 3|) = min(2) = 2 。
可以证明能够取得的最大甜蜜度就是 2 。

示例 3:

1
2
3
输入:price = [7,7,7,7], k = 2
输出:0
解释:从现有的糖果中任选两类糖果,甜蜜度都会是 0 。

此处可以得知可能的甜蜜度的下限为0,上限为最大价格减去最小价格。所以先对price升序排序,确定好甜蜜度的上下限再进行二分查找。查找时的判断条件为判断当前甜蜜度能否满足至少有k类糖果符合甜蜜度的要求。

对于条件判断,可以考虑相邻元素之间的差值,以price[0]初始化参考价格prev,向后遍历,如果price[i] - prev < mid,则继续向后遍历;如果price[i] - prev >= mid,则表明price[i]符合当前甜蜜度的要求,添加到选择的糖果类别中并将prev更新为price[i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public int maximumTastiness(int[] price, int k) {
Arrays.sort(price);
int cnt = 0, ans = 0, prev;
int left = 0, right = price[price.length-1] - price[0], mid;
while (left <= right){ // [left, right]
mid = left + (right - left) / 2;
cnt = 1;
prev = price[0];
for (int i = 1; i < price.length; ++i){
if (price[i] - prev >= mid) {
cnt++;
prev = price[i];
}
}
if (cnt < k) {
right = mid - 1; // 当前甜蜜度太高, 降低
}
else {
ans = mid; // 当前甜蜜度符合要求, 是可能的最大甜蜜度
left = mid + 1; // 继续寻找是否有满足要求的更大的甜蜜度
}
}
return ans;
}
}