不使用反向返回列表中最后一个元素的非尾递归函数?

Posted

技术标签:

【中文标题】不使用反向返回列表中最后一个元素的非尾递归函数?【英文标题】:Non-tail recursive function that returns the last element in list without using reverse? 【发布时间】:2021-11-23 23:54:21 【问题描述】:

我试图让一个非尾递归函数返回列表的最后一个元素,而不使用任何类型的反向、映射、迭代、突变(内置或用户构建)。到目前为止,我已经成功地制作了一个尾递归版本和一个使用反向函数的非尾版本。但我就是想不出如何制作一个非尾递归函数。

非常感谢您的帮助!

【问题讨论】:

当尾递归很容易制作时,为什么要这样做?课堂作业? 你尝试尾递归,如果很难,可能会选择非尾递归。为什么要尝试非尾递归函数? 我很难想象任何使用reverse的递归函数。 【参考方案1】:

想象一下你有这样的尾递归版本:

(define (last-element lst)
  (if base-case-expression
      result-expression
      recursion-expression))

现在为了不让它尾递归,你只需让你的函数对结果做一些事情。例如。将其缓存在绑定中,然后返回:

(define (last-element lst)
  (if base-case-expression
      result-expression
      (let ((result recursion-expression))
        result)))

这里递归调用不是尾部位置。然而,一个足够聪明的编译器可能会使编译后的代码是尾递归的。例如。许多 Scheme 实现将代码转换为连续传递样式,然后每个调用都变成了尾调用,并且堆栈被不断增长的闭包所取代。这两个版本的结果将非常相似。

【讨论】:

【参考方案2】:

注意:出于某种原因,我使用 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的结果进行两次操作,即conscar。似乎可能优化器可以注意到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-2my-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,但这足以防止这成为尾调用。

【讨论】:

以上是关于不使用反向返回列表中最后一个元素的非尾递归函数?的主要内容,如果未能解决你的问题,请参考以下文章

去掉字符串中的非数字字符

单链表的逆向打印删除无头的非尾节点无头链表插入节点约瑟环

python学习 - 列表操作

递归比较两个列表

在 Haskell 中添加存储为 0 和 1 列表的两个二进制数,而不使用反向

DrRacket 生成递归问题需要帮助