Q46- code264- 丑数 II && Q47- code1086- 前五科的均分

53 阅读6分钟

Q46- code264- 丑数 II

实现思路

1 方法1:暴力解法:知道暴力解法,才能有更好的优化解法

  • 暴力解法:从1开始,不断判断一个数是否是丑数,直到找到第n个丑数

  • 判断一个数是否是丑数:

    • 不断除以2直到不能整除,相当于 “不断剥离2”
    • 不断除以3直到不能整除,相当于 “不断剥离3”
    • 不断除以5直到不能整除,相当于 “不断剥离5”
    • 如果最后剩下1,说明这个数只包含2,3,5这些质因子
  • 时间复杂度:O(n * log(m)),其中m是第n个丑数

  • 空间复杂度:O(1)

  • 暴力解法的缺点:

    • 需要遍历处理 很多非丑数
    • 对于每个数字都要进行多次除法运算 来判断是否为丑数

2 方法2:最小堆:

  • 从最小丑数1开始,每次生成新的丑数时,将当前丑数分别乘以2、3、5,把得到的新丑数加入最小堆

  • 每次从堆顶取出最小的丑数,这就保证了我们按照顺序生成丑数

  • 使用一个集合去重,避免重复的丑数进入堆

  • 时间复杂度:O(n * log(n)),其中n是第n个丑数

  • 空间复杂度:O(n)

  • 最小堆的优点:

    • 直接生成丑数:不会额外处理很多非丑数
    • 避免重复计算:不需要 对每个数都进行多次除法运算,来判断是否为丑数

3 方法3:三指针

  • 用uglys有序数组来存储丑数,初始只有1

  • 在2、3、5 票价窗口分别设置指针,其指针指向的是之前的最小丑数

  • 每轮根据 更新窗口的最小值 和 非更新窗口的最小值,来入队本轮的最小丑数

  • 由于依次入队的都是 每轮的最小丑数

    • 所以 各个窗口在更新指针后,指向的必然是之前入队的所有丑数,不会有遗漏
    • 即 可以理解为 是一种 ”渐进式的 穷举遍历“,而不是和堆一样一次性入队
    • 以上即 保证了 ”有序性“ + ”无遗漏“
  • 为了防止有重复丑数,可以使用一个 集合/同时更新窗口指针 来去重 2种方法实现

  • 时间复杂度:O(n)

  • 空间复杂度:O(n)

参考文档

01- 暴力 + 最小堆 参考文档

02.1- 三指针的图示

02.2- 三指针解法答疑

代码实现

1 方法1: 最小堆/优先队列

  • 时间复杂度:O(n * log(n)),其中n是第n个丑数
  • 空间复杂度:O(n)
function nthUglyNumber(n: number): number {
  if (n <= 0) return 0;
  if (n === 1) return 1;
  const saved = new Set<number>();
  const heap = minHeap<number>((a, b) => a < b);
  heap.add(1);
  while (heap.size() && n > 0) {
    const cur = heap.removeTop();
    if (--n === 0) return cur;
    const nums = [cur * 2, cur * 3, cur * 5].filter((num) => !saved.has(num));
    nums.forEach((num) => {
      heap.add(num);
      saved.add(num);
    });
  }
}

function minHeap<T>(compare: (a: T, b: T) => boolean) {
  const heap: T[] = [];

  return {
    size: () => heap.length,
    peek: () => heap[0],
    add: (item: T) => {
      heap.push(item);
      siftUp(heap.length - 1);
    },
    removeTop: (): T => {
      const ret = heap[0];
      swap(0, heap.length - 1);
      heap.pop();
      siftDown(0);
      return ret;
    },
  };

  function swap(i: number, j: number) {
    [heap[i], heap[j]] = [heap[j], heap[i]];
  }

  function siftUp(idx: number) {
    while (idx > 0) {
      const parentIdx = ~~((idx - 1) / 2);
      // compare返回true: 说明需要上浮,即 当前子值 < 父值
      const willUp = compare(heap[idx], heap[parentIdx]);
      if (!willUp) break;
      swap(idx, parentIdx);
      idx = parentIdx;
    }
  }

  function siftDown(idx: number) {
    while (1) {
      const ldx = 2 * idx + 1, rdx = 2 * idx + 2;
      let smller = idx;
      // ldx < idx
      if (ldx < heap.length && compare(heap[ldx], heap[smller])) {
        smller = ldx;
      }
      // rdx < idx
      if (rdx < heap.length && compare(heap[rdx], heap[smller])) {
        smller = rdx;
      }
      if (smller === idx) break;
      swap(smller, idx);
      idx = smller;
    }
  }
}

2 方法2: 三指针/DP法

  • 时间复杂度:O(n),其中n是传入的值
  • 空间复杂度:O(n)
function nthUglyNumber(n: number): number {
  if (n === 1) return 1;
  const uglys = [1];
  let i2 = 0, i3 = 0, i5 = 0;
  for (let i = 1; i < n; i++) {
    const next2 = uglys[i2] * 2;
    const next3 = uglys[i3] * 3;
    const next5 = uglys[i5] * 5;
    const min = Math.min(next2, next3, next5);
    uglys.push(min);
    if (min === next2) i2++;
    if (min === next3) i3++;
    if (min === next5) i5++;
  }
  return uglys[n - 1];
}

Q47- code1086- 前五科的均分

实现思路

1 方法1: 快速选择

  • 明确快速选择 和 快速排序的 区别

2 方法2: 最小堆

  • 明确题意转化

参考文档

01- 译文参考

代码实现

1 方法1: 快速选择

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
function highFive(items: number[][]): number[][] {
  // S1 获取id: [score1, score2...] 映射关系
  const record = new Map<number, number[]>();
  items.forEach(([id, score]) =>
    record.set(id, [...(record.get(id) || []), score])
  );

  // S2 获取每个id对应的前5高分数的 平均数
  return [...record.entries()]
    .map(([id, scores]) => {
      // 保证每个id成绩数组的 前5个元素是第5大的,内部顺序不保证,即topK
      findTop5(scores, 0, scores.length - 1, 4);
      // 获取其前5高分数的 平均数
      return [id, ~~(scores.slice(0, 5).reduce((a, b) => a + b) / 5)];
    })
    .sort(([id1], [id2]) => id1 - id2);
}

function findTop5(arr: number[], l: number, r: number, tdx: number) {
  if (l >= r) return;
  const p = partition(arr, l, r); // 修复函数名拼写
  if (p === tdx) return;
  if (p < tdx) findTop5(arr, p + 1, r, tdx);
  if (p > tdx) findTop5(arr, l, p - 1, tdx);
}

function partition(arr: number[], l: number, r: number): number {
  const rdx = ~~(Math.random() * (r - l + 1)) + l;
  swap(arr, l, rdx);
  const x = arr[l];
  // 保证 [l, i) 都 >= x;  (j, r]都 <=x
  let i = l + 1,
    j = r;
  while (1) {
    while (i <= j && arr[i] > x) i++;
    while (i <= j && arr[j] < x) j--;
    if (i >= j) break;
    swap(arr, i++, j--);
  }
  swap(arr, l, j);
  return j;
}
function swap(arr: number[], i: number, j: number) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}

2 方法2:最大堆

  • 时间复杂度:O(nlog5)
  • 空间复杂度:O(n)
function highFive(items: number[][]): number[][] {
  // S1 创建id和scores映射关系
  const record = items.reduce((map, [id, score]) => {
    map.set(id, [...(map.get(id) ?? []), score]);
    return map;
  }, new Map<number, number[]>());

  // S2 使用大小为5的最小堆获取最大的5个分数
  return [...record.entries()]
    .map(([id, scores]) => {
      const heap = minHeap<number>((a, b) => a < b);
      // 维护大小为5的最小堆,使用链式调用
      scores.forEach((score) =>
        heap.size() < 5
          ? heap.add(score)
          : score > heap.peek() && (heap.remove(), heap.add(score))
      );

      // 计算平均值,使用Array.from
      const sum = Array.from({ length: 5 }, heap.remove).reduce(
        (a, b) => a + b,
        0
      );
      return [id, ~~(sum / 5)];
    })
    .sort((a, b) => a[0] - b[0]);
}

function minHeap<T>(compare: (a: T, b: T) => boolean) {
  const heap: Array<T> = [];
  return {
    size: () => heap.length,
    empty: () => heap.length === 0,
    // 查看堆顶元素
    peek: () => heap[0],
    // 在末尾新增
    add: (item: T) => {
      heap.push(item);
      siftUp(heap.length - 1);
    },
    // 去除堆顶元素
    remove: (): T => {
      if (heap.length === 0) return;
      const ret = heap[0];
      swap(0, heap.length - 1);
      heap.pop();
      siftDown(0);
      return ret;
    },
  };

  function siftUp(idx: number) {
    while (idx > 0) {
      const pdx = ~~((idx - 1) / 2);
      // compare为true,表示a < b,即 当前元素 < parent值
      const willUp = compare(heap[idx], heap[pdx]);
      if (!willUp) break;
      swap(idx, pdx);
      idx = pdx;
    }
  }

  function siftDown(idx: number) {
    while (1) {
      const ldx = idx * 2 + 1;
      const rdx = idx * 2 + 2;
      let next = idx;
      // 如果左子元素较小,则 可能需要让当前元素 下沉
      if (ldx < heap.length && compare(heap[ldx], heap[next])) next = ldx;
      // 如果右子元素较小,则 可能需要让当前元素 下沉
      if (rdx < heap.length && compare(heap[rdx], heap[next])) next = rdx;
      if (next === idx) break;
      swap(idx, next);
      idx = next;
    }
  }

  function swap(i: number, j: number) {
    [heap[i], heap[j]] = [heap[j], heap[i]];
  }
}