给你一个正整数数组 price
,其中 price[i]
表示第 i
类糖果的价格,另给你一个正整数 k
。
商店组合 k
类 不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼盒中任意两种糖果 价格 绝对差的最小值。
返回礼盒的 最大 甜蜜度。
示例 1:
输入: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:
输入:price = [1,3,1], k = 2 输出:2 解释:选出价格分别为 [1,3] 的两类糖果。 礼盒的甜蜜度为 min(|1 - 3|) = min(2) = 2 。 可以证明能够取得的最大甜蜜度就是 2 。
示例 3:
输入:price = [7,7,7,7], k = 2 输出:0 解释:从现有的糖果中任选两类糖果,甜蜜度都会是 0 。
提示:
2 <= k <= price.length <= 105
1 <= price[i] <= 109
class Solution{
public int maximumTastiness(int[] price, int k) {
Arrays.sort(price); // 对价格数组进行排序
int l = 0, r = price[price.length - 1] - price[0]; // 初始化左右边界
while (l < r) {
int mid = (l + r + 1) >> 1; // 取中间值,(l + r + 1) >> 1 相当于 (l + r + 1) / 2
if (check(price, k, mid)) {
l = mid; // 如果check通过,则mid是可行的,将左边界移至mid
} else {
r = mid - 1; // 否则,将右边界移至mid - 1
}
}
return l; // 返回找到的最大可能的最小差值
}
private boolean check(int[] price, int k, int x) {
int cnt = 0, pre = -x; // 初始化计数器cnt和前一个选择的价格pre
for (int cur : price) {
if (cur - pre >= x) {
pre = cur; // 更新前一个选择的价格为当前的cur
++cnt; // 满足条件,计数器加1
}
}
return cnt >= k; // 返回是否能够找到至少k个这样的元素
}
}
- 排序: 首先对价格数组
price
进行升序排序。排序后,数组中相邻的元素差值最小,而两端的元素差值最大。 - 二分查找: 我们使用二分查找来找到最大可能的最小差值。
l
是最小的可能值(0),r
是最大的可能值(最大价格与最小价格之差)。 - 中间值: 在每次循环中,我们计算当前的中间值
mid
,并检查是否可以用这个mid
作为两个价格之间的最小差值。 - 更新边界: 如果
mid
是可行的(即check
方法返回true
),我们将l
移动到mid
,否则将r
移动到mid - 1
。 - 返回结果: 最后,
l
将是我们需要的最大可能的最小差值。
- 初始化: 初始化计数器
cnt
和pre
(前一个选择的价格)为-x
。这里-x
作为pre
的初始值确保第一个价格总是能够被选择。 - 遍历价格数组: 对每个价格
cur
,检查当前价格与上一个选择的价格pre
之间的差值是否大于或等于x
。 - 更新: 如果差值大于或等于
x
,则更新pre
为当前价格cur
,并将计数器cnt
加1。 - 返回结果: 最后判断是否找到了至少
k
个满足条件的元素,如果找到了返回true
,否则返回false
。
总结
这段代码使用二分查找来高效地找到价格数组中能够满足条件的最大可能的最小差值。通过对价格数组进行排序和合理地设置边界条件,确保了算法的正确性和效率。
pre
变量初始化为 -x
是为了确保在第一次迭代时,价格数组中的第一个元素总是能够被选择。x
代表我们在当前二分查找中测试的最小允许差值。-x
的具体作用如下:
1. 保证第一个元素的选择
pre
被初始化为-x
,意味着在遍历价格数组时,第一个元素cur
与pre
之间的差值总是cur - (-x) = cur + x
。因为cur
是一个正数,cur + x
一定大于等于x
,所以if (cur - pre >= x)
这个条件在第一次检查时总是成立。这保证了第一个元素会被选择并且更新pre
为当前的cur
。
2. -x
的含义
- 这里的
-x
实际上是一个策略性的初始值,它并不代表具体的含义,而是为了确保逻辑上cur - pre >= x
在第一次判断时总是为true
。这避免了额外的边界处理,也简化了代码。
示例解释:
假设 x = 5
,价格数组为 [3, 8, 14, 20]
,并且我们想要选择 k = 3
个元素。
- 初始化:
pre = -5
- 第一次迭代:
cur = 3
cur - pre = 3 - (-5) = 8
(大于等于5
)- 条件成立,选择
3
,更新pre = 3
,cnt = 1
- 第二次迭代:
cur = 8
cur - pre = 8 - 3 = 5
(等于5
)- 条件成立,选择
8
,更新pre = 8
,cnt = 2
- 第三次迭代:
cur = 14
cur - pre = 14 - 8 = 6
(大于5
)- 条件成立,选择
14
,更新pre = 14
,cnt = 3
- 完成:此时我们已经选择了
3
个元素,因此返回true
。
在上述示例中,-x
确保了第一次选择价格时不会被错误地跳过,从而保证了算法的正确性。如果没有使用 -x
,而是将 pre
初始化为 0
,那么在第一次选择时,cur - pre
可能不会满足条件,从而导致少选择一个元素,逻辑上不正确。
总之,pre = -x
是一种简化代码的技巧,用来确保第一个元素在任何情况下都能被正确选择。
int mid = (l + r + 1) >> 1;
这一行代码的作用是计算二分查找中的中间值 (mid
),并且使用了位移操作符 >>
来实现整除。我们来逐步解析这段代码。
二分查找的中间值计算
在二分查找中,我们需要不断地计算当前范围的中间值,以决定下一步要检查哪一半。通常,中间值的计算方式如下:
java
复制代码
int mid = (l + r) / 2;
其中,l
是当前区间的左边界,r
是右边界。mid
就是 l
和 r
的中间位置。
为什么使用 (l + r + 1) >> 1;
?
-
避免向下取整偏差: 如果使用
(l + r) / 2
来计算mid
,当l
和r
之间只有两个数时,mid
会偏向较小的那个数。例如,当l = 4
,r = 5
时,(l + r) / 2
会得到4
。而(l + r + 1) >> 1
会得到5
,即它偏向右边一点点。 -
>> 1
的作用:>> 1
是右移位操作符,相当于将一个整数除以2
。因此,(l + r + 1) >> 1
实际上等价于(l + r + 1) / 2
,只不过使用位移操作在某些情况下可能更快(尽管现代编译器已经会优化这种简单除法操作)。
具体解析 (l + r + 1) >> 1
-
l + r + 1
: 在计算中加1
是为了在偶数和奇数的情况下,向上取整。例如,如果l = 4
,r = 5
,l + r + 1 = 10
。当除以2
时得到5
,这使得mid
更加偏向右边,避免始终选择中间偏左的元素。 -
>> 1
: 右移一位,即等于除以2
,结果是得到mid
的值。
例子
假设 l = 4
,r = 5
:
mid = (l + r + 1) >> 1 = (4 + 5 + 1) >> 1 = 10 >> 1 = 5
,mid
被设置为5
。
假设 l = 2
,r = 3
:
mid = (l + r + 1) >> 1 = (2 + 3 + 1) >> 1 = 6 >> 1 = 3
,mid
被设置为3
。
这保证了当 l
和 r
接近时,mid
总是更偏向右边,这对某些二分查找问题有利,特别是当 l
和 r
的取值范围较小时。
prev 这里的设定要求是p取第一个值为真