code540- 有序数组中的单一元素
实现思路
1 分组错位 二分查找
1.1 以m个元素为1组,对其进行分组,获取其分组的l 和 r 边界值
1.2 分组后的元素 具有以下可二分的性质:
- 把 每分组的索引,记作 k
- 每组的 开头元素,其对应的nums的索引为 m * k
- 如果 nums[m * k] === nums[m * k + 1],则说明该组未受到单一元素的影响,则单一元素在右半部分
- 反之,则说明受到了单一元素的影响,则单一元素在 当前分组的左半部分
举例,以 m = 2为例 nums: 0, 0, 1, 1, 2, 3, 3 分组: (0 1) (2 3) (4) (5 6) 分组索引: 0 1 2 3
1.3 易错点:最后返回的值,应该是分组k 对应的i,即 nums[m * k]
参考文档
代码实现
1 方法1- 分组错位 二分查找
- 时间复杂度:O(logn/m)
- 空间复杂度:O(1)
function singleNonDuplicate(nums: number[]): number {
return findM(nums, 2)
}
// 在以m个元素进行重复的 有序数组nums里,找到唯一的 无重复的 元素
function findM(nums: number[], m: number): number {
let l = -1, r = ~~((nums.length + 1) / m)
while (l + 1 < r) {
const mid = (l + r) >> 1
if (nums[mid * m] !== nums[mid * m + 1]) {
r = mid
} else {
l = mid
}
}
return nums[r * m]
}
Q64- code644- 子数组最大平均数 II
实现思路
1 题目分析:找一个长度≥k的连续子数组,使得它的平均值最大
2 暴力解法:计算所有可能的子数组(长度≥4)的平均值
nums = [1, 12, -5, -6, 50, 3], k = 4
长度为4的子数组:
[1, 12, -5, -6] => (1 + 12 - 5 - 6) / 4 = 0.5
[12, -5, -6, 50] => (12 - 5 - 6 + 50) / 4 = 12.75
[-5, -6, 50, 3] => (-5 - 6 + 50 + 3) / 4 = 10.5
长度为5的子数组:
[1, 12, -5, -6, 50] => (1 + 12 - 5 - 6 + 50) / 5 = 10.4
...
...
这个暴力解法的问题:
- 要计算所有可能长度(k到n)的子数组
- 对每个子数组都要计算平均值
- 最后找出最大的平均值
- 时间复杂度:O(n^2 * k)
- 空间复杂度:O(1)
3.1 题意转化:
- 假设最大平均值是 maxAvg
- 那么一定存在一个子数组,它的平均值 = maxAvg
- 而其他所有子数组的平均值 ≤ maxAvg
3.2 我们可以假设一个 当前的平均数值为 x
- 设子数组 [a1, a2, ..., am],长度为m,如果其平均值 > x,则有
- (a1 + a2 + ... + am) / m > x ==>
- a1 + a2 + ... + am > m * x ==>
- a1 + a2 + ... + am - m * x > 0 ==>
- a1 + a2 + ... + am - (x + x ... + x) > 0 ==>
- (a1 - x) + (a2 - x) + ... + (am - x) > 0
3.3 也就是说:
- 如果一个子数组的平均值 > x,则有 (a1 - x) + (a2 - x) + ... + (am - x) > 0
- 即 如果我们把原数组的 每个数都 减去x,然后求和
- 如果和 > 0,则说明这个子数组的平均值 > x
3.4 由此得到新的判断方法: 对于任意猜测值x
-
- 把原数组每个数减去x
-
- 在新数组中寻找一个长度≥k的子数组,其和>0
-
- 如果找到了 => x还可以更大
-
- 如果找不到 => x要更小
4.1 为什么可以用二分查找
- 因为最大平均值 一定在数组的最小值和最大值之间
- 所以我们可以用二分查找,不断猜测和 逼近这个最大平均值
4.2 实现技巧
- 用前缀和计算每个子数组的和
- 用一个变量min 记录之前所有可能起始位置的 最小前缀和
- 为什么需要min:因为我们在找一个区间和最大的子数组
- 区间和 = 右端点前缀和 - 左端点前缀和
- 如果我们要找一个最大的区间和(大于0的区间和),就需要
- 右端点的前缀和要大(这个是固定的,就是sums[i])
- 左端点的前缀和要小(这就是为什么我们要维护min)
时间复杂度:O(n * log(max-min))
- 二分查找部分:log(max-min)次迭代
- 每次迭代时,需要计算前缀和:O(n)
- 总时间复杂度:O(n * log(max-min))
空间复杂度:O(n)
- 需要额外的前缀和数组:O(n)
参考文档
代码实现
1 方法1- 分组错位 二分查找
- 时间复杂度:O(n * log(max-min))
- 空间复杂度:O(n)
function findMaxAverage(nums: number[], k: number): number {
// 可能的平均值的 最小值和最大值
let l = Math.min(...nums) - 1, r = Math.max(...nums) + 1;
// 精度需要保证在 10^-5 以内
while (r - l > 1e-5) {
const mid = (l + r) / 2;
if (check(nums, k, mid)) l = mid;
else r = mid;
}
// 或者返回r,因为此时l和r非常接近
return r;
}
function check(nums: number[], k: number, x: number): boolean {
const n = nums.length;
const sums = new Array(n + 1).fill(0);
// 前缀和: 计算每个子数组的和, x为 此次猜测的平均值
for (let i = 0; i < n; i++) {
sums[i + 1] = sums[i] + (nums[i] - x);
}
// min:从开始到当前位置i之前,所有可能作为起点位置的 前缀和的最小值
let min = 0;
for (let i = k; i <= n; i++) {
// sums[i] - min >= 0 表示找到了一个平均值大于x的子数组
if (sums[i] - min >= 0) return true;
// 更新min,从而找到左端点的最小值,从而让 区间和尽可能大
// 要找:sums[i] - sums[j] > 0
// 其中j的范围是:[0, i-k+1](保证长度≥k)
// 等价于:sums[i] - min(sums[0...i-k+1]) > 0
// 这就是为什么我们需要维护min
min = Math.min(min, sums[i - k + 1]);
}
return false;
}