我正在尝试在LISP中实现反向算法。我对这门语言比较陌生,所以任何帮助都会受到赞赏。
我在LISP中有以下代码,对我来说似乎合乎逻辑但是当我在终端上运行它时它不会输出任何内容。这就是我的rev.lisp文件的样子:
(defun rev (list)
(if (atom list)
(append (rev (cdr list))
(list (rev (car list))))))
我在终端上运行它:
%clisp rev.lisp %
我期待输出 6 5 4 3 2 ,但它不会返回任何内容。
答案 0 :(得分:2)
你已经有了答案,但这里有一些评论。
根据您的背景和偏好,有时可以通过执行跟踪最好地理解递归算法。
TRACE
宏可以帮助您调试代码。
确切的输出因实现而异(这里,我使用的是SBCL)。
因为我想告诉你调用APPEND
多少次,并且因为在不允许 1 中跟踪标准函数,我正在定义一个简单的函数并且重新定义代码以使用它:
(defun append* (&rest args) (apply #'append args))
TRACE的结果如下:
CL-USER> (rev '(2 3 4 5 6))
0: (REV (2 3 4 5 6))
1: (REV (3 4 5 6))
2: (REV (4 5 6))
3: (REV (5 6))
4: (REV (6))
5: (REV NIL)
5: REV returned NIL
5: (REV 6)
5: REV returned 6
5: (APPEND* NIL (6))
5: APPEND* returned (6)
4: REV returned (6)
4: (REV 5)
4: REV returned 5
4: (APPEND* (6) (5))
4: APPEND* returned (6 5)
3: REV returned (6 5)
3: (REV 4)
3: REV returned 4
3: (APPEND* (6 5) (4))
3: APPEND* returned (6 5 4)
2: REV returned (6 5 4)
2: (REV 3)
2: REV returned 3
2: (APPEND* (6 5 4) (3))
2: APPEND* returned (6 5 4 3)
1: REV returned (6 5 4 3)
1: (REV 2)
1: REV returned 2
1: (APPEND* (6 5 4 3) (2))
1: APPEND* returned (6 5 4 3 2)
0: REV returned (6 5 4 3 2)
(6 5 4 3 2)
首先,我们发现有时会在ATOM
元素上调用REV
。即使你的实现用CAR
解包元素并用LIST
再次包装它们,这样做也没有意义。反转列表是一个应用于列表的函数,如果您碰巧传递了非列表参数,它应该会在您的头部引发一个红色标记。为了构建列表的递归函数,通常只关注数据类型的递归定义。
LIST
类型在Lisp中定义为(OR NULL CONS)
,它是NULL
类型和CONS
类型的并集。换句话说,列表是空的或cons-cell。
有很多方法可以区分两种风格大不相同的情况。按照上面的类型方法,您可以使用ETYPECASE
,它会调度其参数的类型,并在没有子句匹配时发出错误信息:
(defun rev (list)
(etypecase list
(null <empty>)
(cons <non-empty> )))
您也可以使用ENDP
。
空列表的反向是一个空列表,如果您只是使用WHEN
并且焦点和非空案例,那么您就是这样:
(defun rev (list)
(when list
<non-empty>))
上面,我们不检查LIST是否是一个缺点,它可能是任何东西。但是,我们在下面使用它的方式只能应用于这些对象,这意味着运行时检查会尽早检测出错误的情况。
(defun rev (list)
(when list
(append* (rev (rest list))
(list (first list)))))
以上内容与您的代码非常相似,只是我没有在第一个元素上调用REV。另外,我使用FIRST
和REST
代替CAR
和CDR
,因为即使它们是相应的同义词,前者更能传达使用列表的意图(这是当然是主观的,但大多数人都遵循这条规则)。
上面的描述显示,只读代码时您可能错过了所有中间列表的APPEND。这在内存和处理方面非常浪费,因为APPEND必须遍历所有元素以将它们复制到新的列表中。如果您调用APPEND n 次,正如您在迭代 n 元素列表时所做的那样,最终会得到二次算法( n 2 的)。
您可以通过使用NCONC
代替APPEND
重复使用相同的中间列表来解决内存问题。您仍然需要多次迭代此列表,但至少重复使用相同的基础cons单元格。通常,递归reverse
使用附加参数 accumulator 编写,该参数用于存储中间结果并将其返回到最深层:
(defun reverse-acc (list acc)
(etypecase list
;; end of input list, return accumulator
(null acc)
;; general case: put head of input list in front
;; of current accumulator and call recursively with
;; the tail of the input list.
(cons (reverse-acc (rest list)
(cons (first list) acc)))))
使用空累加器调用上面的示例。尽管可以直接向用户访问此函数,但您可能更愿意隐藏此实现细节并仅导出具有单个参数的函数:
(defun rev (list) (reverse-acc list nil))
(trace rev reverse-acc)
0: (REV (2 3 4 5 6))
1: (REVERSE-ACC (2 3 4 5 6) NIL)
2: (REVERSE-ACC (3 4 5 6) (2))
3: (REVERSE-ACC (4 5 6) (3 2))
4: (REVERSE-ACC (5 6) (4 3 2))
5: (REVERSE-ACC (6) (5 4 3 2))
6: (REVERSE-ACC NIL (6 5 4 3 2))
6: REVERSE-ACC returned (6 5 4 3 2)
5: REVERSE-ACC returned (6 5 4 3 2)
4: REVERSE-ACC returned (6 5 4 3 2)
3: REVERSE-ACC returned (6 5 4 3 2)
2: REVERSE-ACC returned (6 5 4 3 2)
1: REVERSE-ACC returned (6 5 4 3 2)
0: REV returned (6 5 4 3 2)
(6 5 4 3 2)
跟踪的形状是典型的递归函数,可以消除尾部调用。实际上,REVERSE-ACC
内部的递归调用直接返回我们想要的结果,因此不需要中间内存来存储和处理中间结果。但是,标准不要求Common Lisp实现消除尾部位置的递归调用,特定实现的实际行为甚至可能依赖于优化级别。因此,符合程序不能假设控制堆栈不会随列表的大小线性增长。
递归性最适用于某些类型的问题,这些问题本质上是递归的,并且堆栈的高度不会如此快速地增长w.r.t.输入。对于迭代,使用控件结构,如DO
,LOOP
等。在下面的示例中,我使用DOLIST
来临时RESULT列表中的PUSH
元素,这是在DOLIST结束时回归:
(defun rev (list)
(let ((result '()))
(dolist (e list result)
(push e result))))
跟踪是:
0: (REV (2 3 4 5 6))
0: REV returned (6 5 4 3 2)
(6 5 4 3 2)
1。 11.1.2.1.2 Constraints on the COMMON-LISP Package for Conforming Programs
答案 1 :(得分:1)
你没有打印任何东西,所以你没有看到任何东西。
将(rev '(2 3 4 5 6))
替换为(print (rev '(2 3 4 5 6)))
,您会在屏幕上看到(6 5 4 3 2)
。