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;
}
相关推荐
呆呆的小鳄鱼几秒前
leetcode:322. 零钱兑换[完全背包]
算法·leetcode·职场和发展
Mintopia5 分钟前
Three.js 中的噪声与图形变换:一场数字世界的舞蹈
前端·javascript·three.js
Mintopia9 分钟前
计算机图形学漫游:从像素到游戏引擎的奇幻之旅
前端·javascript·计算机图形学
Gyoku Mint9 分钟前
深度学习×第7卷:参数初始化与网络搭建——她第一次挑好初始的重量
人工智能·pytorch·rnn·深度学习·神经网络·算法·机器学习
钢铁男儿10 分钟前
C#接口实现详解:从理论到实践,掌握面向对象编程的核心技巧
java·前端·c#
mit6.82420 分钟前
[Vroom] 位置与矩阵 | 路由集成 | 抽象,解耦与通信
c++·人工智能·算法
用户403159863966325 分钟前
在工作中学算法——专线配置
java·算法
用户403159863966329 分钟前
在工作中学算法——基于日志的系统故障预测
java·算法
এ᭄画画的北北31 分钟前
力扣-240.搜索二维矩阵 II
算法·leetcode·矩阵
前端的日常31 分钟前
以下代码,那一部分运行快
前端