斐波拉契数列,即形如0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...
,通项公式为An = An-1 + An-2
的数列。那么怎么用递归的方式求出指定位置的斐波拉契数呢?
1. 三种求法
下面将介绍三种利用递归求斐波拉契数的方式,为了比较各方式的效率,需要测量运行时间。程序示例使用Python
语言,首先编写一个装饰器来测量运行时间。
def time(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print("---func {:s}--- {:f} seconds---"
.format(func.__name__, end - start)
)
return r
return wrapper
1.1 直接使用通项公式
既然有通项公式,那么就直接粗暴地使用吧
@time
def fib_1(n):
def aux(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return aux(n - 1) + aux(n - 2)
assert n > 0
return aux(n)
此方式每次求aux(n - 1)
时,都会利用递归aux(n - 1) = aux(n - 2) + aux(n - 3)
,因此,aux(n - 2)
被计算了两次,这样时间复杂度会成指数级增长。
1.2 优化通项
既然求第n
项时要使用到n - 1
以及n - 2
项,那么每次递归时直接返回两项就好了,这样就避免了重复计算。
@time
def fib_2(n):
def aux(n):
if n == 1:
return 0,1
else:
second_front, first_front = aux(n - 1)
current = second_front + first_front
return first_front, current
assert n > 0
return aux(n)[1]
aux(n)
返回的是第n - 1
以及n
项。计算aux(n)
时,先求出aux(n - 1)
,即可得到第n - 2
及n - 1
项,那么第n
项就可以由这两者相加得到,那么aux(n)
也就求出来了。
1.3 尾递归
尽管fib_2
时间复杂度是线性的,但是递归过程中需要用临时变量存储每次递归地结果,这样会导致栈增长,当n
过大时,会导致Stack overflow
。因此,需要想一个尾递归的解法。
这种解法的思路与上述两种不同,不直接从通项入手,通项公式是“从后往前”推导,为什么不用更“人性化”的方法,从前往后计算呢?大家手算时不也是这样算的么?
@time
def fib_3(n):
def aux(second, first, n):
if n == 1:
return first
else:
return aux(first, second + first, n - 1)
assert n > 0
return aux(0, 1, n)
从前往后,依次推到第n
项就OK了,这种方法跟循环的思路差不多。
2. 运行结果
if __name__ == '__main__':
import sys
n = int(sys.argv[1])
result = set()
assert len(sys.argv) > 2
for func in sys.argv[2:]:
try:
r = eval('%s(%d)'%(func,n))
result.add(r)
except Exception as e:
print(e)
assert len(result) == 1
print(list(result)[0])
用上述程序试着运行一下吧
$ python3 fib.py 40 fib_1 fib_2 fib_3
---func fib_1--- 57.110849 seconds---
---func fib_2--- 0.000029 seconds---
---func fib_3--- 0.000013 seconds---
102334155
看来优化还是有效果的嘛。但是fib_3
虽然是尾递归的形式,但python解释器并没有对尾递归做优化
$ python3 fib.py 666 fib_3
---func fib_3--- 0.000601 seconds---
6859356963880484413875401302176431788073214234535725264860437720157972142108894511264898366145528622543082646626140527097739556699078708088
$ python3 fib.py 6666 fib_3
maximum recursion depth exceeded in comparison
Traceback (most recent call last):
File "fib.py", line 62, in <module>
assert len(result) == 1
AssertionError
轻轻松松就爆栈了
3. C语言尾递归优化
既然python不给力,那么就用C来写一下吧。
$ cat fib.c
long fib(long second, long first, int n) {
if (n == 1)
return first;
else
return fib(first, second + first, n - 1);
}
$ gcc -O2 -c -o fib.o fib.c
$ gobjdump -d fib.o
fib.o: file format mach-o-x86-64
Disassembly of section .text:
0000000000000000 <_fib>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 83 fa 01 cmp $0x1,%edx
7: 74 64 je 6d <_fib+0x6d>
9: 8d 4a 07 lea 0x7(%rdx),%ecx
c: 44 8d 42 fe lea -0x2(%rdx),%r8d
10: 83 e1 07 and $0x7,%ecx
13: 74 1f je 34 <_fib+0x34>
15: f7 d9 neg %ecx
17: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
1e: 00 00
20: 48 89 f0 mov %rsi,%rax
23: 48 89 fe mov %rdi,%rsi
26: 48 01 c6 add %rax,%rsi
29: ff ca dec %edx
2b: ff c1 inc %ecx
2d: 48 89 c7 mov %rax,%rdi
30: 75 ee jne 20 <_fib+0x20>
32: eb 03 jmp 37 <_fib+0x37>
34: 48 89 f8 mov %rdi,%rax
37: 41 83 f8 07 cmp $0x7,%r8d
3b: 72 30 jb 6d <_fib+0x6d>
3d: b9 01 00 00 00 mov $0x1,%ecx
42: 29 d1 sub %edx,%ecx
44: 66 66 66 2e 0f 1f 84 data16 data16 nopw %cs:0x0(%rax,%rax,1)
4b: 00 00 00 00 00
50: 48 01 f0 add %rsi,%rax
53: 48 01 c6 add %rax,%rsi
56: 48 01 f0 add %rsi,%rax
59: 48 01 c6 add %rax,%rsi
5c: 48 01 f0 add %rsi,%rax
5f: 48 01 c6 add %rax,%rsi
62: 48 01 f0 add %rsi,%rax
65: 48 01 c6 add %rax,%rsi
68: 83 c1 08 add $0x8,%ecx
6b: 75 e3 jne 50 <_fib+0x50>
6d: 48 89 f0 mov %rsi,%rax
70: 5d pop %rbp
71: c3 retq
MD太复杂了,看不懂。lea 0x7(%rdx),%ecx
是什么鬼?反正用jmp
代替call
就是了。
(完)