使用LISP递归地将列表拆分为两个

时间:2014-03-12 20:33:48

标签: recursion lisp elisp sequence

使用LISP,我需要创建一个将列表拆分为两个列表的函数。第一个列表包括第1个,第3个,第5个,第7个等元素,第二个列表包含第2个,第4个,第6个等元素。

输出示例:

(SPLIT-LIST ( )) => (NIL NIL)

(SPLIT-LIST '(A B C D 1 2 3 4 5)) => ((A C 1 3 5) (B D 2 4))

(SPLIT-LIST '(B C D 1 2 3 4 5)) => ((B D 2 4) (C 1 3 5))

(SPLIT-LIST '(A)) => ((A) NIL)

该函数需要递归。

到目前为止,这是我的代码。

(defun SPLIT-LIST (L)
  (cond
    ((null L) NIL)
    ((= 1 (length L)) (list (car L)))
    (t (cons (cons (car L) (SPLIT-LIST (cddr L))) (cadr L)))))
);cond
);defun

我稍后会尝试使用flatten,以便最终获得两个列表,但就目前而言,我似乎无法正确获取序列。

我的代码:

> (SPLIT-LIST '(1 2 3 4))
((1 (3) . 4) . 2)

我似乎无法使代码打印1 3 2 4而不是1 3 4 2.

> (SPLIT-LIST '(1 2 3 4 5 6))
((1 (3 (5) . 6) . 4) . 2)

我不能使预期输出的后半部分以正确的顺序打印。

7 个答案:

答案 0 :(得分:6)

您的代码

我们通常通过缩进来阅读Lisp代码,而不是全写大写。由于我们通过缩进阅读,我们不需要在他们自己的行上放置关闭的parens(或任何parens,真的)。那么,格式正确的代码是:

(defun split-list (l)
  (cond
    ((null l) '())
    ((= 1 (length l)) (list (car l)))
    (t (cons (cons (car l)
                   (split-list (cddr l)))
             (cadr l)))))

正确获取基本案例

Split-list 总是应该返回两个列表的列表。我们应该首先覆盖这些基础案例。如果l为空,则左侧列表或右侧没有任何内容,因此我们只需返回'(() ())即可。然后第一个条件变为:

((null l) '(() ())) ; or ((endp l) '(() ()))

根据你的第二个案例,我认为你希望第二和第三种情况是:(i)如果只剩下一个元素,它必须是一个奇数编号,并且属于左边结果; (ii)否则,至少剩下两个元素,我们可以为每个元素添加一个元素。那么第二个条件应该是

((= 1 (length l)) (list (car l) '()))

在每一步检查l的长度实际上有点贵。你只关心是否只剩下一个元素。当使用缺点单元格作为列表时,您已经知道l不是空的(从第一种情况开始), so you can just check whether the rest of l is the empty list. I find it more readable to use第一次and休息“,所以我和#39; d将第二个句写为:

((endp (rest l)) (list (list (first l)) '()))

处理递归案例

现在,你的最后一个案例是至少有两个元素。这意味着l看起来像(x y . zs)。您需要做的是致电split-list上的zs以获取(odd-zs even-zs)形式的结果,然后将其拆分并构建((x . odd-zs) (y . even-zs))。这看起来像这样:

(t (let ((split-rest (split-list (rest (rest l)))))
     (list (list* (first l) (first split-rest))
           (list* (second l) (second split-rest)))))

实际上有一些方法可以清理它。我们可以使用destructuring-bind同时拉出odd-zseven-zs。因为这是cond的最后一个子句,如果没有正文形式,则子句返回测试的值,我们不需要初始t。最后一个条款可以是:

((destructuring-bind (odd-zs even-zs)            ; *
       (split-list (rest (rest l)))
     (list (list* (first l) odd-zs)
           (list* (second l) even-zs))))))

* 我省略了t测试,因为如果cond子句没有正文表单,则返回测试的值。这在这里工作得很好。

总而言之,我们已将您的代码重新编写为

(defun split-list (l)
  (cond
    ((endp l) '(() ()))
    ((endp (rest l)) (list (list (first l)) '()))
    ((destructuring-bind (odd-zs even-zs) 
         (split-list (rest (rest l)))
       (list (list* (first l) odd-zs)
             (list* (second l) even-zs))))))
CL-USER> (split-list '(a b c 1 2 3))
((A C 2) (B 1 3))
CL-USER> (split-list '(a b c d 1 2 3))
((A C 1 3) (B D 2))

其他方法

我认为值得探索一些尾递归的方法,因为支持尾调用优化的实现可以将它们转换为循环。 Common Lisp中的尾递归函数通常也很容易转换为do循环,这些循环更有可能被实现为迭代。在这些解决方案中,我们将反向建立结果列表,然后在返回结果列表时将其反转。

一次递归一个元素

如果左右切片是可互换的

如果两个列表中的哪一个不是第一个并不重要,您可以使用以下内容:

(defun split-list (list &optional (odds '()) (evens '()))
  (if (endp list)
      (list (nreverse odds)
            (nreverse evens))
      (split-list (rest list)
                  evens
                  (list* (first list) odds))))
CL-USER> (split-list '(a b c 1 2 3))
((A C 2) (B 1 3))
CL-USER> (split-list '(a b c d 1 2 3))
((B D 2) (A C 1 3))

这实际上可以使用do循环非常简洁地编写,但这通常被视为迭代,而不是递归:

(defun split-list (list)
  (do ((list list (rest list))
       (odds '() evens)
       (evens '() (list* (first list) odds)))
      ((endp list) (list (nreverse odds) (nreverse evens)))))
如果他们不能互换

如果您始终需要包含原始列表的第一个元素的列表,那么您需要更多逻辑。一种可能性是:

(defun split-list (list &optional (odds '()) (evens '()) (swap nil))
  (if (endp list)
      (if swap 
          (list (nreverse evens)
                (nreverse odds))
          (list (nreverse odds)
                (nreverse evens)))
      (split-list (rest list)
                  evens
                  (list* (first list) odds)
                  (not swap))))
CL-USER> (split-list '(a b c 1 2 3))
((A C 2) (B 1 3))
CL-USER> (split-list '(a b c d 1 2 3))
((A C 1 3) (B D 2))

我认为(if swap … …)实际上有点难看。我们可以使用cond,以便我们可以获得多个表单(或ifprogn),并在返回之前交换oddsevens的值。我认为这实际上更容易阅读,但如果你的目标是一个纯粹的递归解决方案(学术任务?),那么你也可能避免变异,所以rotatef不会可用,并使用when只是为了得到一些副作用可能会不受欢迎。

(defun split-list (list &optional (odds '()) (evens '()) (swap nil))
  (cond
    ((endp list)
     (when swap (rotatef odds evens))
     (list (nreverse odds) (nreverse evens)))
    ((split-list (rest list)
                 evens
                 (list* (first list) odds)
                 (not swap)))))

这也适用于do

(defun split-list (list)
  (do ((list list (rest list))
       (odds '() evens)
       (evens '() (list* (first list) odds))
       (swap nil (not swap)))
      ((endp list) 
       (when swap (rotatef odds evens))
       (list (nreverse odds) (nreverse evens)))))

一次递归两个元素

另一种更直接的方法是按cddr(即(rest (rest …)))递减列表,并在每次递归时向左右子列表添加元素。我们需要小心谨慎,但是当输入中存在奇数个元素时,我们不会在nil列表中意外添加额外的right

(defun split-list (list &optional (left '()) (right '()))
  (if (endp list)
      (list (nreverse left)
            (nreverse right))
      (split-list (rest (rest list))
                  (list* (first list) left)
                  (if (endp (rest list))
                      right
                      (list* (second list) right)))))
CL-USER> (split-list '(a b c 1 2 3))
((A C 2) (B 1 3))
CL-USER> (split-list '(a b c d 1 2 3))
((A C 1 3) (B D 2))

再次,do版本:

(defun split-list (list)
  (do ((list list (rest (rest list)))
       (left '() (list* (first list) left))
       (right '() (if (endp (rest list)) right (list* (second list) right))))
      ((endp list) (list (nreverse left) (nreverse right)))))

答案 1 :(得分:2)

这就是我所拥有的:

(defun split-list (lst)
  (if lst
      (if (cddr lst)
          (let ((l (split-list (cddr lst))))
            (list
             (cons (car lst) (car l))
             (cons (cadr lst) (cadr l))))
        `((,(car lst)) ,(cdr lst)))
    '(nil nil)))

在阅读SICP后,我很少对递归感到困惑。 我强烈推荐它。

答案 2 :(得分:1)

这是我的看法,使用内部函数:

(defun split-list (lst)
  (labels ((sub (lst lst1 lst2 flip)
             (if lst
               (if flip
                 (sub (cdr lst) (cons (car lst) lst1) lst2 (not flip))
                 (sub (cdr lst) lst1 (cons (car lst) lst2) (not flip)))
               (list (reverse lst1) (reverse lst2)))))
    (sub lst nil nil t)))

答案 3 :(得分:1)

作为Common Lisp LOOP:

(defun split-list (list)

  "splits a list into ((0th, 2nd, ...) (1st, 3rd, ...))"

  (loop for l = list then (rest (rest l))

        until (null l)                     ; nothing to split

        collect (first l) into l1          ; first split result

        unless (null (rest l))
        collect (second l) into l2         ; second split result

        finally (return (list l1 l2))))

答案 4 :(得分:0)

使用内部尾递归函数以自上而下的方式构建列表(没有反转,loop代码可能编译成等效的东西),带有头哨技巧(为简单起见)。

(defun split-list (lst &aux (lst1 (list 1)) (lst2 (list 2)))
  (labels ((sub (lst p1 p2)
             (if lst 
                 (progn (rplacd p1 (list (car lst))) 
                        (sub (cdr lst) p2 (cdr p1)))
                 (list (cdr lst1) (cdr lst2)))))
    (sub lst lst1 lst2)))

答案 5 :(得分:0)

在Lisp中定义Flatten很有趣。但我永远不会使用它。 所以如果你认为"我可以用flatten来解决这个问题"这可能是因为你正试图解决错误的问题。

答案 6 :(得分:0)

(defun split-list (L)
    (if (endp L)
    '(nil nil)
    (let ((X (split-list (cdr L))))
        (list (cons (car L) (cadr X)) (car X))
)))