[Elementary-Algorithms/3]第n个丑数:改进二的学习理解

可以看出,这些改进都是从发现问题->找到方法->解决问题入手的,发现问题就是发现已存在的解法的不足:例如暴力解法中,需要验证第1500个丑数之前的所有正整数,验证方法是不断地求余取整,这些操作效率很低,于是改进一就从相反方向入手,直接生成丑数,直到第1500个,其中用到了支持弹出第一个元素和有序插入元素这两个操作的线性数据结构,找到这个方法后,就用这个方法解决了问题。但是,改进一中,元素的插入操作需要保证唯一性和有序性,这样在插入的时候就要遍历这个线性数据结构,导致时间复杂度从O(1)降到了O(n)。改进二就是为了解决这个问题而存在的,看描述(文字来自原文):

上面的解法虽然比暴力法快了很多,但是仍然有一些不足。它会产生很多的重复的元素,并且最终都被
丢弃了。其次,它需要扫描队列以保证队列中的元素有序。因此入队操作从常数时间O(1)退化为线性
时间O(|Q|)。
我们可以用三个队列来进行改进。这三个队列表示为Q2,Q3和Q5。它们初始化为Q2 = {2},
Q3 = {3}和Q5 = {5}。我们每次从这三个队列的头部选择最小的一个元素x取出,然后进行下面的
检查:
• 如果x是从Q2取出的,我们将2x加入Q2 ,3x加入Q3 ,5x加入Q5 。
• 如果x是从Q3取出的,我们只将3x加入Q3 ,5x加入Q5 ,而不需要将2x加入Q2 。这是因为2x已
  经在Q3中了。
• 如果x是从Q5取出的,我们只将5x加入Q5 ,而不需要处理2x和3x了。

按照惯例,看下截图更形象(截图来自原文):
这里写图片描述


同惯例的伪码解释(来自原文):

// 获取第n个丑数
function Get-Number(n)
    // 1是第一个丑数,但是1没有存放在任何一个队列中,所以n为1时直接返回1
    if n = 1 then
        return 1
    else // 其它情况下就用上面描述的通用算法
        // 初始化3个队列,用于分别存对应生成的丑数
        Q2 ← {2}
        Q3 ← {3}
        Q5 ← {5}
    // 该循环就是不断生成丑数的过程,直到第n个。n > 1是因为这里不包括1了
    while n > 1 do
        // 获取每个队列的头元素,并把最小的那个赋值给x
        x ← min(Head(Q2), Head(Q3), Head(Q5))
        // 如果x是从队列Q2中取出的
        if x = Head(Q2) then
            // 则让该元素出列
            Dequeue(Q2)
            // 并乘以2/3/5这三个因子,入列到对应队列中
            Enqueue(Q2, 2x)
            Enqueue(Q3, 3x)
            Enqueue(Q5, 5x)
        else if x = Head(Q3) then // 道理同上
            Dequeue(Q3)
            Enqueue(Q3, 3x)
            Enqueue(Q5, 5x)
        else // 道理同上
            Dequeue(Q5)
            Enqueue(Q5, 5x)
        n ← n − 1 // 以上操作执行完后,就排除了一个丑数,所以n自减1
    // 上面的循环结束后,就找到了第n个丑数,直接返回就行了
    return x

最后肯定是源码部分了。原文中作者有给出代码,用到了C++中的队列数据结构,因为C中不带原生的数据结构库。这里就不贴原文中的了,我自己用C简单实现了个队列,其实整个改进二的代码都不难。数据结构的实现在另一篇文章,点击查看队列的实现

#include <stdio.h>
#include "queue.h"

static inline
unsigned long wtlMin_Num(unsigned long a, unsigned long b){
    return a < b ? a : b;
}

unsigned long wtlFindUglyNum(int n){
    if (1 == n) {
        return 1;
    }

    wtlQUEUE* q2 = wtlQueue_Create();
    wtlQUEUE* q3 = wtlQueue_Create();
    wtlQUEUE* q5 = wtlQueue_Create();

    wtlQueue_Push(q2, 2);
    wtlQueue_Push(q3, 3);
    wtlQueue_Push(q5, 5);

    unsigned long x;
    while (n-- > 1) {
        x = wtlMin_Num(
                wtlMin_Num(
                    wtlQueue_Front(q2),
                    wtlQueue_Front(q3)
                ), wtlQueue_Front(q5)
        );

        if (x == wtlQueue_Front(q2)) {
            wtlQueue_Pop(q2);

            wtlQueue_Push(q2, x * 2);
            wtlQueue_Push(q3, x * 3);
            wtlQueue_Push(q5, x * 5);
        } else if (x == wtlQueue_Front(q3)) {
            wtlQueue_Pop(q3);

            wtlQueue_Push(q3, x * 3);
            wtlQueue_Push(q5, x * 5);
        } else {
            wtlQueue_Pop(q5);

            wtlQueue_Push(q5, x * 5);
        }
    }

    wtlQueue_Destroy(&q2);
    wtlQueue_Destroy(&q3);
    wtlQueue_Destroy(&q5);

    return x;
}

int main(int argc, char* argv[])
{
    unsigned long u_num = wtlFindUglyNum(1500);
    printf("%d\n", u_num); // 859963392

    return 0;
}

代码仅供参考,也可用其它语言实现。代码中就不加注释了,没什么好解释的,步骤就跟算法描述的一样。


到此书的PART 1就差不多结束了:)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值