使用LISP递归地将列表分成两个。

3
使用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)

我无法使预期输出的后半部分按正确顺序打印出来。

1
请注意,您的代码没有正确实现前两个cond子句(基本情况)。首先修复这些问题,然后再继续处理最后一个子句。 - C. K. Young
7个回答

7

你的代码

通常我们通过缩进来阅读Lisp代码,不需要全大写。因为我们通过缩进来阅读,所以不需要将右括号(或任何括号)放在单独的一行上。因此,您的代码应该这样格式化:

(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不为空(来自第一个情况),因此您可以检查剩下的l是否为空列表。在处理cons单元作为列表时,我发现使用firstrest更易读,所以我会将第二个子句写成:
((endp (rest l)) (list (list (first l)) '()))

处理递归情况

现在,最终情况是至少有两个元素。这意味着l看起来像 (x y . zs)。你需要做的是在zs上调用split-list来获取形式为(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)))))

实际上,有一些方法可以使代码更加简洁。我们可以使用解构绑定同时提取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…)))向下递归列表,并在每次递归时向左右子列表添加元素。不过,我们需要小心,避免在输入元素数为奇数时意外地向right列表中添加一个额外的nil

(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)))))

谢谢!不仅答案有帮助,而且解释也非常出色!非常感谢! - shle2821
嘿,方法列表和列表*之间的明确区别是什么?我进行了反向工程,并且有一个模糊的想法,但只是想知道将来的参考是什么确切的区别。谢谢! - shle2821
@shle2821 反向工程是学习的好方法,但是为了以后参考,最好检查实际参考资料,即 HyperSpec 中 LIST、LIST* 的条目。我假设我们知道 list 的作用。list* 是将两个参数进行列表 cons(list* a b) == (a . b),所以 (list* a '(b c)) == (a b c))),但也可以接受更多参数((list* a b c) == (a b . c),所以 (list* a b '(c d)) == (a b c d))),或者少于两个参数,但至少需要一个参数((list* a) == a)。 - Joshua Taylor
@shle2821 list* 实际上是更通用的工具,因为 (list ...) == (list* ... '())(list* args...) 可以轻松地实现为 (reduce 'cons args :from-end t) - Joshua Taylor

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之后,我很少对递归感到困惑了。我强烈推荐这本书。


这并不是很酷,因为length每次都必须遍历整个列表。 - Svante
1
过早优化。 首先确保函数能够正确工作。 然后,如果您关心性能并且知道您的 length 实现是 O(n),则进行优化。 - abo-abo
elisp甚至有数组 https://www.gnu.org/software/emacs/manual/html_node/elisp/Arrays.html 哇! - Display Name
还有哈希表。通过calc支持大数和有理数。这对你来说是个惊喜吗? - abo-abo
@abo-abo,嘿,我尝试对您的代码进行反向工程,并想知道在 ((,(car lst)) ,(cdr lst))) 上使用反引号的具体目的是什么。`<- 这个符号靠近 1 键。我尝试在网上搜索,但找不到清晰明了的解释。谢谢。 - shle2821
显示剩余3条评论

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)))

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))))

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)))

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

-1

在Lisp中定义flatten是很有趣的。但我从来没有用过它。 所以如果你认为"我可以用flatten解决这个问题",那可能是因为你正在尝试解决错误的问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接