Q48-code88- 合并两个有序数组 + Q49- code692- 前K个高频单词

73 阅读4分钟

Q48-code88- 合并两个有序数组

实现思路

1 方法1: 双指针后插法

  • 利用数组有序特性,使用指针 后插法

参考文档

01- 直接参考文档

代码实现

1 方法1: 双指针后插法

  • 时间复杂度:O(m + n)
  • 空间复杂度:O(1)
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
  let p1 = m - 1, p2 = n - 1, p3 = m + n - 1;
  // 双指针 + 后插入法
  while (p1 >= 0 && p2 >= 0) {
    if (nums1[p1] >= nums2[p2]) nums1[p3--] = nums1[p1--];
    else nums1[p3--] = nums2[p2--];
  }
  // 防止此时nums1/ nums2 还有未处理元素
  while (p1 >= 0) nums1[p3--] = nums1[p1--];
  while (p2 >= 0) nums1[p3--] = nums2[p2--];
}

Q49- code692- 前K个高频单词

实现思路

1 方法1: Map + 最小堆

  • 易错点1:要在构建和维护堆的过程中,就考虑到 相同频率的字典序
  • 易错点2:需要先判断堆大小,再判断是否需要替换
  • 易错点3:需要先按词频排序,再按字典序排序

2 方法2: Map + 快速选择

  • 易错点1:需要在内部排序时,就考虑到字典序情况
  • 易错点2:findK只能保证前K个元素位置正确,但内部顺序不保证
  • 理解compare 的固定返回含义 + 返回实现的灵活性

参考文档

01- 直接参考文档

代码实现

1 方法1: Map + 最小堆

  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(n)
function topKFrequent(words: string[], k: number): string[] {
  // S1: 统计词频
  const record = words.reduce((map, word) => {
    const item = map.get(word);
    map.set(word, { word, fre: (item?.fre ?? 0) + 1 });
    return map;
  }, new Map<string, { word: string; fre: number }>());

  // S2: 构建最小堆
  // 易错点1: 需要在构建和维护堆的过程中,就考虑到 相同频率的字典序
  // 否则最后留在堆里的,可能不是 字典序高的那个同频词
  type MapValue<T> = T extends Map<any, infer V> ? V : never;
  type TItem = MapValue<typeof record>;
  const heapCompare = (a: TItem, b: TItem) =>
    a.fre !== b.fre ? a.fre < b.fre : a.word > b.word;

  const heap = minHeap<TItem>(heapCompare);
  // S3: 维持堆大小为k,通过最小堆保证留下的都是 高频词
  // 易错点2.1: 只有在满足特定条件下,才能入堆
  // 易错点2.2: 需要先判断堆大小,再判断是否需要替换
  [...record.values()].forEach((item) => {
    if (heap.getSize() < k) {
      heap.add(item);
    } else if (heapCompare(heap.peek(), item)) {
      heap.extract();
      heap.add(item);
    }
  });

  // S4: 返回结果
  // 易错点3:需要先按词频排序,再按字典序排序
  return heap
    .toArr()
    .sort((a, b) => b.fre - a.fre || a.word.localeCompare(b.word))
    .map(({ word }) => word);
}

function minHeap<T>(compare: (a: T, b: T) => boolean) {
  const heap: T[] = [];
  return {
    getSize: () => heap.length,
    isEmpty: () => heap.length === 0,
    peek: () => heap[0],
    toArr: () => heap,
    add: (item: T) => {
      heap.push(item);
      siftUp(heap.length - 1);
    },
    extract: (): T => {
      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表示:a < b,此时cur < 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, rdx = ldx + 1;
      let ndx = idx;
      // compare表示:a < b,此时child值 < cur
      if (ldx < heap.length && compare(heap[ldx], heap[ndx])) ndx = ldx;
      if (rdx < heap.length && compare(heap[rdx], heap[ndx])) ndx = rdx;
      if (ndx === idx) break;
      swap(idx, ndx);
      idx = ndx;
    }
  }

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

2 方法2: Map + 快速选择

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
function topKFrequent(words: string[], k: number): string[] {
  // S1- 统计词频
  const records = words.reduce((map, word) => {
    map.set(word, (map.get(word) ?? 0) + 1);
    return map;
  }, new Map<string, number>());

  // S2- findK: 找到前K个元素
  const arr = [...records.entries()];
  findK(arr, 0, arr.length - 1, k - 1);

  // S3- 返回结果
  return (
    arr
      .slice(0, k)
      // 易错点2:findK只能保证前K个元素位置正确,但内部顺序不保证
      .sort(compare)
      .map(([word]) => word)
  );
}

function findK(arr: [string, number][], l: number, r: number, tdx: number) {
  if (l === r) return;
  const p = partition(arr, l, r);
  if (p === tdx) return;
  if (p < tdx) findK(arr, p + 1, r, tdx);
  if (p > tdx) findK(arr, l, p - 1, tdx);
}

function partition(arr: [string, 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) {
    // 易错点1:需要在内部排序时,就考虑到字典序情况
    while (i <= j && compare(arr[i], x) < 0) i++;
    while (i <= j && compare(arr[j], x) > 0) j--;
    if (i >= j) break;
    swap(arr, i++, j--);
  }
  swap(arr, l, j);
  return j;
}

// 新增:统一的比较函数: 先按词频降序,再按字典序升序
function compare(a: [string, number], b: [string, number]): number {
  if (a[1] !== b[1]) return b[1] - a[1];
  return a[0].localeCompare(b[0]);
}

function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}

3 方法3:桶排序

  • 时间复杂度:O(n + wlogw),其中w是同频率单词的最大数量
  • 空间复杂度:O(n)
function topKFrequent(words: string[], k: number): string[] {
  // S1: 统计词频
  const record = new Map<string, number>();
  words.forEach((word) => record.set(word, (record.get(word) ?? 0) + 1));
  
  // S2: 创建桶 - 索引是频率,值是该频率的 单词数组
  // 易错点:由于buckets索引为0(feq = 0空置),所以长度需要 words.length + 1
  const buckets: string[][] = Array.from(
    { length: words.length + 1 },
    () => []
  );
  // S3: 将单词放入对应频率的桶中
  record.forEach((fre, word) => buckets[fre].push(word));

  // S4: 从高频率到低频率收集结果
  const res: string[] = [];
  // i 表示 freq
  for (let i = buckets.length - 1; i >= 0 && res.length < k; i--) {
    const words = buckets[i].sort().slice(0, k - res.length);
    res.push(...words);
  }
  return res;
}