题目难度: 中等
今天继续更新剑指 offer 系列, 老样子晚上 6 点 45 分准时更新公众号 每日精选算法题, 大家记得关注哦~ 另外在公众号里回复 offer 就能看到剑指 offer 系列当前连载的所有文章了
题目描述
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
- 1 是丑数。
- n 不超过 1690。
题目样例
示例
- 输入: n = 10
- 输出: 12
- 解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
题目思考
- 如何从当前的丑数得到后面的丑数?
- 如何保证从小到大?
解决方案
方案 1
思路
- 一个比较容易想到的思路是使用一个小顶堆, 每次从堆顶 pop 出当前最小的丑数, 然后乘以 2/3/5 得到新丑数, 如果新丑数没有在堆中的话, 就将其加入堆中
- 这样 pop n 次即为第 n 个丑数
- 判断新丑数是否存在, 可以额外使用一个集合, 这样判断存在性就只需要 O(1)
- 显然初始化堆和集合中的元素都是 1, 代表第 1 个丑数
- 以上的操作是不是很类似 BFS 的思路? 不同的是这里利用了堆而不是双端队列/列表来处理当前的元素, 所以举一反三很重要
复杂度
- 时间复杂度 O(NlogN): 共需要 N 次堆操作, 每次堆操作的时间复杂度是 logN
- 空间复杂度 O(N): 使用了一个小顶堆和一个集合
代码
class Solution: def nthUglyNumber(self, n: int) -> int: # 初始化集合和堆中元素都为1 v = {1} q = [1] for i in range(n): res = heapq.heappop(q) for factor in (2, 3, 5): nex = res * factor if nex not in v: # 新丑数没在堆里的话, 将其加入堆中 v.add(nex) heapq.heappush(q, nex) return res
方案 2
思路
- 回顾方案 1, 因为引入了堆, 所以时间复杂度达到了 O(NlogN), 那有没有更优的方案呢, 比如 O(N) 时间复杂度?
- 答案也是有的, 我们重新分析题目, 要求数字的质因子只有 2/3/5, 我们就可以把当前丑数乘以 2/3/5 的数字分别存入三个数组中, 并将 1 作为第 1 个值, 这样就可以将题目转换成将三个有序数组进行合并去重后求第 n 个最小值
arr2 = [1*2, 2*2, 3*2, 4*2, 5*2, 6*2, 8*2, ...] (注意7不是丑数)
- arr3 和 arr5 类似, 只是把乘数改成了 3 和 5
- 为什么需要去重呢? 举个例子, 对于 6, 它既存在于 arr2 中, 也存在于 arr3 中
- 如何合并去重呢? 我们可以维护 3 个指针, 分别对应这三个数组遍历到的元素位置, 那么当前最小值自然就是 3 个元素中最小的那个了, 然后将最小元素对应的指针后移(可能会遇到最小值不止一个, 这个时候移动的指针也不止一个), 继续判断即可
- 需要保存哪些数组呢? 注意 arr2/arr3/arr5 有个共同特点是作为因子的丑数序列是相同的, 都是
[1,2,3,4,5,6,8,...]
, 只是需要乘的质因子不同. 所以我们并没有必要真正保存 3 个数组, 而只需要保存升序丑数序列即可, 这样恰好该序列的第 n 个数就是最终结果. 而 arr2/arr3/arr5 只需要在该丑数序列基础上乘以 2/3/5 即可得到, 然后三个数组的指针移动还和上面的分析一样 - 下面的代码对必要步骤有详细的注释, 方便大家理解
复杂度
- 时间复杂度 O(N): 只需要遍历一遍
- 空间复杂度 O(N): 额外使用了一个数组存升序丑数序列
代码
class Solution: def nthUglyNumber(self, n: int) -> int: # 初始化丑数序列第一个元素为1 ugly = [1] # 初始化arr2/arr3/arr5的下标都为0 i2, i3, i5 = 0, 0, 0 while len(ugly) < n: # 取arr2/arr3/arr5三者中的最小值追加到当前升序丑数序列中 # 根据下面三个if判断的逻辑, 新追加的值一定大于之前丑数序列的最大值(最后一个值), 因为之前的最大值若等同于arr2/arr3/arr5的下标对应的值的话, 会将下标+1的, 新下标的值一定大于原下标的 ugly.append(min( 2 * ugly[i2], 3 * ugly[i3], 5 * ugly[i5], )) if ugly[-1] == 2 * ugly[i2]: # 最小值和arr2下标的值*2一样, i2加1 i2 += 1 if ugly[-1] == 3 * ugly[i3]: # 同上 i3 += 1 if ugly[-1] == 5 * ugly[i5]: # 同上 i5 += 1 return ugly[-1]
大家可以在下面这些地方找到我~😊
我的公众号: 每日精选算法题, 欢迎大家扫码关注~😊