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