【问题标题】:Recursive split-list function LISP递归拆分列表函数 LISP
【发布时间】:2016-01-22 08:01:11
【问题描述】:

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 个,3rd.. 位于返回列表的右侧,其余位于左侧。

例如:

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

应该交换顺序。我的逻辑中是否存在我遗漏的重大缺陷?我可以在不进行重大改动的情况下纠正这种情况吗?

【问题讨论】:

  • 编程问题/编码问题在这里是题外话,但可以在 Stack Overflow 上提问。
  • 在每个递归调用上调用length 不是一个好主意。它必须每次遍历列表l
  • 递归也不是一个好主意,因为它限制了可用堆栈大小的处理。
  • 一种可能有用的方法是一次将列表向下移动一个元素,跟踪“奇数或偶数位置”并将一个元素添加到一个或另一个列表中。
  • 递归作为一种概念工具在开发解决方案时帮助我们思考总是一个好主意。一旦制定了正确的代码,如果您的语言在处理递归方面受到限制,请重新编写它以使用其他方式。

标签: list recursion split lisp common-lisp


【解决方案1】:

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

  1. (endp (cdr L))添加案例
  2. 递归调用cddr L
  3. 之后,else case 将始终有两个新元素,一个用于 cons 到每个列表中;不再需要length 电话

【讨论】:

    【解决方案2】:

    首先,当您的cond 只有一个测试和默认的t 子句时,请改用if。 另外,您使用的是first,但cadrsecond 在您的上下文中比 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)))
    

    而且由于您可能必须在下一次作业中将列表拆分为 2 个以上的列表,因此使用更通用的版本,仍然带有循环:

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

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

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

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

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

        我第一次在user ed'kaold F# (IIRC) answer 中看到这个巧妙的技巧(IIRC);好几年前了。 (但实际上它似乎一直是in haskellwiki,因为它或多或少是永远的)。

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

        列表的头元素必须出现在第一个子组中。无需费力去安排它的发生,只要说出来,它就会发生,因为你这么说,完全靠它自己,因为 recursion  的魔力! p>

        (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,因为 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))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-11
          • 1970-01-01
          • 2016-02-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多