Q46- code264- 丑数 II
实现思路
1 方法1:暴力解法:知道暴力解法,才能有更好的优化解法
-
暴力解法:从1开始,不断判断一个数是否是丑数,直到找到第n个丑数
-
判断一个数是否是丑数:
- 不断除以2直到不能整除,相当于 “不断剥离2”
- 不断除以3直到不能整除,相当于 “不断剥离3”
- 不断除以5直到不能整除,相当于 “不断剥离5”
- 如果最后剩下1,说明这个数只包含2,3,5这些质因子
-
时间复杂度:O(n * log(m)),其中m是第n个丑数
-
空间复杂度:O(1)
-
暴力解法的缺点:
- 需要遍历处理 很多非丑数
- 对于每个数字都要进行多次除法运算 来判断是否为丑数
2 方法2:最小堆:
-
从最小丑数1开始,每次生成新的丑数时,将当前丑数分别乘以2、3、5,把得到的新丑数加入最小堆
-
每次从堆顶取出最小的丑数,这就保证了我们按照顺序生成丑数
-
使用一个集合去重,避免重复的丑数进入堆
-
时间复杂度:O(n * log(n)),其中n是第n个丑数
-
空间复杂度:O(n)
-
最小堆的优点:
- 直接生成丑数:不会额外处理很多非丑数
- 避免重复计算:不需要 对每个数都进行多次除法运算,来判断是否为丑数
3 方法3:三指针
-
用uglys有序数组来存储丑数,初始只有1
-
在2、3、5 票价窗口分别设置指针,其指针指向的是之前的最小丑数
-
每轮根据 更新窗口的最小值 和 非更新窗口的最小值,来入队本轮的最小丑数
-
由于依次入队的都是 每轮的最小丑数
- 所以 各个窗口在更新指针后,指向的必然是之前入队的所有丑数,不会有遗漏
- 即 可以理解为 是一种 ”渐进式的 穷举遍历“,而不是和堆一样一次性入队
- 以上即 保证了 ”有序性“ + ”无遗漏“
-
为了防止有重复丑数,可以使用一个 集合/同时更新窗口指针 来去重 2种方法实现
-
时间复杂度:O(n)
-
空间复杂度:O(n)
参考文档
代码实现
1 方法1: 最小堆/优先队列
- 时间复杂度:O(n * log(n)),其中n是第n个丑数
- 空间复杂度:O(n)
function nthUglyNumber(n: number): number {
if (n <= 0) return 0;
if (n === 1) return 1;
const saved = new Set<number>();
const heap = minHeap<number>((a, b) => a < b);
heap.add(1);
while (heap.size() && n > 0) {
const cur = heap.removeTop();
if (--n === 0) return cur;
const nums = [cur * 2, cur * 3, cur * 5].filter((num) => !saved.has(num));
nums.forEach((num) => {
heap.add(num);
saved.add(num);
});
}
}
function minHeap<T>(compare: (a: T, b: T) => boolean) {
const heap: T[] = [];
return {
size: () => heap.length,
peek: () => heap[0],
add: (item: T) => {
heap.push(item);
siftUp(heap.length - 1);
},
removeTop: (): T => {
const ret = heap[0];
swap(0, heap.length - 1);
heap.pop();
siftDown(0);
return ret;
},
};
function swap(i: number, j: number) {
[heap[i], heap[j]] = [heap[j], heap[i]];
}
function siftUp(idx: number) {
while (idx > 0) {
const parentIdx = ~~((idx - 1) / 2);
// compare返回true: 说明需要上浮,即 当前子值 < 父值
const willUp = compare(heap[idx], heap[parentIdx]);
if (!willUp) break;
swap(idx, parentIdx);
idx = parentIdx;
}
}
function siftDown(idx: number) {
while (1) {
const ldx = 2 * idx + 1, rdx = 2 * idx + 2;
let smller = idx;
// ldx < idx
if (ldx < heap.length && compare(heap[ldx], heap[smller])) {
smller = ldx;
}
// rdx < idx
if (rdx < heap.length && compare(heap[rdx], heap[smller])) {
smller = rdx;
}
if (smller === idx) break;
swap(smller, idx);
idx = smller;
}
}
}
2 方法2: 三指针/DP法
- 时间复杂度:O(n),其中n是传入的值
- 空间复杂度:O(n)
function nthUglyNumber(n: number): number {
if (n === 1) return 1;
const uglys = [1];
let i2 = 0, i3 = 0, i5 = 0;
for (let i = 1; i < n; i++) {
const next2 = uglys[i2] * 2;
const next3 = uglys[i3] * 3;
const next5 = uglys[i5] * 5;
const min = Math.min(next2, next3, next5);
uglys.push(min);
if (min === next2) i2++;
if (min === next3) i3++;
if (min === next5) i5++;
}
return uglys[n - 1];
}
Q47- code1086- 前五科的均分
实现思路
1 方法1: 快速选择
- 明确快速选择 和 快速排序的 区别
2 方法2: 最小堆
- 明确题意转化
参考文档
代码实现
1 方法1: 快速选择
- 时间复杂度:O(n)
- 空间复杂度:O(n)
function highFive(items: number[][]): number[][] {
// S1 获取id: [score1, score2...] 映射关系
const record = new Map<number, number[]>();
items.forEach(([id, score]) =>
record.set(id, [...(record.get(id) || []), score])
);
// S2 获取每个id对应的前5高分数的 平均数
return [...record.entries()]
.map(([id, scores]) => {
// 保证每个id成绩数组的 前5个元素是第5大的,内部顺序不保证,即topK
findTop5(scores, 0, scores.length - 1, 4);
// 获取其前5高分数的 平均数
return [id, ~~(scores.slice(0, 5).reduce((a, b) => a + b) / 5)];
})
.sort(([id1], [id2]) => id1 - id2);
}
function findTop5(arr: number[], l: number, r: number, tdx: number) {
if (l >= r) return;
const p = partition(arr, l, r); // 修复函数名拼写
if (p === tdx) return;
if (p < tdx) findTop5(arr, p + 1, r, tdx);
if (p > tdx) findTop5(arr, l, p - 1, tdx);
}
function partition(arr: 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) {
while (i <= j && arr[i] > x) i++;
while (i <= j && arr[j] < x) j--;
if (i >= j) break;
swap(arr, i++, j--);
}
swap(arr, l, j);
return j;
}
function swap(arr: number[], i: number, j: number) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
2 方法2:最大堆
- 时间复杂度:O(nlog5)
- 空间复杂度:O(n)
function highFive(items: number[][]): number[][] {
// S1 创建id和scores映射关系
const record = items.reduce((map, [id, score]) => {
map.set(id, [...(map.get(id) ?? []), score]);
return map;
}, new Map<number, number[]>());
// S2 使用大小为5的最小堆获取最大的5个分数
return [...record.entries()]
.map(([id, scores]) => {
const heap = minHeap<number>((a, b) => a < b);
// 维护大小为5的最小堆,使用链式调用
scores.forEach((score) =>
heap.size() < 5
? heap.add(score)
: score > heap.peek() && (heap.remove(), heap.add(score))
);
// 计算平均值,使用Array.from
const sum = Array.from({ length: 5 }, heap.remove).reduce(
(a, b) => a + b,
0
);
return [id, ~~(sum / 5)];
})
.sort((a, b) => a[0] - b[0]);
}
function minHeap<T>(compare: (a: T, b: T) => boolean) {
const heap: Array<T> = [];
return {
size: () => heap.length,
empty: () => heap.length === 0,
// 查看堆顶元素
peek: () => heap[0],
// 在末尾新增
add: (item: T) => {
heap.push(item);
siftUp(heap.length - 1);
},
// 去除堆顶元素
remove: (): T => {
if (heap.length === 0) return;
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为true,表示a < b,即 当前元素 < 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;
const rdx = idx * 2 + 2;
let next = idx;
// 如果左子元素较小,则 可能需要让当前元素 下沉
if (ldx < heap.length && compare(heap[ldx], heap[next])) next = ldx;
// 如果右子元素较小,则 可能需要让当前元素 下沉
if (rdx < heap.length && compare(heap[rdx], heap[next])) next = rdx;
if (next === idx) break;
swap(idx, next);
idx = next;
}
}
function swap(i: number, j: number) {
[heap[i], heap[j]] = [heap[j], heap[i]];
}
}