Q43- code973- 最接近原点的 K 个点 + Q44- code347- 前 K 个高频元素

Q43- code973- 最接近原点的 K 个点

实现思路

1 方法1:快排partition- 思考路径:

S1 问题转化:找最近的k个点 → 找距离第k小的点==> TopK问题

S2 快排/最大堆(优先队列) 都可以解决 TopK问题

S3.1 快排本质 && 优化点

  • 排序所有元素:对所有元素排序,但我们只需要前k个
  • 快速选择:只关心第k小的位置,其他元素不需要完全排序
  • 快排的本质:分治法,将问题分解为子问题,然后递归处理子问题
  • 每次partition 都能排除一半的元素

S3.2 快排实现步骤

  • 随机选择一个基准点p
  • 将小于基准点p 的放左边,大于基准点p的 放右边
  • 如果p的位置正好是k,就找到了
  • 如果p的位置小于k,就递归处理右边的点
  • 如果p的位置大于k,就递归处理左边的点

2 方法2: 最大堆实现 时间复杂度: O(nlogk); 空间复杂度: O(k)

S1 通过 最大堆/优先队列 存储K个元素

S2 维护 最大堆的性质,返回最后结果

参考文档

01- 直接参考实现

代码实现

1 方法1: 快排partition 时间复杂度: O(n); 空间复杂度 O(logn)

ts 复制代码
function kClosest(points: number[][], k: number): number[][] {
  let pointsWithDis = points.map(([x, y]) => ({
    point: [x, y],
    dis: x * x + y * y,
  }));
  // 第k小个数,对应的下标是 k-1
  quickSel(pointsWithDis, 0, points.length - 1, k - 1);
  return pointsWithDis.slice(0, k).map(({ point }) => point); 
};

function quickSel(arr, l, r, k) {
  if (l >= r) return;
  const p = partition(arr, l, r);
  if (p === k) return;
  if (p < k) quickSel(arr, p + 1, r, k);
  if (p > k) quickSel(arr, l, p - 1, k);
}

function partition(arr, l, r) {
  // [0,1) ==> [0, r+1) ==> [0, r]==> [l, r + l]
  // const rdx = l + Math.floor(Math.random() * (r + 1));
  // 所以正确的思考步骤应该是: [0, r-l+1) ==> [0, r-l]==>  [l, r]
  const rdx = l + ~~(Math.random() * (r - l + 1));
  swap(arr, l, rdx);
  const x = arr[l].dis;
  // 保证性质[l, i) <= x; (j, r] >= x
  let i = l + 1, j = r;
  while (1) {
    while (i <= j && arr[i].dis < x) i++;
    while (i <= j && arr[j].dis > x) j--;
    if (i > j) break;
    swap(arr, i++, j--);
  }
  // 运行到此,此时必然满足[l, i-1] <= x && [i, r] >= x
  // 所以把x放到 i-1位置 + 返回i-1 即可,即 --i
  swap(arr, l, --i);
  return i;
}

function swap(arr, l, r) {
  [arr[l], arr[r]] = [arr[r], arr[l]];
}

2 方法2: 最大堆实现 时间复杂度: O(nlogk); 空间复杂度: O(k)

ts 复制代码
type TPoint = ReturnType<typeof createPointsWithDis>[number];
type THeap<T> = ReturnType<typeof createMaxHeap<T>>;

function kClosest(points: number[][], k: number): number[][] {
  // S1 创建带有距离的Point
  const pointsWithDis = createPointsWithDis(points);
  // S2 创建最大堆
  const heap: THeap<TPoint> = createMaxHeap<TPoint>((a, b) => a.dis - b.dis);
  // S3 在堆中处理点,并返回处理后的堆
  const processedHeap = pointsWithDis.reduce(processPoint(k), heap);
  // S4 返回堆中的点
  return processedHeap.getArr().map(({ point }) => point);
}

function createPointsWithDis(points: number[][]) {
  return points.map(([x, y]) => ({
    point: [x, y],
    dis: x * x + y * y,
  }));
}

function createMaxHeap<T>(compare: (a: T, b: T) => number) {
  const heap: T[] = [];
  return {
    getSize,
    peek,
    add,
    replace,
    getArr: () => heap,
  };

  function getSize() {
    return heap.length;
  }

  function peek() {
    return heap[0];
  }

  function add(item: T) {
    heap.push(item);
    siftUp(getSize() - 1);
  }

  function replace(item: T) {
    const max = peek();
    heap[0] = item;
    siftDown(0);
    return max;
  }

  function siftUp(idx: number) {
    //S1 循环更新:比较父节点与当前节点大小
    //S2 当 当前节点值 > 父节点值 时,则交换位置 + 更新当前index
    while (idx > 0) {
      const parentIdx = ~~((idx - 1) / 2);
      const curIsLarger = compare(heap[idx], heap[parentIdx]) > 0;
      if (!curIsLarger) break;
      swap(heap, idx, parentIdx);
      idx = parentIdx;
    }
  }

  function siftDown(idx: number) {
    // 上浮/下沉是一个 深度比较操作,所以需要循环更新
    while (1) {
      // S1 获取左右子节点中较大索引值
      const left = 2 * idx + 1;
      const right = 2 * idx + 2;
      let largerIdx = idx;

      if (left < getSize() && compare(heap[left], heap[largerIdx]) > 0) {
        largerIdx = left;
      }
      if (right < getSize() && compare(heap[right], heap[largerIdx]) > 0) {
        largerIdx = right;
      }
      // S2 比较子节点和当前节点值,子节点较小时 则停止,否则交换位置
      if (largerIdx === idx) break;
      swap(heap, idx, largerIdx);
      idx = largerIdx;
    }
  }
}

function swap(arr: any[], l: number, r: number) {
  [arr[l], arr[r]] = [arr[r], arr[l]];
}

function processPoint(k: number) {
  return (heap: THeap<TPoint>, point: TPoint) => {
    if (heap.getSize() < k) {
      heap.add(point);
    } else if (point.dis < heap.peek().dis) {
      heap.replace(point);
    }
    return heap;
  };
}

Q44- code347- 前 K 个高频元素

实现思路

1 方法1:快排实现

基本同 code973


2 方法2: 最大堆实现

基本同 code973


3 方法3 桶排序

1 统计每个元素出现的次数: Map 2 创建频率桶:

  • 索引为频率,值是该频率的 元素数组
  • 长度为 n + 1, 因为频率最大为 n && idx 从 0 开始

3 从后向前遍历频率桶,直到收集到 前k个高频元素时,返回结果数组

  • 使用 res.length < k 作为条件,确保只收集前k个高频元素

参考文档

01- 方法1和方法2参考实现

02- 方法3桶排序实现

代码实现

1 方法1: 快排实现 时间复杂度: O(n); 空间复杂度 O(logn)

ts 复制代码
function topKFrequent(nums: number[], k: number): number[] {
  const record = new Map();
  for (const num of nums) {
    record.set(num, (record.get(num) || 0) + 1);
  }
  const arr: [number, number][] = Array.from(record);
  quickSel(arr, 0, arr.length - 1, k - 1);
  // 易错点1: 要直接返回前k个元素
  return arr.slice(0, k).map((item) => item[0]);
}

function quickSel( arr: Array<[number, number]>, l: number, r: number, k: number): void {
  if (l >= r) return;
  const p = partition(arr, l, r);
  if (p === k) return;
  if (p < k) quickSel(arr, p + 1, r, k);
  if (p > k) quickSel(arr, l, p - 1, k);
}

function partition(arr: Array<[number, number]>, l: number, r: number): number {
  const rdx = ~~(Math.random() * (r - l + 1)) + l;
  swap(arr, l, rdx);
  // 易错点2:所有左边界都是l,而不是0
  // [l, i)>= x;  (j, r] <= x
  let x = arr[l][1], i = l + 1, j = r;
  while (1) {
    while (i <= j && arr[i][1] > x) i++;
    while (i <= j && arr[j][1] < x) j--;
    if (i > j) break;
    swap(arr, i++, j--);
  }
  swap(arr, l, --i);
  return i;
}

function swap(arr: Array<[number, number]>, l: number, r: number): void {
  [arr[l], arr[r]] = [arr[r], arr[l]];
}

2 方法2 最大堆 时间复杂度: O(nlogk); 空间复杂度: O(n)

ts 复制代码
type TArrWithFre = ReturnType<typeof createArrWithFre>[number];
type THeap = ReturnType<typeof createMaxHeap>;

function topKFrequent(nums: number[], k: number): number[] {
  const arrWithFre = createArrWithFre(nums);
  const heap = createMaxHeap<TArrWithFre>((a, b) => b.fre - a.fre);
  return arrWithFre.reduce(processArr(k), heap).getNums();
}

function createArrWithFre(arr: number[]) {
  const record = new Map<number, { num: number; fre: number }>();
  arr.map((val) => {
    record.set(val, {
      num: val,
      fre: (record.get(val)?.fre || 0) + 1,
    });
  });
  return [...record.values()];
}

function createMaxHeap<T extends TArrWithFre>(compare: (a: T, b: T) => number) {
  let heap: T[] = [];
  return {
    getSize,
    peek() {
      return heap[0];
    },
    add(item: T) {
      heap.push(item);
      siftUp(heap.length - 1);
    },
    replace(item: T) {
      const max = heap[0];
      heap[0] = item;
      siftDown(0);
      return max;
    },
    getNums() {
      return heap.map((item) => item.num);
    },
  };

  function getSize() {
    return heap.length;
  }

  function siftUp(idx: number) {
    //S1 循环更新:比较父节点与当前节点大小
    //S2 当 当前节点值 > 父节点值 时,则交换位置 + 更新当前index
    while (idx > 0) {
      const parentIdx = (idx - 1) >> 1;
      const curIsLarger = compare(heap[idx], heap[parentIdx]) > 0;
      if (!curIsLarger) break;
      swap(heap, idx, parentIdx);
      idx = parentIdx;
    }
  }

  function siftDown(idx: number) {
    while (1) {
      // 取左右子节点中最大的
      let maxIdx = idx;
      const leftIdx = idx * 2 + 1;
      const rightIdx = idx * 2 + 2;
      if (leftIdx < getSize() && compare(heap[leftIdx], heap[maxIdx]) > 0) {
        maxIdx = leftIdx;
      }
      if (rightIdx < getSize() && compare(heap[rightIdx], heap[maxIdx]) > 0) {
        maxIdx = rightIdx;
      }
      if (maxIdx === idx) break;
      swap(heap, idx, maxIdx);
      idx = maxIdx;
    }
  }

  function swap(arr: T[], l: number, r: number) {
    [arr[l], arr[r]] = [arr[r], arr[l]];
  }
}

function processArr(k: number) {
  return (heap: THeap, cur: TArrWithFre) => {
    if (heap.getSize() < k) {
      heap.add(cur);
    } else if (cur.fre > heap.peek().fre) {
      heap.replace(cur);
    }
    return heap;
  };
}

3 方法3 桶排序 时间复杂度: O(n); 空间复杂度: O(n)

ts 复制代码
function topKFrequent(nums: number[], k: number): number[] {
  // S1 统计每个元素出现的次数
  const record = new Map<number, number>();
  for (const num of nums) {
    record.set(num, (record.get(num) || 0) + 1);
  }
  // S2 创建频率桶,其中最大频率可以节省空间,它可能小于 nums.length
  const maxFreq = Math.max(...record.values());
  const bucket = Array.from({ length: maxFreq + 1 }, () => []);

  // S3 将元素添加到频率桶中
  for (const [num, freq] of record.entries()) {
    bucket[freq].push(num);
  }

  // S4 从后向前遍历频率桶,收集前k个高频元素
  // 易错点:是前k个高频元素,而不是 频率是排名前k的 所有元素
  const res = [];
  for (let i = maxFreq; i >= 0 && res.length < k; i--) {
    res.push(...bucket[i]);
  }
  return res;
}
相关推荐
GreenTea7 分钟前
GrokBot 核心成员 Lauren Tan:每月交付 2000 个 PR 的人,是怎么用 AI 的
前端·后端·架构
周末也要写八哥1 小时前
经典算法实例:游戏中弱角色的数量(二)
算法
mCell1 小时前
用 Knip 清理 AI Coding 留下的冗余代码
前端·ai编程·前端工程化
是Yu欸1 小时前
鸿蒙PC移植:2048 从网页小游戏到 AI 桌面应用
大数据·人工智能·算法·数据挖掘·openharmony·codex
鹿角片ljp1 小时前
KV Cache 解析
java·算法
liliangcsdn2 小时前
IVOL与偏度因子的对比测量分析
算法
笃行3502 小时前
使用Rokid AIUI做了一个童年推箱子游戏
前端
threerocks3 小时前
Jev 入门第一课
算法
excel3 小时前
前端加密的作用与使用场景
前端
西柚研究生1234564 小时前
论文分析17:YOLOv11_UAVNet:无人机航拍图像专用目标检测算法
人工智能·python·深度学习·算法·目标检测