今天分享两道 LeetCode 题目,它们都可以巧妙地利用二分查找来解决。
275. H 指数 II
问题描述

思路:二分查找
H 指数的定义是:一个科学家有 h
篇论文分别被引用了至少 h
次。
题目给定的 citations
数组是升序排列的。这为我们使用二分查找提供了可能。
我们可以尝试猜测一个 H 指数 h
,然后检查是否满足条件。如果 citations
数组中存在 h
篇论文的引用次数都大于等于 h
,那么这个 h
就是一个可能的 H 指数。我们希望找到最大的那个 h
。
考虑数组下标 mid
和数组长度 n
。
如果 citations[mid]
表示第 mid
篇论文的引用次数(数组从0开始计数),那么从 mid
到 n-1
一共有 n - mid
篇论文。
由于数组是升序的,这 n - mid
篇论文的引用次数都至少为 citations[mid]
。
我们的目标是找到一个最大的 h
,使得有 h
篇论文的引用次数至少为 h
。
这可以转化为:找到一个最大的 h
,使得 citations[n-h] >= h
。
解题过程:二分查找 H 指数的边界
我们可以利用二分查找来寻找这个 h
的临界点。
搜索范围是 [0, n]
(可能的h指数范围),但更方便的是直接在数组下标 [0, n-1]
上进行二分。
-
二分条件 :比较
citations[mid]
和n - mid
。n - mid
代表的是:如果我们假设 H 指数是h = n - mid
,那么我们需要n - mid
篇论文的引用次数至少为n - mid
。- 由于数组已排序,从下标
mid
到n-1
的这n - mid
篇论文是引用次数最高的。 citations[mid]
是这n - mid
篇论文中引用次数最少的那一篇。- 因此,判断条件
citations[mid] >= n - mid
意味着:以mid
为起点的这n - mid
篇论文,它们的引用次数都至少为n - mid
。这表明h = n - mid
是一个可能的 H 指数。
-
指针移动:
- 如果
citations[mid] >= n - mid
:说明当前的mid
对应的h = n - mid
是一个潜在的 H 指数。但是,我们想找最大 的h
。更大的h
对应着更小 的数组下标。所以,我们尝试在左半部分[left, mid - 1]
继续搜索,看看能否找到一个更小的mid'
使得citations[mid'] >= n - mid'
成立(这意味着更大的h
)。因此,right = mid - 1
。 - 如果
citations[mid] < n - mid
:说明当前的mid
对应的h = n - mid
太大了,连引用次数最少的citations[mid]
都达不到n - mid
次。我们需要减小h
,也就是增大mid
。所以,在右半部分[mid + 1, right]
搜索。因此,left = mid + 1
。
- 如果
-
结果:
- 循环结束时 (
left > right
),left
指向的是第一个不满足citations[mid] >= n - mid
条件的mid
的下一个位置(或者说是第一个满足citations[mid] < n - mid
的位置,如果我们是从左往右看的话)。 - 根据 H 指数的定义,满足
citations[i] >= n - i
的下标i
越小,对应的h = n - i
就越大。 left
是第一个使得h = n - left
不再满足条件(或刚好满足条件的边界的下一个)的下标。- 那么,最大的满足条件的
h
就是n - left
。因为从left
到n-1
一共有n - left
个元素,而left
是使得citations[left] >= n - left
潜在成立的最小下标(或者说,left-1
是最后一个明确满足citations[right] >= n - right
的right
值之后的位置)。因此,有n - left
篇论文(下标从left
到n-1
)的引用次数大于等于n - left
。
- 循环结束时 (
复杂度
- 时间复杂度 : O ( log n ) O(\log n) O(logn), 因为使用了二分查找。
- 空间复杂度 : O ( 1 ) O(1) O(1), 只使用了常数级别的额外空间。
Code
java
class Solution {
public int hIndex(int[] citations) {
int n = citations.length;
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (citations[mid] >= n - mid) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return n - left;
}
}
2226. 每个小孩最多能分到多少糖果
问题描述

思路:二分查找
这个问题的核心在于找到一个最大 的糖果数 x
,使得我们可以从 candies
数组中分割出至少 k
堆,每堆都包含 x
个糖果。
我们可以观察到这个问题的答案具有单调性:
- 如果我们能让每个小孩分到
x
个糖果,那么我们肯定也能让每个小孩分到x-1
个糖果。 - 如果我们无法让每个小孩分到
x
个糖果,那么我们肯定也无法让每个小孩分到x+1
个糖果。
这种单调性非常适合使用二分查找 来解决。我们可以在可能的糖果数范围 [1, max_candies]
内进行二分查找。
解题过程:二分答案
-
确定搜索范围:
- 每个小孩至少分到 1 个糖果(如果可能的话),所以下界
left = 1
。 - 每个小孩最多能分到的糖果数不会超过
candies
数组中的最大值max(candies[i])
,也不会超过总糖果数除以小孩数sum(candies) / k
。所以上界right
可以取min(max(candies[i]), sum(candies) / k)
。需要注意,如果sum < k
,则一个都分不了,应返回 0。我们在计算right
时处理这种情况。 - 注意:糖果总数
sum
和小孩数k
可能很大,需要使用long
类型。
- 每个小孩至少分到 1 个糖果(如果可能的话),所以下界
-
check(mid)
函数 :我们需要一个辅助函数check(candies, mid, k)
来判断:如果每个小孩分mid
个糖果,是否能够满足k
个小孩的需求。- 遍历
candies
数组中的每一堆糖果c
。 - 对于每一堆
c
,它可以分出c / mid
份,每份包含mid
个糖果。 - 计算所有糖果堆能分出的总份数
count = sum(c / mid)
。 - 判断逻辑 :如果
count >= k
,说明分mid
个糖果是可行 的。如果count < k
,说明分mid
个糖果是不可行的。
- 遍历
-
二分查找逻辑:
- 计算中间值
mid = left + (right - left) / 2
。 - 调用
check(candies, mid, k)
。 - 指针移动 :
- 如果
check
返回true
(即count >= k
),说明每个小孩分mid
个糖果是可行 的。我们希望找到最大 的可行值,所以我们尝试增大mid
,在右半部分[mid + 1, right]
继续搜索,并记录mid
作为一个可能的答案。ans = mid; left = mid + 1;
- 如果
check
返回false
(即count < k
),说明每个小孩分mid
个糖果是不可行 的,mid
太大了。我们需要减小mid
,在左半部分[left, mid - 1]
搜索。right = mid - 1;
- 如果
- 计算中间值
-
结果:
-
我们可以维护一个变量
ans
来记录最后一次check
成功的mid
值。当循环结束时,ans
就是最大可行解。 -
循环结束后,
right
指向的就是最大的可行解。为什么?因为right
最终停在最后一个使得check
成功的mid
上(或者是mid-1
移动过来的)。当left
超过right
时,left
指向第一个使得check
失败的值,而right
指向最后一个使得check
成功的值。 -
代码细节 :下面代码中的
check
函数返回的是count < k
(即是否不可行)。if (check(candies, mid, k))
为true
(即count < k
,不可行),则right = mid - 1
(需要减小糖果数)。if (check(candies, mid, k))
为false
(即count >= k
,可行),则left = mid + 1
(尝试增加糖果数)。- 循环结束后,
left
是第一个使得count < k
的mid
值,right
是最后一个使得count >= k
的mid
值。因此返回right
。
-
复杂度
- 时间复杂度 : O ( N log M ) O(N \log M) O(NlogM), 其中 N 是糖果堆的数量 (
candies.length
),M 是糖果数的最大可能范围(即right
的初始值)。每次check
需要 O ( N ) O(N) O(N) 时间,二分查找进行 O ( log M ) O(\log M) O(logM) 次。 - 空间复杂度 : O ( 1 ) O(1) O(1), 只使用了常数级别的额外空间。
Code
java
class Solution {
public int maximumCandies(int[] candies, long k) {
long sum = 0;
int max = 0;
for (int i = 0; i < candies.length; i++) {
sum += candies[i];
max = Math.max(max, candies[i]);
}
int left = 1, right = (int)Math.min(max, sum / k);
if (right <= 0) {
return 0;
}
while (left <= right) {
int mid = left + (right - left) / 2;
if (check(candies, mid, k)) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return right;
}
private boolean check(int[] arr, int max, long k) {
long count = 0;
for (int i = 0; i < arr.length; i++) {
count += arr[i] / max;
}
return count < k;
}
}