List 递归拆分列表函数LISP

List 递归拆分列表函数LISP,list,recursion,split,lisp,common-lisp,List,Recursion,Split,Lisp,Common Lisp,split list函数获取一个列表,并返回由输入的交替元素组成的两个列表的列表。我写了以下内容: (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)) (c

split list函数获取一个列表,并返回由输入的交替元素组成的两个列表的列表。我写了以下内容:

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

应该交换订单。我的逻辑中是否有一个我遗漏的重大缺陷?我可以在不进行重大修改的情况下纠正此情况吗?

是的,您可以在不进行重大修改的情况下纠正此问题

  • (endp(cdr L))
  • 对cddr L执行递归调用
  • 之后,else案例将始终有两个新元素,每个列表上有一个到cons;不再需要
    length
    调用

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

    现在,顺序被交换为偶数列表。尝试一步一步地执行。手工操作可能有点乏味,但这有助于理解发生了什么。我个人更喜欢使用
    跟踪
    宏:
    (跟踪拆分列表)
    。然后,运行您的示例:

       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)))))
    
    但这可能会破坏堆栈以获得足够大的输入。供您参考,这里有一个使用
    循环的简单非递归方法:

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

    递归列表处理中非常常见的习惯用法之一是以相反的顺序建立结果列表,然后在返回结果列表之前将其反转。这个成语在这里很有用。任务的本质是返回一个包含两个列表的列表,第一个列表应包含偶数索引元素,第二个列表应包含奇数索引元素。下面是我处理这个问题的方法(如果我是递归的)。其思想是维护一个偶数元素和奇数元素的列表,以及一个布尔值,指示我们在整个列表中处于偶数还是奇数位置。在每个递归中,我们向“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))))
    


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

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

    正确/宁静先简单,后效率

    满足您需求的简单解决方案是(在Haskell的可执行“伪代码”中)

    我第一次看到这个巧妙的把戏是在一本书中(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元素必须出现在第一个子组中。不必费劲去安排它的发生,只要说出来,它就发生了,因为你这么说,一切都是因为它本身,因为递归的魔力&hairsp

    旁注:我决定不再使用
    if
    ,而优先使用
    cond
    ,因为
    if
    子句本身并没有说明它的激活条件——我们必须计算所有事情,才能知道哪个是哪个。使用
    cond
    很简单,它就在子句的开头


    修改此项很容易,例如,生成三向拆分,带有

    (define (split3 xs)
      (cond
        ((null? xs) (list '() '() '()))
        (else (apply
               (lambda (a b c)             ; Scheme-style destructuring
                 (list (cons (car xs) c)   ; rotate right:
                       a                   ;   xs's 2nd elt to appear in the 2nd group!
                       b))                 ;   head element of (cdr xs) is in `a`
               (split3 (cdr xs))))))       ; the recursive result
    
    (split3 '(a b c 1 2 3))
    (split3 '(a b c 1 2))
    
    ; '((a 1) (b 2) (c 3))
    ; '((a 1) (b 2) (c))
    

    编程问题/编码问题在这里是离题的,但可以在堆栈溢出时询问。在每次递归调用时调用
    length
    不是一个好主意。它必须每次遍历列表
    l
    。递归也不是一个好主意,因为它通过可用堆栈大小限制处理。一种可能有用的方法是一次向下移动列表中的一个元素,跟踪“奇数或偶数位置”将元素添加到一个列表或另一个列表中。递归作为概念工具,在开发解决方案时帮助我们思考始终是一个好主意。一旦制定了正确的代码,如果您的语言在处理递归方面受到限制,请重新编写以使用其他方法。
    (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))))))               
    
    (split '(a b c 1 2 3))
    (split '(a b c 1 2))
    
    ; '((a c 2) (b 1 3))
    ; '((a c 2) (b 1))
    
    (define (split3 xs)
      (cond
        ((null? xs) (list '() '() '()))
        (else (apply
               (lambda (a b c)             ; Scheme-style destructuring
                 (list (cons (car xs) c)   ; rotate right:
                       a                   ;   xs's 2nd elt to appear in the 2nd group!
                       b))                 ;   head element of (cdr xs) is in `a`
               (split3 (cdr xs))))))       ; the recursive result
    
    (split3 '(a b c 1 2 3))
    (split3 '(a b c 1 2))
    
    ; '((a 1) (b 2) (c 3))
    ; '((a 1) (b 2) (c))