Q63- code540- 有序数组中的单一元素 + Q64- code644- 子数组最大平均数 II

41 阅读5分钟

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]

参考文档

01- 方法1参考文档

代码实现

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

    1. 把原数组每个数减去x
    1. 在新数组中寻找一个长度≥k的子数组,其和>0
    1. 如果找到了 => x还可以更大
    1. 如果找不到 => 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)

参考文档

01- 方法1参考文档

代码实现

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;
}