今天分享两道 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;
}
}