记录 1 道算法题
丑数ii
丑数是指因数含有是 2,3,5 的数。
1 也被作为丑数
可以使用堆进行解决,因为题目的要求是返回 第 n 个丑数。
丑数集合按照升序排列,所以是找第 n 小的丑数,使用大顶堆解决。
也可以使用动态规划解决,使用3个指针。
每一个丑数都是2,3,5的倍数,所以每一个丑数都可以是前面的某一个丑数的 2,3,5倍。
例如 4 是 2 的倍数,所以 4 是丑数, 8 是 2 的倍数也是前面的丑数 4 的倍数。
所以丑数也是累积的,我们就可以根据前面的丑数来推算后面的丑数,例如 2 可以推出 4, 6, 10。然后 4 又可以推出 8, 12, 20。
同时我们也发现会有重复的数,比如 2 和 3 都能推出 6。
有了这些规律之后,怎么进行代码上的推导呢。
首先我们准备一个数组,为了直接对照下标,我们让数组从下标为 1 开始,然后设置 arr[1] = 1 作为累积的基础,1 可以推导出 2, 3, 5。
假设我们存好的数组是 [0, 1, 2, 3, 4, 5]
下标 1, 值 1, 推导出 2, 3, 5
下标 2, 值 2, 推导出 4, 6, 10
下标 3, 值 3, 推导出 6, 9, 15
下标 4, 值 4, 推导出 8, 12, 20
问题是数组里面每一个下标的值怎么设置,因为初始化的时候,我们只知道 [empty, 1], arr[1] = 1,以及 1 推导出的 3 个数。上帝视角可以知道下标为 2 的值应该为 2,但代码不知道,我们要做的就是推算 下标为 2 的时候,值应该是什么。
每一个下标的值应该从之前的数的 2,3,5倍里面选择。
下标为 2 的值就应该从 1 * 2, 1 * 3, 1 * 5 里面选择。保持升序排列,我们选择最小的数。
此时, 下标 2, 值 2, 可以推导出 4, 6, 10
但是 上一次推导的 3 和 5 还没存起来。我们需要有一个指针指着他们。假设我们这么设计指针,每个倍数档都有自己的记录,上一个数 1 有 3 个分支, 1 * 2, 1 * 3,1 * 4。分别被 n1,n2,n3 指着。
0 1
2 ↑
3 ↑
5 ↑
下标 2, 值 2,所推导出的 4, 6, 10,其中代表着前面的数的 3 倍的分支的值 6,前面的数的 5 倍的分支的值 10 永远比前一个分支大,所以必须先分配前面的分支才行,这时可以不更新指针,让他继续指着之前的分支,我们可以通过让指针停在 1 这里。永远可以得到 1 * 3,1 * 5。
而 2 已经被分配了。他可以向前走一步,更新到 下标 2, 值 2 的 2 倍分支中。
0 1 2
2 ↑
3 ↑
5 ↑
于是我们就在 2 * 2,1 * 3, 1 * 5 之中推导下标 3 的值,3 倍分支进行了分配,可以往前走一步。
最小的数是 3, 所以 下标 3, 值 3, 可以推导出 6, 9, 15。
0 1 2 3
2 ↑
3 ↑
5 ↑
我们可以看到更新了 3 倍分支之后,取到了上次排着队等分配的 6。
下一个下标 4 就是比较 2 * 2, 2 * 3, 1 * 5。最小是 4,2倍分支再走一步。
0 1 2 3 4
2 ↑
3 ↑
5 ↑
下一个下标 5,比较 3 * 2, 2 * 3, 1 * 5, 最小是 5。
0 1 2 3 4 5
2 ↑
3 ↑
5 ↑
这时我们发现 下一个下标 6 的时候,比较 3 * 2, 2 * 3, 2 * 5。出现了两个 6。我们可以理解为 2 倍分支和 3倍分支都得到了分配。所以都往前走一步。如果不是同时走的话,下一轮还能得到 6。就会重复存 6。
0 1 2 3 4 5 6
2 ↑
3 ↑
5 ↑
可能有人会觉得 0, 1, 2, 3, 4, 5, 6 是下标,其实这些是值,只是刚好这些都是丑数而已,我们计算倍数是根据前一个数的值进行计算的,所以是丑数。
下标 7, 比较 4 * 2, 3 * 3, 2 * 5,最小是8
0 1 2 3 4 5 6 8
2 ↑
3 ↑
5 ↑
之后只要重复一直比较最小值就行。循环的条件就是下标,我们要第 n 个,就循环到 下标为 n 就可以了。比如我要第 6 个, 累积到下标为 6 的时候,已经能得到结果了。
最后总结一下,三个指针之所以可以按照倍数来设置,是因为后面的丑数都是前面的丑数的2, 3,5倍。而且是递增的。如果某个倍数分支(丑数下标指着的丑数)没有分配进数组存起来,这个倍数后面的数只会越来越大,所以必须前面的先分配,于是就让他停在当前的丑数下标,指着这个丑数,指代这个丑数的多少倍,直到他被分配就指着下一个推导的数。
另一种视图:
2 的倍数 根据下标 2, 4, 6, 8
3 的倍数 根据下标 3, 6, 9, 12
5 的倍数 根据下标 5, 10,15,20
可以看到是依次取的,所以我们需要 3 个指针,分别指向 2, 3, 5倍。
代码如下:
function nthUglyNumber(n) {
const arr = new Array(n + 1)
arr[1] = 1
let n1 = 1,
n2 = 1,
n3 = 1
for (let i = 2; i <= n; i++) {
// 得到推导的数
const a = arr[n1] * 2,
b = arr[n2] * 3,
c = arr[n3] * 5
// 比较最小值
const num = Math.min(a, b, c)
// 存起来,实现升序排列
arr[i] = num
// 确认是哪个分支的,因为只知道最小值,还不知道是谁的。
if (num === a) {
n1++
}
if (num === b) {
n2++
}
if (num === c) {
n3++
}
}
return arr[n]
}
结束