递归拆分列表函数LISP

时间:2015-10-22 21:25:39

标签: list recursion split lisp common-lisp

拆分列表函数采用一个列表并返回两个列表的列表,这两个列表由输入的交替元素组成。我写了以下内容:

(defun split-list (L)
  (cond
      ((endp L) (list NIL  NIL))
      (t (let ((X (split-list (cdr L))))
         (cond
             ((oddp (length L))
              (list (cons (first L) (first X)) (cadr X)))
             (t (list (first X) (cons (first L) (cadr X)))))))))

输出与奇数列表的预期一致,第一个列表包括第1个,第3个,第5个等元素,第二个部分包括第2个,第4个,第6个等。但是,偶数列表,第1个,第2个,3 ..位于返回列表的右侧,其余位于左侧。

例如:

(SPLIT-LIST '(a b c 1 2 3))
(SPLIT-LIST RETURNED ((b 1 3) (a c 2))

应该交换订单。我的逻辑中存在一个我缺失的重大缺陷吗?我可以在不做出重大改动的情况下纠正这种情况吗?

4 个答案:

答案 0 :(得分:4)

是的,您可以在不做任何重大修改的情况下解决问题。

  1. (endp (cdr L))
  2. 添加案例
  3. cddr L
  4. 上进行递归调用
  5. 之后,else case将始终有两个新元素,一个元素可以包含在每个列表中;不再需要length来电

答案 1 :(得分:2)

首先,当您cond只有一个测试和默认t子句时,请改用if。  此外,您使用的是first,但cadr; second在您的上下文中比cadr更具可读性。

现在,订单交换为偶数列表。尝试执行逐步执行。手动可能有点乏味,但这对于了解发生的情况很有用。我个人更喜欢使用trace宏:(trace split-list)。然后,运行您的示例:

   0: (split-list (a b c 1 2 3))
    1: (split-list (b c 1 2 3))
      2: (split-list (c 1 2 3))
        3: (split-list (1 2 3))
          4: (split-list (2 3))
            5: (split-list (3))
              6: (split-list nil)
              6: split-list returned (nil nil)
            5: split-list returned ((3) nil)
          4: split-list returned ((3) (2))
        3: split-list returned ((1 3) (2))
      2: split-list returned ((1 3) (c 2))
    1: split-list returned ((b 1 3) (c 2))
  0: split-list returned ((b 1 3) (a c 2))

不清楚?尝试使用奇怪的列表:

   0: (split-list (a b c 1 2))
    1: (split-list (b c 1 2))
      2: (split-list (c 1 2))
        3: (split-list (1 2))
          4: (split-list (2))
            5: (split-list nil)
            5: split-list returned (nil nil)
          4: split-list returned ((2) nil)
        3: split-list returned ((2) (1))
      2: split-list returned ((c 2) (1))
    1: split-list returned ((c 2) (b 1))
  0: split-list returned ((a c 2) (b 1))

您似乎始终将最里面的结果存储在左侧列表中!

可能的递归实现大致如下:

(defun split-list (list)
  (if (endp list)
      '(nil nil)
      (destructuring-bind (left right) (split-list (cddr list))
        (list (cons (first list) left)
              (if (second list)
                  (cons (second list) right)
                  right)))))

但是这可以将堆栈吹得足够大。对于您的信息,这是一个简单的非递归方法loop

(defun split-list (list)
    (loop for (a b) on list by #'cddr
          collect a into left
          when b 
            collect b into right
          finally (return (list left right)))

由于您可能需要在下一个作业中将列表拆分为两个以上的列表,这是一个更通用的版本,仍然带有循环:

(defun split-list (list &optional (n 2))
  (loop with a = (make-array n :initial-element nil)
        for e in list
        for c = 0 then (mod (1+ c) n)
        do (push e (aref a c))
        finally (return (map 'list #'nreverse a))))

(split-list '(a b c d e f g) 3)
=> ((a d g) (b e) (c f))

如果你想玩圆形清单,你也可以尝试这个,它适用于任何序列,而不仅仅是列表:

(defun split-n (sequence &optional (n 2))
  (let* ((ring (make-list n :initial-element nil))
         (head ring)
         (last (last ring)))
    (setf (cdr last) ring)
    (map nil
         (lambda (u)
           (push u (first ring))
           (pop ring))
         sequence)
    (setf (cdr last) nil)
    (map-into head #'nreverse head)))

如果您打算调查其工作原理,请先评估(setf *print-circle* t)

答案 2 :(得分:1)

递归列表处理中一个非常常见的习惯用法是以相反的顺序构建结果列表,然后在返回它们之前将它们反转。这个成语在这里很有用。您的任务的本质是返回两个列表的列表,第一个列表应包含偶数索引元素,其中第二个列表应包含奇数索引元素。这是我如何解决这个问题(如果我是递归地做)。我们的想法是维护偶数元素和奇数元素的列表,以及一个布尔值,指示我们是在整个列表中的偶数位置还是奇数位置。在每次递归时,我们在“evens”列表中添加一个元素,因为当前列表的当前索引始终为零,这始终是偶数。诀窍是在每次递归调用时,我们交换平均值和赔率,我们否定布尔值。最后,我们使用该布尔值来确定哪些列表是“真正的”均衡和赔率列表。

(defun split-list (list &optional (evens '()) (odds '()) (evenp t))
  "Returns a list of two lists, the even indexed elements from LIST
and the odd indexed elements LIST."
  (if (endp list)
      ;; If we're at the end of the list, then it's time to reverse
      ;; the two lists that we've been building up.  Then, if we ended
      ;; at an even position, we can simply return (EVENS ODDS), but
      ;; if we ended at an odd position, we return (ODDS EVENS).
      (let ((odds (nreverse odds))
            (evens (nreverse evens)))
        (if evenp
            (list evens odds)
            (list odds evens)))
      ;; If we're not at the end of the list, then we add the first
      ;; element of LIST to EVENS, but in the recursive call, we swap
      ;; the position of EVENS and ODDS, and we flip the EVENP bit.
      (split-list (rest list)
                  odds
                  (list* (first list) evens)
                  (not evenp))))

CL-USER> (split-list '())
(NIL NIL)
CL-USER> (split-list '(1))
((1) NIL)
CL-USER> (split-list '(1 2))
((1) (2))
CL-USER> (split-list '(1 2 3))
((1 3) (2))
CL-USER> (split-list '(1 2 3 4))
((1 3) (2 4))
CL-USER> (split-list '(1 2 3 4 5 6 7 8 9 10))
((1 3 5 7 9) (2 4 6 8 10))

答案 3 :(得分:1)

递归总是一个好主意,作为一个概念工具,在开发问题的解决方案时帮助我们思考。一旦制定了正确的代码, iff 您的语言在递归处理方面受到限制,请重新编写以使用其他方法。

Scheme派生语言的现代实现(Scheme是一种Lisp,对吗?),Racket具有无限递归,在堆上实现调用堆栈。因此,递归算法的递归代码非常好。

正确/ 宁静简单第一,效率更高!

满足您要求的简单解决方案是(在"伪代码" Haskell中)

    foldr (\x [a,b] -> [x:b,a]) [[],[]] 

我首先在用户 ed' ka (IIRC)的旧F#(IIRC)答案中看到了这个巧妙的技巧;天哪,差不多几年了。或许这是一个评论。

在Scheme中以直接递归样式编码,它是

(define (split xs)
  (cond
    ((null? xs) (list '() '()))
    ((split (cdr xs)) => (lambda (acc) 
         (list (cons (car xs) (cadr acc))   ; always in the first subgroup!
               (car acc))))))               

列表的head元素必须出现在第一个子组中。没有必要努力安排自己的事情发生,只是说出来,而这只是因为你这么说,所有这一切,因为递归的魔力

(split '(a b c 1 2 3))
(split '(a b c 1 2))

; '((a c 2) (b 1 3))
; '((a c 2) (b 1))

附注:我决定再次使用 if ,而不是 cond ,因为 {{1 } 的条款本身没有说明它的激活条件 - 我们必须计算,在所有事情上,知道哪个是哪个。使用 if 它很简单,并且它正好在条款的开头。

很容易修改它以产生例如一个三个 -way分割,带

cond