Recursion 遍历列表时反转列表与非尾部递归

Recursion 遍历列表时反转列表与非尾部递归,recursion,functional-programming,scheme,lisp,racket,Recursion,Functional Programming,Scheme,Lisp,Racket,我想知道你们这些有经验的Lisper/函数式程序员通常是如何决定使用什么的。比较: (define (my-map1 f lst) (reverse (let loop ([lst lst] [acc '()]) (if (empty? lst) acc (loop (cdr lst) (cons (f (car lst)) acc)))))) 及 这个问题可以用以下方式描述:每当我们必须遍历一个列表时,我们是否应该在累加器中收集结果,累

我想知道你们这些有经验的Lisper/函数式程序员通常是如何决定使用什么的。比较:

(define (my-map1 f lst)
  (reverse
   (let loop ([lst lst] [acc '()])
     (if (empty? lst)
         acc
         (loop (cdr lst) (cons (f (car lst)) acc))))))

这个问题可以用以下方式描述:每当我们必须遍历一个列表时,我们是否应该在累加器中收集结果,累加器保留尾部递归,但最终需要列表反转?或者我们应该使用未优化的递归,但是我们不需要反转任何东西

在我看来,第一个解决方案总是更好的。事实上,这里还有额外的复杂性(O(n))。然而,它使用的内存要少得多,更不用说调用函数不是立即完成的了

然而,我看到了使用第二种方法的不同例子。要么我遗漏了什么,要么这些例子只是教育性的。是否存在未优化递归更好的情况?

带累加器的尾部递归
  • 遍历列表两次
  • 构造两个列表
  • 恒定堆栈空间
  • 可能会因malloc错误而崩溃
朴素递归
  • 遍历列表两次(一次构建堆栈,一次拆除堆栈)
  • 构造一个列表
  • 线性堆栈空间
  • 可能因堆栈溢出(不太可能在racket中)或malloc错误而崩溃

在我看来,第一个解决方案总是更好

分配通常比额外的堆栈帧花费更多的时间,所以我认为后一个会更快(不过您必须对其进行基准测试才能确定)

是否存在未优化递归更好的情况

是的,如果您正在创建一个延迟求值的结构,在haskell中,您需要cons单元格作为求值边界,并且您不能延迟求值尾部递归调用


基准测试是唯一可以确定的方法,racket有很深的堆栈框架,所以您应该能够使用这两个版本

stdlib版本是,这表明如果愿意牺牲可读性,通常可以挤出一些性能

给定同一函数的两个实现,使用相同的O表示法,我将在95%的时间内选择更简单的版本。

带累加器的尾部递归
  • 遍历列表两次
  • 构造两个列表
  • 恒定堆栈空间
  • 可能会因malloc错误而崩溃
朴素递归
  • 遍历列表两次(一次构建堆栈,一次拆除堆栈)
  • 构造一个列表
  • 线性堆栈空间
  • 可能因堆栈溢出(不太可能在racket中)或malloc错误而崩溃

在我看来,第一个解决方案总是更好

分配通常比额外的堆栈帧花费更多的时间,所以我认为后一个会更快(不过您必须对其进行基准测试才能确定)

是否存在未优化递归更好的情况

是的,如果您正在创建一个延迟求值的结构,在haskell中,您需要cons单元格作为求值边界,并且您不能延迟求值尾部递归调用


基准测试是唯一可以确定的方法,racket有很深的堆栈框架,所以您应该能够使用这两个版本

stdlib版本是,这表明如果愿意牺牲可读性,通常可以挤出一些性能


给定相同函数的两个实现,使用相同的O表示法,我会选择95%的时间使用更简单的版本。

有很多方法可以使递归保持迭代过程

我通常直接做连续传球。这是我做这件事的“自然”方式

要考虑函数的类型。有时,您需要将您的函数与其周围的函数连接起来,根据它们的类型,您可以选择另一种执行递归的方法

你应该从解决“小阴谋家”入手,为这个问题打下坚实的基础。在“LittleTyper”中,您可以发现另一种类型的doing递归,它基于其他计算哲学,用于agda、coq等语言


在scheme中,您有时可以编写实际上是haskell的代码(您可以编写由haskell编译器作为中间语言生成的一元代码)。在这种情况下,执行递归的方法也不同于“通常”的方法,等等。

有许多方法可以使递归保持迭代过程

我通常直接做连续传球。这是我做这件事的“自然”方式

要考虑函数的类型。有时,您需要将您的函数与其周围的函数连接起来,根据它们的类型,您可以选择另一种执行递归的方法

你应该从解决“小阴谋家”入手,为这个问题打下坚实的基础。在“LittleTyper”中,您可以发现另一种类型的doing递归,它基于其他计算哲学,用于agda、coq等语言


在scheme中,您有时可以编写实际上是haskell的代码(您可以编写由haskell编译器作为中间语言生成的一元代码)。在这种情况下,执行递归的方法也不同于“常规”方法等。

如果可能,我使用高阶函数,如
map
,它在引擎盖下构建一个列表。在Common Lisp中,我也倾向于大量使用
循环
,它有一个
collect
关键字,用于向前构建列表(我还使用
系列
库,该库也透明地实现了它)

我有时使用非尾部递归的递归函数,因为它们更好地表达了我想要的内容,而且列表的大小将相对较小;特别是,在编写宏时,被操作的代码通常不是很大

对于我没有收集到列表中的更复杂的问题,我通常接受为每个解决方案调用的回调函数。这确保了
(define (my-map2 f lst)
  (if (empty? lst)
      '()
      (cons (f (car lst)) (my-map2 f (cdr lst)))))
(defun range (n f)
  (dotimes (i n)
    (funcall f i)))
(defun range-list (N)
  (let ((list nil))
    (range N (lambda (v) (push v list)))
    (nreverse list)))
(defun queue ()
  (let ((list (list nil)))
    (cons list list)))

(defun qpush (queue element)
  (setf (cdr queue)
        (setf (cddr queue)
              (list element))))

(defun qlist (queue)
  (cdar queue))
(defun range-list (n)
  (let ((q (queue)))
    (range N (lambda (v) (qpush q v)))
    (qlist q)))
(defun empty-index (vector)
  (block nil
    (range (length vector)
           (lambda (d) 
             (when (null (aref vector d))
               (return d))))))