Python面试宝典第48题:找丑数

题目

我们把只包含质因子2、3和5的数称作丑数(Ugly Number)。比如:6、8都是丑数,但14不是,因为它包含质因子7。习惯上,我们把1当做是第一个丑数。求按从小到大的顺序的第n个丑数。

示例 1:

python 复制代码
输入:5
输出:5

示例 2:

python 复制代码
输入:7
输出:8

暴力法

本题最直观的解法是使用暴力法,即从1开始逐个检查每个自然数是否为丑数,直到找到第n个丑数为止。使用暴力法求解本题的主要步骤如下。

1、定义一个函数,用于判断一个数是否为丑数。

2、从1开始遍历每一个数,对每个数调用该函数。

3、计数器记录已经找到的丑数的数量,当计数器达到n时,返回当前检查的数作为第n个丑数。

根据上面的算法步骤,我们可以得出下面的示例代码。

python 复制代码
def is_ugly_number(num):
    if num <= 0:
        return False
    for factor in [2, 3, 5]:
        while num % factor == 0:
            num //= factor
    return num == 1

def find_ugly_numbers_by_brute_force(n):
    count = 0
    num = 1
    while True:
        if is_ugly_number(num):
            count += 1
            if count == n:
                return num
        num += 1

print(find_ugly_numbers_by_brute_force(5))
print(find_ugly_numbers_by_brute_force(7))

优先队列法

优先队列法利用了最小堆的数据结构来保持未处理的丑数候选,其基本思想为:初始时,我们将1加入到优先队列中,因为1是最小的丑数;之后,每次从队列中取出最小的元素,并将其与2、3、5相乘后再次加入队列中。为了避免重复加入相同的丑数,我们需要记录之前加入队列的最大值,并确保不加入小于等于该最大值的数。使用优先队列法求解本题的主要步骤如下。

1、初始化堆和已知丑数。

(1)创建一个最小堆heap,并向其中添加第一个丑数1。

(2)创建一个集合seen,用于存储已经发现的丑数,以防止重复计算。

(3)设置变量last_ugly为 1,这代表最后加入的丑数。

(4)设置计数器count为 1,用来跟踪已经找到的丑数的数量。

2、循环直到找到第n个丑数,并执行以下操作。

(1)每次循环从堆中弹出最小的丑数current,并使用三个质因数(2、3、5)分别乘以current来生成新的丑数。

(2)对于每个新生成的丑数new_ugly,检查它是否已经被计算过,即是否存在于seen集合中。

(3)如果new_ugly还未被计算过,则将其添加到seen集合中,并将其推入堆heap中。

3、更新最后加入的丑数。

(1)如果弹出的丑数current不等于last_ugly,这意味着我们找到了一个新的丑数。

(2)更新last_ugly为current,并将count加一。

(3)当count达到n时,循环结束,此时last_ugly即为第n个丑数。

4、返回last_ugly作为第n个丑数。

根据上面的算法步骤,我们可以得出下面的示例代码。

python 复制代码
import heapq

def find_ugly_numbers_by_priority_queue(n):
    # 初始化最小堆
    heap = [1]
    # 记录最后一个加入堆的丑数
    last_ugly = 1
    # 已找到的丑数数量
    count = 1
    # 使用集合来去重
    seen = set([1])
    
    while count < n:
        # 弹出当前最小的丑数
        current = heapq.heappop(heap)
        
        # 生成新的丑数并加入堆中
        for factor in [2, 3, 5]:
            new_ugly = current * factor
            # 如果新生成的丑数还没有被加入过,则加入堆中
            if new_ugly not in seen:
                seen.add(new_ugly)
                heapq.heappush(heap, new_ugly)
        
        # 只有在当前丑数不等于最后一个加入的丑数时,才进行下一步操作
        if current != last_ugly:
            last_ugly = current
            count += 1
    
    # 最后一个加入的丑数就是第n个丑数
    return last_ugly

print(find_ugly_numbers_by_priority_queue(5))
print(find_ugly_numbers_by_priority_queue(7))

动态规划法

使用动态规划法的关键是:定义状态转移方程。假设dp[i]表示第i个丑数,则本题的状态转移方程为:dp[i] = min(2 * dp[j], 3 * dp[k], 5 * dp[l])。其中,j、k、l分别表示最近乘以2、3、5得到dp[i]的索引。边界条件为dp[1] = 1,因为1是最小的丑数。使用动态规划法求解本题的主要步骤如下。

1、初始化数组dp,长度为n + 1,其中dp[1] = 1。

2、定义三个指针j、k、l,初始值均为1。

3、对于每个i(从2到n),进行以下操作。

(1)计算dp[i]的值,它是2 * dp[j]、3 * dp[k]、5 * dp[l]中的最小值。

(2)如果dp[i]等于2 * dp[j],则j加1。

(3)如果dp[i]等于3 * dp[k],则k加1。

(4)如果dp[i]等于5 * dp[l],则l加1。

4、当i达到n时,dp[n]就是第n个丑数。

根据上面的算法步骤,我们可以得出下面的示例代码。

python 复制代码
def find_ugly_numbers_by_dp(n):
    if n == 1:
        return 1
    
    # 初始化dp数组
    dp = [0] * (n + 1)
    dp[1] = 1
    j, k, l = 1, 1, 1
    
    for i in range(2, n + 1):
        # 计算下一个丑数
        dp[i] = min(2 * dp[j], 3 * dp[k], 5 * dp[l])
        
        # 更新指针
        if dp[i] == 2 * dp[j]:
            j += 1
        if dp[i] == 3 * dp[k]:
            k += 1
        if dp[i] == 5 * dp[l]:
            l += 1
    
    return dp[n]

print(find_ugly_numbers_by_dp(5))
print(find_ugly_numbers_by_dp(7))

总结

暴力法的实现较为简单,但效率低下,尤其是在n较大的时候。最坏情况下,需要检查所有的数直到找到第n个丑数。假设第n个丑数是u(n),则暴力法的时间复杂度为O(u(n))。其空间复杂度为O(1),只需要常数级别的额外空间。

使用优先队列法时,插入和删除操作的时间复杂度为O(logu(n)),且需要做n次这样的操作,故总的时间复杂度为O(n*logu(n))。堆的大小最多为u(n),故总的空间复杂度为O(u(n))。与暴力法相比,优先队列法的效率更高,但可能带来较多的空间消耗。

使用动态规划法时,每个数只被访问一次,故时间复杂度为O(n)。其空间复杂度也为O(n),因为仅需要一个长度为n的数组来存储结果。动态规划法在时间和空间上都较为平衡,故实际应用中,通常倾向于使用动态规划法来解决这类问题。

相关推荐
AAA修煤气灶刘哥几秒前
日志排查不用慌!从采集到 ELK 实战,手把手教你搞定线上问题
后端·面试·debug
似水流年流不尽思念2 分钟前
MySQL 的 MVCC 到底解决了幻读问题没有?请举例说明。
mysql·面试
cxyll12342 分钟前
从接口自动化测试框架设计到开发(三)主流程封装、返回数据写入excel
前端·python·excel
就是帅我不改2 分钟前
SpringBoot多租户架构设计终极指南:5种方案彻底解决企业级SaaS隔离难题
后端·面试·架构
Kyln.Wu5 分钟前
【python实用小脚本-190】Python一键删除PDF任意页:输入页码秒出干净文件——再也不用在线裁剪排队
服务器·python·pdf
NAGNIP25 分钟前
LLaMA 3:离 AGI 更近一步?
算法
页面仔Dony30 分钟前
任务队列的中断和恢复实现
javascript·面试
言兴36 分钟前
深度解析:React Fiber 机制 —— 从“同步阻塞”到“可中断调度”的革命
前端·javascript·面试
PineappleCoder37 分钟前
力扣【2348. 全0子数组的数目】——从暴力到最优的思考过程
前端·javascript·算法
九章云极AladdinEdu1 小时前
Scikit-learn通关秘籍:从鸢尾花分类到房价预测
人工智能·python·机器学习·分类·scikit-learn·gpu算力