当前位置: 首页 > article >正文

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

题目

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

        示例 1:

输入:5
输出:5

        示例 2:

输入:7
输出:8

暴力法

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

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

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

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

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

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个丑数。

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

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个丑数。

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

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的数组来存储结果。动态规划法在时间和空间上都较为平衡,故实际应用中,通常倾向于使用动态规划法来解决这类问题。


http://www.kler.cn/a/302093.html

相关文章:

  • 【Java基础知识系列】之Java类的初始化顺序
  • 数据库范式、MySQL 架构、算法与树的深入解析
  • 怎么样绑定域名到AWS(亚马逊云)服务器
  • Java集合框架之Collection集合遍历
  • 《Python Web 抓取实战:豆瓣电影 Top 250 数据抓取与分析》
  • 写给初学者的React Native 全栈开发实战班
  • element-plus组件问题汇总
  • javaWeb三剑客:html,css
  • “精装朋友圈”的年轻人,开始在40度高温买羽绒服
  • 青少年人工智能编程水平测试YCL备考秘籍
  • 针对Docker容器的可视化管理工具—DockerUI
  • 排序算法总结
  • AE软件下载,辅助你完成梦想作品
  • Redis 主从复制的原理详解
  • 获取1688 API 接口
  • PFC和LLC的本质和为什么要用PFC和LLC电路原因
  • 【Linux】Linux 共享内存:高效的进程间通信
  • 磁盘空间不足扩容lvm
  • proteus+51单片机+实验(LCD1620、定时器)
  • 【JavaScript】LeetCode:31-35
  • 软件工程知识点总结(7):软件项目管理
  • 【STM32项目】基于STM32+RTOS音频光通信设计与实现(完整工程资料源码)
  • 网络安全产品认证证书大全(持续更新...)
  • 【复杂系统系列(中级)】Kolmogorov复杂度——信息的无序度量【通俗理解】
  • Python设计模式实战:开启软件设计的精进之旅
  • Log4j 1.x如何升级到Log4j 2.x