用 Groovy 计算斐波那契数列的回顾
作者:Paul King
发布时间:2022-09-08 10:59AM
迭代式
除非你最初学习的是函数式编程语言,否则你可能在最初的编程学习练习中编写过迭代版本的阶乘或斐波那契数列。对于斐波那契数列,这样的算法可能看起来像这样
def fib(n) {
if (n <= 1) return n
def previous = n.valueOf(1), next = previous, sum
(n-2).times {
sum = previous
previous = next
next = sum + previous
}
next
}
assert fib(10) == 55
assert fib(50L) == 12586269025L
assert fib(100G) == 354224848179261915075G
这个解决方案中唯一有趣的部分是使用了动态特性。我们没有为n
提供显式类型,所以鸭子类型意味着该方法可以很好地用于Integer
、Long
和BigInteger
值。这种实现使用提供的n
的类型进行所有计算,因此该方法的用户控制着这方面。
Groovy 提供了其他选择,例如指定显式类型(如Number
)或使用TypeChecked
或CompileStatic
进行类型推断,如果你需要的话。我们稍后将看到这些选项的示例。
递归
掌握迭代编程后,你接下来的编程学习练习可能是阶乘或斐波那契数列的递归版本。对于斐波那契数列,你可能编写了类似下面的代码
def fib(n) {
if (n <= 1) return n
fib(n - 1) + fib(n - 2)
}
assert fib(10) == 55
assert fib(50L) == 12586269025L
assert fib(100G) == 354224848179261915075G
这个天真的版本非常低效。例如,调用 fib(6) 会导致 fib(2) 被计算了五次
有几种方法可以避免这种重复。一种选择是使用@Memoized
注解。在方法上添加这个注解会导致编译器向方法注入适当的代码以缓存结果。对于像斐波那契数列这样的纯函数,这非常理想,因为它们对于给定的输入始终返回相同的输出。注解属性可以调整缓存的大小,但这里不需要这么复杂。
@Memoized
def fib(n) {
if (n <= 1) return n
fib(n - 1) + fib(n - 2)
}
assert fib(10) == 55
assert fib(50L) == 12586269025L
assert fib(100G) == 354224848179261915075G
现在,它的运行速度与迭代方法一样快。如果你碰巧使用的是Closure
而不是方法,你可以调用Closure
上的memoize
方法之一。
这种方法(实际上递归本身)的一个问题是,对于较大的n
值,例如fib(500G)
,你会遇到堆栈溢出异常。Groovy 通过包含TailRecursive
注解支持尾递归消除。在这种情况下,编译器会注入一个“展开”的非递归版本算法。为了使“展开”成功,需要重新整理算法,以便在任何返回语句中最多只调用一次 fib。下面是经过重新整理的算法版本
@TailRecursive
static fib(n, a, b) {
if (n == 0) return a
if (n == 1) return b
fib(n - 1, b, a + b)
}
assert fib(10, 0, 1) == 55
assert fib(50L, 0L, 1L) == 12586269025L
assert fib(100G, 0G, 1G) == 354224848179261915075G
assert fib(500G, 0G, 1G) == 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125G
如果你以前没有见过,这个版本可能比原始版本更难理解,但现在它既高效又可以处理较大的n
值。我们可以使用编译静态来获得更快的速度,例如
@TailRecursive
@CompileStatic
static fib(Number n, Number a, Number b) {
if (n == 0) return a
if (n == 1) return b
fib(n - 1, b, a + b)
}
assert fib(10, 0, 1) == 55
assert fib(50L, 0L, 1L) == 12586269025L
assert fib(100G, 0G, 1G) == 354224848179261915075G
assert fib(500G, 0G, 1G) == 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125G
如果你使用的是Closure
,你可以考虑使用Closure
上的trampoline
方法来实现类似的结果。
流
我们在本文开头看到了基于 Stream 的“单行代码”解决方案。让我们采用到目前为止使用过的鸭子类型特性,并定义一个 fib 方法。它可能看起来像这样
def fib(n) {
def zero = n.valueOf(0)
def one = n.valueOf(1)
Stream.iterate([zero, one], t -> [t[1], t.sum()])
.skip(n.longValue())
.findFirst().get()[0]
}
assert fib(10) == 55
assert fib(50L) == 12586269025L
assert fib(100G) == 354224848179261915075G
字节码和 AST 转换
最后,为了让你了解所有选择,这里有一个使用@Bytecode AST 转换的版本,它允许你直接在 Groovy 中编写 JVM 字节码!请注意,这属于“永远不要这样做”的类别,但为了让你知道你可以这样做,它包含在这里
@Bytecode
int fib(int i) {
l0
iload 1
iconst_2
if_icmpgt l1
iconst_1
_goto l2
l1
frame SAME
aload 0
iload 1
iconst_2
isub
invokevirtual '.fib','(I)I'
aload 0
iload 1
iconst_1
isub
invokevirtual '.fib', '(I)I'
iadd
l2
frame same1,'I'
ireturn
}
assert fib(10) == 55
在考虑使用该转换进行任何除了极端情况之外的操作之前,请阅读该转换的注意事项。它更像是用来尝试的有趣的事情,而不是任何人在生产环境中想要做的事情。
祝你编写自己的算法愉快!