从暴力到高效:彻底搞懂丑数问题,面试再也不怕啦!

在算法学习的道路上,总有一些经典题目如同“拦路虎”,既考验我们对数学定义的理解,又要求我们跳出常规思维寻找优化方案。“丑数问题”就是这样一道兼具数学性与编程性的经典题目,今天我们就从定义出发,一步步拆解问题本质,带你掌握高效求解思路,让你再遇到这类问题时能轻松应对!

一、先搞清楚:到底什么是丑数?

首先,我们得明确丑数的数学定义:只包含因子2、3和5的正整数称为丑数,并且习惯上把1作为第一个丑数

为了让大家更直观地理解,我们来看几个例子:

  • 符合条件的丑数:6(2×3)、8(2³)、10(2×5),这些数的所有质因子都只在2、3、5的范围内。

  • 不符合条件的数:14(包含因子7)、21(包含因子7),因为它们存在2、3、5之外的质因子。

通过简单枚举,我们能发现前6个丑数依次是1、2、3、4、5、6,这个规律可是后续优化解法的重要基础,大家一定要记牢哦!

二、问题分析:别再用暴力法浪费时间啦!

我们常见的丑数问题是“按从小到大的顺序找到第N个丑数”。如果一开始就想到“暴力法”,也就是对每个数依次判断是否为丑数,那可就麻烦了。

比如当N比较大(像N=1000)时,我们需要遍历大量的非丑数(如7、11、13等),这会导致时间复杂度极高,效率特别低,在实际面试或工程应用中根本行不通。

这时候,我们就得转换思路,从丑数的生成规律入手。根据丑数的定义,除了1之外,所有的丑数都是由更小的丑数乘以2、3或5得到的。比如:

  • 4 = 2×2(这里的2是第2个丑数)

  • 6 = 2×3(2是第2个丑数,3是第3个丑数)

  • 9 = 3×3(3是第3个丑数)

既然有这样的规律,那我们就可以“主动生成丑数”,而不是“被动判断丑数”。具体来说,就是用一个数组存储已经排好序的丑数,后面的丑数都由数组中前面的元素推导而来,这样就能大大提高效率。

三、核心难点:如何保证生成的丑数有序?

不过,新的问题又出现了。如果我们直接把数组中所有元素分别乘以2、3、5,再筛选最小值,不仅会有重复计算(比如6既可以由2×3生成,也可以由3×2生成),效率也依然不高。

那该怎么解决呢?关键在于跟踪“有效乘数位置”。对于因子2、3、5,我们分别设置一个“临界位置”,记为t2、t3、t5。这三个临界位置需要满足:

  • 位置t2之前的丑数乘以2,结果都小于当前数组中最大的丑数,这些结果已经存在于数组中了,不需要再考虑,避免重复;

  • 位置t2及之后的丑数乘以2,结果大于当前最大的丑数,这些结果是候选的新丑数。

t3和t5的作用也类似,分别对应因子3和5的临界位置。每次生成新的丑数后,我们就更新这三个临界位置,这样就能确保后续生成的丑数始终是有序的。

四、高效解法实现:多语言代码示例

基于上面的思路,我们可以设计出时间复杂度为O(N)、空间复杂度为O(N)的解法。下面分别给大家展示C++和Python的实现代码,并解析关键步骤。

1. C++实现

#include <vector>
#include <algorithm> // 用于min函数
using namespace std;

class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        // 前6个丑数是1-6,直接返回index
        if (index < 7) {
            return index;
        }
        // 定义数组存储有序丑数,大小为index
        vector<int> ugly_nums(index);
        // 初始化前6个丑数
        for (int i = 0; i < 6; ++i) {
            ugly_nums[i] = i + 1;
        }
        // 初始化三个临界位置:t2对应因子2,t3对应因子3,t5对应因子5
        int t2 = 3, t3 = 2, t5 = 1;
        // 从第7个丑数开始生成(索引6)
        for (int i = 6; i < index; ++i) {
            // 新丑数是三个候选值中的最小值
            ugly_nums[i] = min(ugly_nums[t2] * 2, min(ugly_nums[t3] * 3, ugly_nums[t5] * 5));
            // 更新t2:确保ugly_nums[t2] * 2是下一个大于当前最大丑数的值
            while (ugly_nums[i] >= ugly_nums[t2] * 2) {
                t2++;
            }
            // 同理更新t3
            while (ugly_nums[i] >= ugly_nums[t3] * 3) {
                t3++;
            }
            // 同理更新t5
            while (ugly_nums[i] >= ugly_nums[t5] * 5) {
                t5++;
            }
        }
        // 返回第index个丑数(数组索引从0开始)
        return ugly_nums[index - 1];
    }
};

2. Python实现

Python语法更简洁,不需要手动管理数组大小,直接用列表动态追加元素即可:

# -*- coding:utf-8 -*-
class Solution:
    def GetUglyNumber_Solution(self, index):
        # 前6个丑数直接返回
        if index < 7:
            return index
        # 初始化有序丑数列表
        ugly_nums = [1, 2, 3, 4, 5, 6]
        # 初始化三个临界位置
        t2, t3, t5 = 3, 2, 1
        # 生成第7个到第index个丑数
        for i in range(6, index):
            # 计算候选值并取最小作为新丑数
            new_ugly = min(ugly_nums[t2] * 2, min(ugly_nums[t3] * 3, ugly_nums[t5] * 5))
            ugly_nums.append(new_ugly)
            # 更新临界位置
            while ugly_nums[t2] * 2 <= new_ugly:
                t2 += 1
            while ugly_nums[t3] * 3 <= new_ugly:
                t3 += 1
            while ugly_nums[t5] * 5 <= new_ugly:
                t5 += 1
        # 返回第index个丑数
        return ugly_nums[index - 1]

3. 关键步骤解析

  • 初始化优化:因为前6个丑数固定是1-6,所以当index < 7时,我们可以直接返回index,避免进行多余的计算,节省时间。

  • 临界位置初始化

    • t2=3:对应的丑数是4(索引3),4×2=8,这是下一个由因子2生成的丑数;

    • t3=2:对应的丑数是3(索引2),3×3=9,这是下一个由因子3生成的丑数;

    • t5=1:对应的丑数是2(索引1),2×5=10,这是下一个由因子5生成的丑数。

  • 循环更新:每次生成新的丑数后,我们通过while循环移动临界位置,确保下一次生成的候选值始终大于当前最大的丑数,这样就能避免重复生成丑数,同时保证丑数的有序性。

五、解法验证:用实例说话

光说不练假把式,我们以“求第7个丑数”为例,来验证一下算法的逻辑是否正确。

  1. 初始状态:数组是[1,2,3,4,5,6],t2=3、t3=2、t5=1;

  2. 计算候选值:4×2=8、3×3=9、2×5=10,这三个候选值中的最小值是8,把8加入数组后,数组变成[1,2,3,4,5,6,8];

  3. 更新临界位置:

  • 因为8 >= 4×2,所以t2更新为4(对应的丑数是5,5×2=10);

  • 8 < 3×3,所以t3保持不变;

  • 8 < 2×5,所以t5保持不变。

最终得到第7个丑数是8,和我们预期的结果一致。如果再进一步求第8个丑数,候选值就是5×2=10、3×3=9、2×5=10,最小值是9,把9加入数组后更新t3=3,逻辑也完全成立。

六、总结:掌握思路,举一反三

丑数问题的核心在于“从生成规律出发,通过跟踪临界位置保证有序性”。和暴力法相比,这种解法把时间复杂度从O(N×K)(其中K是判断一个数是否为丑数的时间)优化到了O(N),空间复杂度是O(N)(用于存储已经生成的丑数)。

而且,这个思路不仅仅适用于丑数问题,还可以推广到“由固定因子生成有序序列”的同类问题中,比如寻找只包含因子2和3的数。这是算法学习中“找规律+优化”思维的典型应用,掌握了这种思维,以后再遇到类似的算法题,就能轻松找到突破口啦!

希望今天的分享能帮助大家彻底搞懂丑数问题,在后续的算法学习和面试中都能更上一层楼!如果大家有其他疑问或者更好的解法,欢迎在评论区留言讨论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值