程序员面试金典 - 面试题 17.09. 第 k 个数

题目难度: 中等

原题链接

今天继续更新程序员面试金典系列, 大家在公众号 算法精选 里回复 面试金典 就能看到该系列当前连载的所有文章了, 记得关注哦~

题目描述

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

示例 1:

  • 输入: k = 5

  • 输出: 9

题目思考

  1. 如何从当前的有效数得到后面的有效数?
  2. 如何保证从小到大?

解决方案

方案 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 即可得到, 然后三个数组的指针移动还和上面的分析一样
  • 下面的代码对必要步骤有详细的注释, 方便大家理解
复杂度
  • 时间复杂度 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]

大家可以在下面这些地方找到我~😊

我的 GitHub

我的 Leetcode

我的 CSDN

我的知乎专栏

我的头条号

我的牛客网博客

我的公众号: 算法精选, 欢迎大家扫码关注~😊

算法精选 - 微信扫一扫关注我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值