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 维护 最大堆的性质,返回最后结果
参考文档
代码实现
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个高频元素
参考文档
代码实现
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;
}