Recursion Lisp-拆分递归

Recursion Lisp-拆分递归,recursion,lisp,common-lisp,Recursion,Lisp,Common Lisp,我试图创建一个递归函数,根据所需元素的数量将一个列表拆分为两个列表 例: (一三五七九)(一三五)(七九) (一三五七九)(一三五七九)零) (拆分0'(135 79))(零(135 79)) 我的代码如下: (defun split (e L) (cond ((eql e 0) '(() L)) ((> e 0) (cons (car L) (car (split (- e 1) (cdr L)))))))) 我找不到一种方法来连接第一个列表元素并返回第二个列表。

我试图创建一个递归函数,根据所需元素的数量将一个列表拆分为两个列表

例:

(一三五七九)(一三五)(七九)

(一三五七九)(一三五七九)零)

(拆分0'(135 79))(零(135 79))

我的代码如下:

(defun split (e L) 
  (cond ((eql e 0) '(() L))
        ((> e 0) (cons (car L) (car (split (- e 1) (cdr L))))))))

我找不到一种方法来连接第一个列表元素并返回第二个列表。

记住:split返回一个包含两个列表的列表

(defun split (e L) 
  (cond ((eql e 0)
         '(() L))       ; you want to call the function LIST
                        ;  so that the value of L is in the list,
                        ;  and not the symbol L itself

        ((> e 0)

         ; now you want to return a list of two lists.
         ; thus it probably is a good idea to call the function LIST

         ; the first sublist is made of the first element of L
         ;  and the first sublist of the result of SPLIT

         ; the second sublist is made of the second sublist
         ;  of the result of SPLIT

         (cons (car L)
               (car (split (- e 1)
                           (cdr L)))))))

好吧,让我们试着推导我们应该做的递归

(split 0 l) = (list () l)
这就是我们的基本情况。现在我们知道了

(split 1 (cons a b)) = (list (list a) b)
但是我们想了想,我们正在建立左边的第一个参数,建立列表的方法是使用
CONS
,所以我们写下来

(split 1 (cons a b)) = (list (cons a ()) b)
然后我们想一想什么是
(分割0 l)
,我们可以写下
n>=1

(split n+1 (cons a b)) = (list (cons a l1) l2) where (split n b) = (list l1 l2)
让我们用Lisp写下来:

(defun split (n list)
  (ecase (signum n)
    (0 (list nil list))
    (1 (if (cdr list)
           (destructuring-bind (left right) (split (1- n) (cdr list))
             (list (cons (car list) left) right))
           (list nil nil)))))
最惯用的解决方案是:

(defun split (n list)
  (etypecase n
    ((eql 0) (list nil list))
    (unsigned-integer
      (loop repeat n for (x . r) on list
        collect x into left
        finally (return (list left r))))))

尾部递归解决方案

(defun split (n l &optional (acc-l '()))
  (cond ((null l) (list (reverse acc-l) ()))
        ((>= 0 n) (list (reverse acc-l) l))
        (t (split (1- n) (cdr l) (cons (car l) acc-l)))))
改进版

(defun split (n l &optional (acc-l '()))
  (cond ((null l) (list (reverse acc-l) ()))
        ((>= 0 n) (list (reverse acc-l) l))
        (t (split (1- n) (cdr l) (cons (car l) acc-l)))))
(在此版本中,确保
acc-l
位于开头
”()
):

测试它:

(split 3 '(1 2 3 4 5 6 7))
;; returns: ((1 2 3) (4 5 6 7))
(split 0 '(1 2 3 4 5 6 7))
;; returns: (NIL (1 2 3 4 5 6 7))
(split 7 '(1 2 3 4 5 6 7))
;; returns ((1 2 3 4 5 6 7) NIL)
(split 9 '(1 2 3 4 5 6 7))
;; returns ((1 2 3 4 5 6 7) NIL)
(split -3 '(1 2 3 4 5 6 7))
;; returns (NIL (1 2 3 4 5 6 7))
在改进的版本中,通过使用
标签
(一种
let
,允许定义局部函数,但允许它们自己调用,因此允许递归局部函数),递归函数被更深一层(一种封装)

我是如何找到解决方案的:

不知何故,结果中的第一个列表必须是从
l
开始依次考虑一个又一个元素的结果。但是,考虑在现有列表的开头而不是结尾添加元素。 因此,依次考虑列表中的car将导致相反的顺序。 因此,很明显,在最后一步中,当返回第一个列表时,它必须是
reverse
d。第二个列表只是上一步的
(cdr l)
,因此可以在返回结果时添加到上一步的结果中

因此我认为,最好将第一个列表累加到(
acc-l
)-累加器主要是尾部递归函数的参数列表中的最后一个元素,即第一个列表的组件。我称之为acc-l-累加器列表

在编写递归函数时,首先要从琐碎的情况开始
cond
部分。如果输入是一个数字和一个列表,那么最简单的情况——以及递归的最后一步——就是当

  • 列表为空
    (等于l'())
    -->
    (空l)
  • 数字是零-->
    (=n0)
    -实际上是
    (zeropn)
    。但后来我将其更改为
    (>=n0)
    ,以捕获输入为负数的情况
  • (因此,递归
    cond
    部分在其条件中通常具有
    null
    zerop
    。)

    当列表
    l
    为空时,必须返回两个列表,而第二个列表为空列表,第一个列表为反向
    d
    acc-l
    。 您必须使用
    (list)
    构建它们,因为
    list
    参数在返回前不久会得到评估(与
    quote
    =
    '(…)
    相反,在最后一步中无法对结果进行评估。)

    当n为零时(稍后:当n为负时),则只需返回l作为第二个列表以及迄今为止第一个列表的累积值,但顺序相反

    在所有其他情况下,
    (t…
    ,列表
    l
    中的car被
    cons
    分配到迄今为止累积的列表中(对于第一个列表):
    (cons(car l)acc-l)
    ,我将其作为累加器列表(
    acc-l
    )分配到
    split
    ,列表的其余部分作为此调用中的新列表
    (cdrl)
    (1-n)
    。递归调用中的这种递减对于递归函数定义非常典型

    这样,我们就涵盖了递归中一个步骤的所有可能性。 这使得递归如此强大:在一个步骤中征服所有的可能性——然后您定义了如何处理几乎无限多的情况

    非尾部递归解决方案 (受Dan Robertson的解决方案启发-谢谢Dan!尤其是他用
    解构绑定的解决方案,我很喜欢。)

    和一个仅具有非常基本功能的解决方案(仅
    null
    list
    =
    let
    t
    cons
    car
    cdr
    cadr


    解构绑定
    -这是非常优雅的解决方法!而且
    (如果(cdr列表)
    部分也很整洁。啊,但我也看到:
    (ecase(signum n)(0第一个案例处理)(1秒案例处理))
    可以简化为
    (如果(nullp n)第一个案例处理第二个案例处理)
    。或者
    (cond((nullp n)第一个案例处理(t第二个案例处理)
    哪一个可读性更好,不是吗?与其使用
    etypecase
    ,不如使用
    (if(zeropn)
    ?我宁愿使用
    check type
    assert
    单独检查类型。这很懒。我在手机上键入了它,而
    etypecase
    让我将验证和特殊情况结合起来。finally子句是针对副作用进行评估的。如果您想返回那里的内容,需要使用
    return
    return-从
    @Svante是的,你是对的。我怀疑那里也可能有一个错误。
    (拆分3'(1 2 3 4 5 6 7))
    在CLISP中返回
    (1.2)
    。第二部分的内容不正确,我认为。。。
    (defun split (n l)
      (cond ((null l) (list '() '()))
            ((>= 0 n) (list '() l))
            (t (destructuring-bind (left right) (split (1- n) (cdr l))
                        (list (cons (car l) left) right)))))
    
    (defun split (n l)
      (cond ((null l) (list '() '()))
            ((>= 0 n) (list '() l))
            (t (let ((res (split (1- n) (cdr l))))
                  (let ((left-list (car res))
                        (right-list (cadr res)))
                    (list (cons (car l) left-list) right-list))))))