曾经在微博中谈到在可计算理论中著名的Ackermann函数。当时接触到的算法是Rózsa Péter 在1935年提出的算法:
A(0, n):= n + 1 for n ≥ 0 A(m, 0):= A(m – 1, 1) for m > 0 A(m, n):= A(m – 1, A(m, n – 1)) for m, n > 0
该算法基于两个参变量的递归函数,但不是尾递归的。由于此算法对递归深度的增加是超过了指数级的,因此在32位的系统中,这个算法很快就能将堆栈消耗光。首先给出此算法的CL代码:
;;Ackermann Function, 1935 Rozsa Peter, classic mad benchmark
(defun ack (m n)
(cond ((= m 0) (1+ n))
((= n 0) (ack (1- m) 1))
(t (ack (1- m) (ack m (1- n))))))
对于较大的m值,此函数递归深度增加的速度非常快,因此实际上计算较大的m和n的结果将是不可能的。
(虽然他是一个可计算函数,但是却非primitive recursive(不能用有限的迭代来代替)的)
今天在lambda-the-ultimate.org中看到一篇关于尾递归的讨论贴,其中有关于Ackermann函数的尾递归实现(scheme代码),其基本思想是用树结构来记录在Rozsa Peter算法中(特别是第三个表达式)需要的中间结果,将对堆栈的依赖转移到对树的创建与遍历。以下是这个尾递归Ackermann函数的CL代码:
;;Ackermann Function Tail Recursive, in lambda-the-ultimate.org
(defun ack-tr (m n)
(cond ((null m) n)
(t (let ((nm 0) (nn 0))
(cond ((numberp m) ;;对用户传入参数的处理。若m不是树,则将其变成树根节
(progn
(setf nm (list m))
(setf nn n)))
((= (first m) 0) ;;对应A(0, n):= n + 1 for n ≥ 0,以求出值,删除节点
(progn
(setf nm (rest m))
(setf nn (1+ n))))
((= n 0) ;;对应A(m, 0):= A(m – 1, 1) for m > 0,替换节点,继续求值
(progn
(setf nm (cons (1- (first m)) (rest m)))
(setf nn 1)))
(t ;;对应A(m, n):= A(m – 1, A(m, n – 1)) for m, n > 0,增加节点,代替内层函数求值
(progn
(setf nm (cons (first m)
(cons (1- (first m))
(rest m))))
(setf nn (1- n)))))
(ack-tr nm nn))))) ;;尾递归在这里出现!
由于尾递归可以很容易的被编译器优化为迭代,因此,上述的尾递归算法实际对堆栈的消耗是个常量。不过,为了维护列表m,实际的开销并没有减少。
Reference:
http://home.versatel.nl/vspickelen/Largefiles/Ackerman.htm