Q48-code88- 合并两个有序数组
实现思路
1 方法1: 双指针后插法
- 利用数组有序特性,使用指针 后插法
参考文档
代码实现
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 的固定返回含义 + 返回实现的灵活性
参考文档
代码实现
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;
}