记录今天解决的两道 LeetCode 算法题,主要涉及二分查找的应用。
1283. 使结果不超过阈值的最小除数
题目描述

思路
核心思路是 二分查找。
解题过程
-  
为什么可以使用二分?
关键在于单调性。对于一个固定的数组nums,当除数divisor增大时,每个元素num / divisor(向上取整) 的值是 非递增 的,因此它们的总和也是 非递增 的。
我们要求的是满足sum <= threshold的 最小divisor。- 如果当前除数 
mid计算出的总和sum大于threshold,说明mid太小了,我们需要增大除数。因此,真正的答案一定在[mid + 1, right]区间。 - 如果当前除数 
mid计算出的总和sum小于等于threshold,说明mid是一个可能的解,但可能存在更小的除数也满足条件。因此,我们尝试在[left, mid - 1]区间寻找更小的解,同时记录mid作为潜在答案。 
 - 如果当前除数 
 -  
二分查找的目标?
我们要查找的是满足条件的 最小 正整数除数。 -  
查找范围?
left:最小可能的除数是 1(题目要求正整数除数)。right:最大可能的除数是多少?如果除数非常大(例如,等于数组中的最大元素),那么每个数除后的结果向上取整至少是 1,总和至少是nums.length。如果除数是数组中的最大值max(nums),那么每个ceil(num / max(nums))都是 1,总和为nums.length。因此,一个合理的上界是数组中的最大元素值。如果阈值threshold非常小(比如等于nums.length),那么除数可能需要等于最大元素值。
 -  
check函数:
需要一个辅助函数check(nums, divisor)来计算在当前除数divisor下,数组元素的除法结果(向上取整)之和。计算ceil(x / divisor)可以用(x + divisor - 1) / divisor的整数除法实现,或者像原始代码那样判断余数。 
复杂度
- 时间复杂度: O(N log M) 
- 其中 N 是数组 
nums的长度。 - M 是二分查找的范围上限,即 
nums中的最大元素值。 - 每次 
check函数需要 O(N) 的时间遍历数组。 - 二分查找需要 O(log M) 次 
check调用。 
 - 其中 N 是数组 
 - 空间复杂度: O(1) 
- 我们只需要常数级别的额外空间来存储 
left,right,mid和sum等变量。 
 - 我们只需要常数级别的额外空间来存储 
 
Code
class Solution {public int smallestDivisor(int[] nums, int threshold) {Arrays.sort(nums);int left = 1, right = nums[nums.length - 1];while (left <= right) {int mid = left + (right - left) / 2;if (check(nums, mid) < threshold + 1) {right = mid - 1;} else {left = mid + 1;}}return left;}private int check(int[] nums, int divisor) {int sum = 0;for (int x : nums) {if (x % divisor == 0) {sum += x / divisor;} else {sum += x / divisor + 1;}}return sum;}
}
 
1170. 比较字符串最小字母出现频次
题目描述

思路
结合 预处理 和 二分查找。
解题过程
-  
计算频率
f(s):
首先,需要实现一个函数f(s),用于计算给定字符串s中字典序最小的字符的出现次数。遍历字符串,找到最小字符,并统计其出现次数。 -  
预处理
words:
对words数组中的每个字符串W,计算其f(W),并将这些频率值存储在一个新的整数数组wFreq中。 -  
排序
wFreq:
为了能够高效地查找满足f(queries[i]) < f(W)的W的数量,我们将wFreq数组进行升序排序。 -  
二分查找:
对于每个queries[i]:
a. 计算qVal = f(queries[i])。
b. 我们需要在已排序的wFreq数组中找到 第一个 大于qVal的元素的位置idx。
c.wFreq数组中从idx到末尾的所有元素都满足f(W) > qVal。因此,满足条件的W的数量就是wFreq.length - idx。
d. 查找 “第一个大于qVal的元素” 可以通过二分查找实现。具体地,我们可以查找 第一个大于等于qVal + 1的元素的位置。 
复杂度
- 时间复杂度: O(Lq * N + Lw * M + M log M + N log M) 
- 其中 N 是 
queries的长度,M 是words的长度。 - Lq 和 Lw 分别是 
queries和words中字符串的最大长度。 - 计算所有 
f(queries[i])需要 O(Lq * N)。 - 计算所有 
f(W)需要 O(Lw * M)。 - 对 
wFreq排序需要 O(M log M)。 - 对每个 
query进行二分查找需要 O(log M),总共 N 次查找为 O(N log M)。 - 整体复杂度由这些部分相加决定。如果字符串长度较小,主要由排序和查找决定。
 
 - 其中 N 是 
 - 空间复杂度: O(N + M) 
- 需要 O(N) 空间存储 
queries的频率结果(或者直接在结果数组中计算)。 - 需要 O(M) 空间存储 
words的频率数组wFreq。 - 排序可能需要 O(log M) 或 O(M) 的额外栈空间或临时空间,但 O(N+M) 通常是主导。
 
 - 需要 O(N) 空间存储 
 
Code
import java.util.Arrays;class Solution {public int[] numSmallerByFrequency(String[] queries, String[] words) {int n = queries.length, m = words.length;int[] qFreq = new int[n]; // 存储 queries 的 f 值int[] wFreq = new int[m]; // 存储 words 的 f 值int[] ans = new int[n];   // 存储最终结果// 1. 计算 queries 中每个字符串的 f 值for (int i = 0; i < n; i++) {qFreq[i] = calculateF(queries[i]);}// 2. 计算 words 中每个字符串的 f 值for (int i = 0; i < m; i++) {wFreq[i] = calculateF(words[i]);}// 3. 对 words 的频率数组进行排序Arrays.sort(wFreq);// 4. 对每个 query 的频率值,在排好序的 wFreq 中进行二分查找for (int i = 0; i < n; i++) {int targetFreq = qFreq[i];// 查找第一个严格大于 targetFreq 的元素的位置// 等价于查找第一个大于等于 targetFreq + 1 的元素的位置int index = findFirstGreater(wFreq, targetFreq);ans[i] = m - index; // 从该位置到数组末尾的元素个数即为所求}return ans;}// 计算字符串 s 的 f(s) 值private int calculateF(String s) {if (s == null || s.isEmpty()) {return 0;}char minChar = 'z' + 1; // 初始化为一个比 'z' 大的字符int count = 0;for (char c : s.toCharArray()) {if (c < minChar) {minChar = c;count = 1; // 找到了更小的字符,重置计数} else if (c == minChar) {count++; // 遇到了相同的最小字符,增加计数}}return count;}// 在升序数组 arr 中查找第一个严格大于 target 的元素的索引// 如果所有元素都小于等于 target,返回 arr.lengthprivate int findFirstGreater(int[] arr, int target) {int left = 0, right = arr.length - 1;int resultIndex = arr.length; // 默认为数组长度,表示没找到while (left <= right) {int mid = left + (right - left) / 2;if (arr[mid] > target) {// arr[mid] 比 target 大,可能是第一个,也可能前面还有resultIndex = mid; // 记录当前这个可能的位置right = mid - 1;  // 继续向左查找更小的索引} else {// arr[mid] 小于等于 target,需要向右查找更大的值left = mid + 1;}}return resultIndex;}
}