List 这种常见的Lisp代码是否与它们的';温斯顿/霍恩用LISP第三版重新教学?

List 这种常见的Lisp代码是否与它们的';温斯顿/霍恩用LISP第三版重新教学?,list,time-complexity,lisp,common-lisp,reverse,List,Time Complexity,Lisp,Common Lisp,Reverse,他们这么说 (defun user-reverse (l) (if (endp l) nil (append (user-reverse (rest l)) (list (first l))))) 由于计算量为n(n+1)/2,因此不好 然后他们说这样走: (defun user-reverse (l &optional result) (if (endp l) result (user-reverse

他们这么说

(defun user-reverse (l)
  (if (endp l)
      nil
      (append (user-reverse (rest l))
              (list (first l)))))
由于计算量为n(n+1)/2,因此不好

然后他们说这样走:

(defun user-reverse (l &optional result)
  (if (endp l)
      result
      (user-reverse (rest l)
                    (cons (first l) result))))
所以我在想,为什么你不能做这样的事:

(defun user-reverse (l)
  (do ((new-list nil))
      ((endp l) new-list)
    (push (pop l) new-list)))
这是一个坏习惯还是什么?还是抄袭了名单什么的?他们教了“Do”几个章节,想知道有没有LisPaS可以说这是标准的还是更坏的,或者他们的例子没有什么?基本上,我想知道他们的坏“n”例子是如何站起来的?

有两个问题:

  • 算法和数据结构的复杂性和效率
  • 递归与循环
让我们先看看复杂性/效率问题。

Lisp中的列表是cons单元格的单链接列表或空列表。 因此,在列表的末尾添加一个简单的附件是一个代价高昂的操作。在Lisp列表中,将元素添加到列表的前面成本较低,而将元素添加到列表的末尾成本较高。因此,应该编写一个例程,这样它就不会使用这些代价高昂的操作

如果我们有一个递归例程,就像第一个使用昂贵的append操作的例程一样,我们希望找到一个不同的例程,它不会添加到列表的末尾。这就是为什么第二套比第一套好

递归与循环

您发布的第二个版本是end recursive。递归调用可以替换为两个变量的跳转和更新。这使用了尾部调用优化(TCO)。在Lisp中,这是一种优化,编译器通常可以这样做。但它们不需要这样做,基于解释器的实现通常不需要这样做。在Scheme中,语言定义要求实现支持TCO

对于TCO,第二个版本既使用了高效的操作,又在恒定的空间中运行(尽管它分配了一个反向列表)

如果没有TCO,第二个版本将使用高效的操作,但可能会导致大型列表的堆栈溢出

循环

第三个版本使用循环-here
DO
。它既使用高效的运算符,又只受内存大小的限制。它不受堆栈大小的限制,因为它不执行任何递归调用

通常,在现实世界中,Lisp中的软件(->不是为教学目的编写的)会发现代码使用循环,因为TCO通常不适用于所有实现。例如,公共Lisp的ABCL实现在Java虚拟机(JVM)上运行。因为JVM不提供实现TCO的直接特性,所以JVM的编程语言实现通常不提供TCO。ABCL也不例外

dolist

可以使用
dolist
编写循环版本:

(defun user-reverse (l &aux r)
  (dolist (e l r)
    (push e r)))

“在Lisp列表中,将其添加到列表末尾的成本很高。”。确切地说,
rplacd
既便宜又简单。从顶部节点发现终端节点的成本很高。另外,当然,
append
复制了第一个列表的全部内容。好的,很酷,谢谢你的深入回答和更多!另外,谢谢你的提醒。非常好的信息,再次感谢您抽出时间为我解决一些问题!另一个与您的版本不同的地方是:它改变了变量绑定。这没有什么错,除非有:我的意思是,在很大程度上,变异状态的程序是否更糟糕是一个意见问题,但有时不这样做的程序更清晰(有时这样做的程序更清晰),也许编译器也更容易推理。