注意:出于某种原因,我使用 Common Lisp 编写了此答案,然后才注意到该问题被标记为 scheme、racket 和 lisp。在任何情况下,Common Lisp 都属于后者,代码很容易适应 Scheme 或 Racket。
对于非尾递归的函数,您需要进行递归调用,使它们不在尾位置,即,在返回之前不需要对递归调用的结果进行进一步的操作。因此,您需要一种递归策略来获取列表的最后一个元素,以对递归调用的结果进行进一步的操作。
一种策略是在从基本案例返回的路上建立一个“反向列表”,同时将该列表分开,以便在最后留下所需的结果。这是一个reversal 函数,可以在不拆开任何东西的情况下展示这个想法:
(defun reversal (xs)
(if (cdr xs)
(cons (reversal (cdr xs)) (car xs))
xs))
上面的函数用输入列表的元素反向构建一个嵌套的点列表:
CL-USER> (reversal '(1 2 3 4 5))
(((((5) . 4) . 3) . 2) . 1)
现在,可以在此结果上多次调用 car 函数以获取输入的最后一个元素,但我们可以在构造新列表时这样做:
(defun my-last (xs)
(car (if (cdr xs)
(cons (my-last (cdr xs)) (car xs))
xs)))
这里my-last函数是在调用(trace my-last)之后调用的:
CL-USER> (trace my-last)
(MY-LAST)
CL-USER> (my-last '(1 2 3 4 5))
0: (MY-LAST (1 2 3 4 5))
1: (MY-LAST (2 3 4 5))
2: (MY-LAST (3 4 5))
3: (MY-LAST (4 5))
4: (MY-LAST (5))
4: MY-LAST returned 5
3: MY-LAST returned 5
2: MY-LAST returned 5
1: MY-LAST returned 5
0: MY-LAST returned 5
5
该方案需要对调用my-last的结果进行两次操作,即cons和car。似乎可能优化器可以注意到car 正在被cons 的结果调用,并将my-last 优化为:
(defun my-last-optimized (xs)
(if (cdr xs)
(my-last-optimized (cdr xs))
(car xs)))
如果是这种情况,那么优化的代码将是尾递归的,然后可以应用尾调用优化。我不知道是否有任何 lisp 实现可以进行这种优化。
另一种策略是存储原始列表,然后在使用cdr 从基本案例备份的路上将其拆开。这是使用辅助函数的解决方案:
(defun my-last-2 (xs)
(car (my-last-helper xs xs)))
(defun my-last-helper (xs enchilada)
(if (cdr xs)
(cdr (my-last-helper (cdr xs) enchilada))
enchilada))
这也可以按预期工作。这是一个示例,再次使用trace 查看函数调用。这次my-last-2 和my-last-helper 都变成了traced:
(trace my-last-2 my-last-helper)
(MY-LAST-2 MY-LAST-HELPER)
CL-USER> (my-last-2 '(1 2 3 4 5))
0: (MY-LAST-2 (1 2 3 4 5))
1: (MY-LAST-HELPER (1 2 3 4 5) (1 2 3 4 5))
2: (MY-LAST-HELPER (2 3 4 5) (1 2 3 4 5))
3: (MY-LAST-HELPER (3 4 5) (1 2 3 4 5))
4: (MY-LAST-HELPER (4 5) (1 2 3 4 5))
5: (MY-LAST-HELPER (5) (1 2 3 4 5))
5: MY-LAST-HELPER returned (1 2 3 4 5)
4: MY-LAST-HELPER returned (2 3 4 5)
3: MY-LAST-HELPER returned (3 4 5)
2: MY-LAST-HELPER returned (4 5)
1: MY-LAST-HELPER returned (5)
0: MY-LAST-2 returned 5
5
在这种情况下,递归调用 my-last-2 返回后唯一需要的操作是 cdr,但这足以防止这成为尾调用。