一位有匠心的手艺人

和他的生活方式

[译]快速斐波那契算法

| Comments

摘要

本文译自Fast Fibonacci

斐波那契数列在诸多方面,从头状花序通用编码(如禁忌编码)表现出种种有趣的性质。研究高效计算斐氏数列的过程充满乐趣。

斐波那契数列的经典公式表示为如下递推形式:

由此引出其直观(朴素)的递归实现:

1
2
3
4
5
6
7
uint64_t fibo_rec(uint64_t n)
{
    if (n <= 2)
        return 1;
    else
        return fibo_rec(n - 1) + fibo_rec(n - 2);
}

看上去很完美,可是计算的过程中产生了次递归调用!运行时间呈指数增长(因为,其中为黄金比例)。

通过显式地优化尾递可以消除递归调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
uint64_t fibo_iter(uint64_t n)
{
    uint64_t n_1 = 1, n_2 = 0;

    for (uint64_t i = 0; i < n; i++)
    {
        uint64_t t = n_1 + n_2;
        n_2 = n_1;
        n_1 = t;
    }

    return n_1;
}

这次计算的时间复杂度下降到,大大优于原始递归版本。迭代版本借助临时变量保存和,并使用移位寄存器将后继的斐波那契数列分别保存到n_2n_1。有些人可能会忌讳临时变量,那么可以将上述代码重写为:

1
2
3
4
5
6
7
8
9
10
11
12
uint64_t fibo_iter_var(uint64_t n)
{
    uint64_t n_1 = 1, n_2 = 0;

    for (uint64_t i = 0; i < n; i++)
    {
        n_1 = n_1 + n_2;
        n_2 = n_1 - n_2;
    }

    return n_1;
}

n_2 = n_1 - n_2部分化简得n_1,即,结果为)。此处我们通过额外操作抵消掉临时变量,好坏别当别论。

是否有进一步优化的空间?根据以往的经验,线性时间似乎是最好的结果。无独有偶,有些聪明的家伙发现矩阵

具有“平移”并增加一个矢量分量的性质。比如

看上去有几分眼熟吧。事实上我们将a代换为b代换为,得

有点眉目了!但这是基于已知的前提下,一旦成立意味着存在(递归)分解形式:

反复如此迭代到初始条件,那么有

矩阵运算似乎代价不菲,至少要进行线性次数的矩阵乘法。幸运的是不需要那么多次,我们有办法在时间复杂度内计算出(此处矩阵维数为常数,故可视矩阵乘积为常量)。如何计算呢?且观察式子。整数情况时计算过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int expo(int x, int n)
{
    int t = 1;
    int y = x;
    while (n)
    {
        if (n & 1)
            t *= y;
        y *= y;
        n >>= 1;
    }
    return t;
}

矩阵计算方式同理可得。以上代码中t为乘积当前值,y为乘积“平方”。给定两个矩阵

第一个初始化为单位阵,第二个初始化为斐波那契矩阵。根据矩阵的对称性,各自恒等于,这两个变量可以分别消掉。

其中虚线位置表示对称部分。将上述式子代入幂函数,将有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
uint64_t fibo_expo(uint64_t n)
{
    uint64_t a_t = 1, b_t = 0, d_t = 1, // "1"
            a_y = 0, b_y = 1, d_y = 1; // "y"

    while (n)
    {
        if (n & 1)
        {
            // t*=y;
            uint64_t a = a_t * a_y + b_t * b_y;
            uint64_t b = a_t * b_y + b_t * d_y;
            uint64_t d = b_t * b_y + d_t * d_y;

            a_t = a;
            b_t = b;
            d_t = d;
        }

        //y*=y;
        uint64_t a = a_y * a_y + b_y * b_y;
        uint64_t b = b_y * (a_y + d_y);
        uint64_t d = b_y * b_y + d_y * d_y;

        a_y = a;
        b_y = b;
        d_y = d;

        n >>= 1;
    }
    return b_t;
}

提取出公共子式,改变赋值顺序,消除多余的临时变量,得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint64_t fibo_expo_var(uint64_t n)
{
    uint64_t a_t = 1, b_t = 0, d_t = 1, // "1"
            a_y = 0, b_y = 1, d_y = 1; // "y"

    while (n)
    {
        if (n & 1)
        {
            // t*=y;
            uint64_t bx = b_t * b_y;
            b_t = a_t * b_y + b_t * d_y;
            a_t = a_t * a_y + bx;
            d_t = bx + d_t * d_y;
        }

        //y*=y;
        uint64_t b2 = b_y * b_y;
        b_y *= (a_y + d_y);
        a_y = a_y * a_y + b2;
        d_y = b2 + d_y * d_y;

        n >>= 1;
    }
    return b_t;
}

太棒了,我们找着了一个的算法来计算斐波那契数列。不过仍有稍加改进的余地。再怎么改进呢?且注意,

这样一来消去变量d,得到仅包含变量ab的方程。修改乘积项和平方项使其只包含变量ab

以及

其中虚线表示无需关心的部分。利用上述结果,可得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint64_t fibo_expo_var_2(uint64_t n)
{
    uint64_t a_t = 1, b_t = 0, // "1"
            a_y = 0, b_y = 1; // "y"

    while (n)
    {
        if (n & 1)
        {
            // t*=y;
            uint64_t bx = b_t * b_y;
            b_t = a_t * b_y + b_t * a_y + bx;
            a_t = a_t * a_y + bx;
        }

        //y*=y;
        uint64_t b2 = b_y * b_y;
        b_y = 2 * a_y * b_y + b2;
        a_y = a_y * a_y + b2;

        n >>= 1;
    }
    return b_t;
}

这几个算法就运行速度比较结果如何?原始递归算法效率低得惊人,生成需要一分多钟,而其他算法只花费几微秒。进行10000000(1000万)次迭代测试(使用相同的随机输入,随机数n位于1到70之间),结果如下

iter 3.29529s
iter_var 3.30153s
expo 2.28118s
expo_var 2.2531s
expo_var_2 2.22531s


最后三个版本的确快很多,但是快速乘幂算法版本之间的效率差距并不显著。假如改进程度相比迭代版本没有那么大的话,我们应该意识到刚只是计算相对较小的斐波那契数列。64位无符号整数最大仅允许存储(因为,所以两边取对数解出n,推出),不过通过大整数(抽象为类)来计算,远比迭代方法和递归算法快得多得多。

[译]设计模式滥用之Hello World

| Comments

设计模式流行之初,我曾在某新闻组里看到一条评论,说有人号称他们试图倾尽23个GoF设计模式去编写一个奇葩程序。不过他们说最终没有成,极其所能也只用到其中20个。他们希望客户叫他们返工,没准能加塞另外3个。

试图用遍所有模式是行不通的,终将以生拼硬凑的设计告终——投机取巧的设计充满百无一用的灵活性。这年头软件太过复杂了。我们不能猜测还要什么,而应该关注于本该要什么。

gamma1

大家刚开始学习设计模式时,很少考虑模式的适用场景,想方设法到处套用,觉得模式用得越多,设计越好。结果产生大量不必要复杂的代码。2

与其说是“运用模式”,不如说是“滥用模式”。有人试图在Hello World程序中套用模式,不可避免地导致滥用。

让我们考察一个示例。这是一个经典问题: 编写一个程序,打印字符串Hello World至标准输出。

编程新手会写出如下代码(Java语言):

1
System.out.println("hello world");

这段代码看起来超乎简单。能否加点设计模式?接下来…

首先,定义两个接口SubjectObserver,加入观察者模式

1
2
3
4
5
6
7
8
9
public interface Subject {
    public void attach(Observer observer);
    public void detach(Observer observer);
    public void notifyObservers();
}

public interface Observer {
    public void update(Subject subject);
}

然后,定义两个类HelloWorldSubjectHelloWorldObserver实现以上两个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class HelloWorldSubject implements Subject {

    private ArrayList<Observer> observers;
    private String str;

    public HelloWorldSubject() {
        super();

        observers = new ArrayList<Observer>();
    }

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        Iterator<Observer> iter = observers.iterator();

        while (iter.hasNext()) {
            Observer observer = iter.next();
            observer.update(this);
        }
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
        notifyObservers();
    }
}

public class HelloWorldObserver implements Observer {

    public void update(Subject subject) {
        HelloWorldSubject sub = (HelloWorldSubject)subject;
        System.out.println(sub.getStr());
    }

}

接着,再添加一个命令模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Command {
    void execute();
}

public class HelloWorldCommand implements Command {

    private HelloWorldSubject subject;

    public HelloWorldCommand(Subject subject) {
        super();

        this.subject = (HelloWorldSubject)subject;
    }

    public void execute() {
        subject.setStr("hello world");
    }

}

然后添加一个抽象工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface AbstractFactory {
    public Subject createSubject();
    public Observer createObserver();
    public Command createCommand(Subject subject);
}

public class HelloWorldFactory implements AbstractFactory {

    public Subject createSubject() {
        return new HelloWorldSubject();
    }

    public Observer createObserver() {
        return new HelloWorldObserver();
    }

    public Command createCommand(Subject subject) {
        return new HelloWorldCommand(subject);
    }
}

再添加一个单例模式即大功告成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FactoryMakerSingleton {

    private static FactoryMakerSingleton instance = null;
    private AbstractFactory factory;

    private FactoryMakerSingleton() {
        factory = new HelloWorldFactory();
    }

    public static synchronized FactoryMakerSingleton getInstance() {
        if (instance == null) {
            instance = new FactoryMakerSingleton();
        }

        return instance;
    }

    public AbstractFactory getFactory() {
        return factory;
    }
}

最后编写一个主类(用作测试)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AbuseDesignPatterns {

    public static void main(String[] args) {
        AbstractFactory factory = FactoryMakerSingleton.getInstance().getFactory();

        Subject subject = factory.createSubject();
        subject.attach(factory.createObserver());

        Command command = factory.createCommand(subject);

        command.execute();
    }

}

程序运行输出: Hello World

哇,我们已经在Hello World程序中成功地运用四种模式。(嗯,其实还能添加一个迭代器模式,不过已经引用了内置的迭代器,Java中经常如此)。这必须是一个拿得出手的设计呀。那么问题出在哪儿呢?

  1. 对于一个Hello World程序而言代码过于复杂。
  2. 包含多余的灵活性。
  3. 设计和实现的时间成本是彻彻底底的浪费。
  4. 引发了类膨胀。
  5. 违反了KISS原则。3
  6. 没满足问题的要求。问题目的本在于学习打印标准输出,以上代码却缪以千里。

总结

设计模式应具体场景具体运用,不要为运用而运用。设计模式是构建大型软件的强大工具。应恰到好处,切忌滥用。

这儿下载文中的代码。欢迎来函反馈。如果谁有办法在Hello World程序中堆砌更多设计模式,再好不过了。

参考文章

Hello, Octopress

| Comments

代码高亮测试

c

七段码显示代码链接
1
main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

ruby

Discover if a number is primeSource Article
1
2
3
4
5
class Fixnum
  def prime?
    ('1' * self) !~ /^1?$|^(11+?)\1+$/
  end
end

Latex数学公式测试

行内公式$\exp(-\frac{x^2}{2})$测试

公式测试一

公式测试二