我试图制作一个递归函数,根据想要的元素数量将列表拆分为两个列表。
例如:
(分3'(1 3 5 7 9))((1 3 5)(7 9))
(分裂7'(1 3 5 7 9))((1 3 5 7 9)NIL)
(分裂0'(1 3 5 7 9))(无(1 3 5 7 9))
我的代码是这样的:
(defun split (e L)
(cond ((eql e 0) '(() L))
((> e 0) (cons (car L) (car (split (- e 1) (cdr L))))))))
我找不到加入第一个列表元素并返回第二个列表的方法。
答案 0 :(得分:3)
记住:split返回两个列表的列表。
(defun split (e L)
(cond ((eql e 0)
'(() L)) ; you want to call the function LIST
; so that the value of L is in the list,
; and not the symbol L itself
((> e 0)
; now you want to return a list of two lists.
; thus it probably is a good idea to call the function LIST
; the first sublist is made of the first element of L
; and the first sublist of the result of SPLIT
; the second sublist is made of the second sublist
; of the result of SPLIT
(cons (car L)
(car (split (- e 1)
(cdr L)))))))
答案 1 :(得分:3)
尾递归解决方案
(defun split (n l &optional (acc-l '()))
(cond ((null l) (list (reverse acc-l) ()))
((>= 0 n) (list (reverse acc-l) l))
(t (split (1- n) (cdr l) (cons (car l) acc-l)))))
改进版
(在此版本中,确保acc-l
位于开头'()
):
(defun split (n l)
(labels ((inner-split (n l &optional (acc-l '()))
(cond ((null l) (list (reverse acc-l) ()))
((= 0 n) (list (reverse acc-l) l))
(t (inner-split (1- n) (cdr l) (cons (car l) acc-l))))))
(inner-split n l)))
测试它:
(split 3 '(1 2 3 4 5 6 7))
;; returns: ((1 2 3) (4 5 6 7))
(split 0 '(1 2 3 4 5 6 7))
;; returns: (NIL (1 2 3 4 5 6 7))
(split 7 '(1 2 3 4 5 6 7))
;; returns ((1 2 3 4 5 6 7) NIL)
(split 9 '(1 2 3 4 5 6 7))
;; returns ((1 2 3 4 5 6 7) NIL)
(split -3 '(1 2 3 4 5 6 7))
;; returns (NIL (1 2 3 4 5 6 7))
在改进版本中,通过使用labels
(允许定义局部函数的let
种类,将递归函数放置得更深一层(一种封装))以允许他们自己调用的方式 - 所以它允许递归本地函数。)
我是如何找到解决方案的:
很明显,结果中的第一个列表必须是从l
的开头以连续的顺序逐个出现一个元素。但是,consing在其开头而不是结束时向现有列表添加元素。
因此,连续出售列表中的汽车将导致逆序。
因此,很明显,在最后一步中,当返回第一个列表时,它必须是reverse
d。第二个列表只是最后一步的(cdr l)
,因此可以在返回结果时将其添加到最后一步的结果中。
所以我想,最好将第一个列表累积到(acc-l
)中 - 累加器主要是尾递归函数的参数列表中的最后一个元素,即第一个列表的组件。我叫它acc-l
- accumulator-list。
在编写递归函数时,可以用普通的情况开始cond
部分。如果输入是一个数字和一个列表,那么最简单的情况 - 以及递归的最后一步,就是这种情况,
(equal l '())
---> (null l)
(= n 0)
- 实际上(zerop n)
。但后来我将其更改为(>= n 0)
以捕获负数作为输入的情况。(因此,递归cond
部分通常会在其条件中null
或zerop
。)
当列表l
为空时,则必须返回两个列表 - 而第二个列表是空列表,第一个列表是 - 不直观地 - reverse
d {{1} }。
您必须使用acc-l
构建它们,因为(list )
参数在返回之前不久会被评估(与list
= quote
相反,其中结果无法在最后评估步骤。)
当n为零(以及之后:当n为负数)时,除了将l作为第二个列表返回l以及为第一个列表累积的内容之前没有任何事情要做 - 但顺序相反。
在所有其他情况'(...)
中,列表(t ...)
的汽车l
已列入到目前为止累积的列表(第一个列表):cons
我将此作为累加器列表((cons (car l) acc-l)
)提供给acc-l
,将其余列表作为此调用split
和(cdr l)
中的新列表。递归调用中的这种递减对于递归函数定义非常典型。
由此,我们已经涵盖了递归中一步的所有可能性。 这使得递归变得如此强大:在一步中征服所有可能性 - 然后你已经定义了如何处理几乎无限多的案例。
非尾递归解决方案
(灵感来自Dan Robertson的解决方案 - 谢谢你Dan!特别是他的解决方案(1- n)
我很喜欢。)
destructuring-bind
只有非常基本功能的解决方案(仅限(defun split (n l)
(cond ((null l) (list '() '()))
((>= 0 n) (list '() l))
(t (destructuring-bind (left right) (split (1- n) (cdr l))
(list (cons (car l) left) right)))))
,null
,list
,>=
,let
, t
,cons
,car
,cdr
)
cadr
答案 2 :(得分:2)
好吧,让我们尝试推导出我们应该做的递归。
(split 0 l) = (list () l)
这就是我们的基本案例。现在我们知道
(split 1 (cons a b)) = (list (list a) b)
但是我们想了一下,我们正在构建左边的第一个参数,并且建立列表的方式是CONS
,所以我们写下来
(split 1 (cons a b)) = (list (cons a ()) b)
然后我们会思考一下,我们会考虑(split 0 l)
是什么,我们可以写下n>=1
:
(split n+1 (cons a b)) = (list (cons a l1) l2) where (split n b) = (list l1 l2)
所以让我们在Lisp中写下来:
(defun split (n list)
(ecase (signum n)
(0 (list nil list))
(1 (if (cdr list)
(destructuring-bind (left right) (split (1- n) (cdr list))
(list (cons (car list) left) right))
(list nil nil)))))
最惯用的解决方案是:
(defun split (n list)
(etypecase n
((eql 0) (list nil list))
(unsigned-integer
(loop repeat n for (x . r) on list
collect x into left
finally (return (list left r))))))