【问题标题】:Flatten a list using common lisp使用 common lisp 展平列表
【发布时间】:2014-11-10 01:21:37
【问题描述】:

我正在阅读 Paul Graham 的《On Lisp》一书。在第 4 章“实用函数”中,他给出了对列表进行操作的小函数的示例,这对编写更大的程序很有帮助。

其中一个是flatten。给定任意级别的嵌套列表作为参数,flatten 将删除所有嵌套元素并将它们放在顶层。

以下是我实现扁平化的尝试:

(defun flatten (lst)
  (labels ((rflatten (lst1 acc)
                     (dolist (el lst1)
                       (if (listp el)
                         (rflatten el acc)
                         (push el acc)))
                     acc))
    (reverse (rflatten lst nil))))

但是上面的函数并不能正确地展平列表。

; returns (1) instead of (1 2)
(print (flatten '(1 (2))))
(1)

使用(1 (2)) 调用函数返回(1) 而不是(1 2)

我找不到我的扁平化实现有什么问题。是我使用的方式吗 labels?还是我使用dolist 宏的方式? dolist 宏总是返回 nil。但这并不重要,因为我使用累加器 acc 来存储扁平列表。

【问题讨论】:

  • lots of questions about flatten;这可能是其中之一的副本。如果没有,这些仍然可能会有所帮助。
  • @JoshuaTaylor 最疯狂的是,大多数带有flattenlisp 问题要么是Scheme 问题,要么是类似于但不完全像flatten 问题的请求。我知道 Will 在他们当中有一个很好的答案,但我猜只是回答比找到它更容易。此外,他在实施方面需要帮助,我没有评论他实际上是如何做到的,因为在 CL 标准中它看起来对我来说没问题。 (自我提醒:为好的答案添加书签)
  • 正如我所说,我在搜索中也没有找到重复项,但我希望找到 this 关于扁平化列表的问题的人也可能会找到 那些关于扁平化列表的问题也很有用。
  • 在亚历山大图书馆:flattencommon-lisp.net/project/alexandria/draft/alexandria.html#Conses“按顺序遍历树,将非空叶子收集到一个列表中。”。

标签: linked-list lisp common-lisp flatten on-lisp


【解决方案1】:

conses 构建结果的非递归代码,在comments 之后并从code 开始,user:Sylwester

(defun flatten (lst &optional back acc)
  (loop
     (cond 
        ((consp lst) (psetq lst (cdr lst)              ; parallel assignment
                           back (cons (car lst) back)))
        (back
                  (if (consp (car back))  
                    (psetq lst (cdar back)
                          back (cons (caar back) (cdr back)))
                    (psetq acc (if (car back) (cons (car back) acc) acc)
                          back (cdr back))))
        (t     
               (return acc)))))                        ; the result

它不漂亮,但它seems to work。并行赋值PSETQ用于模拟尾递归调用帧更新,无需担心精确排序。

实现与良好编码的过程相同的过程

(defun flatten2 (l z)
    (cond
        ((endp l) z)
        ((listp (car l)) (flatten2 (car l) (flatten2 (cdr l) z)))
        ((atom (car l)) (cons (car l) (flatten2 (cdr l) z)))))

(defun flatten (l)
   (flatten2 l nil))

隐式堆栈操作被解释为变量之间的列表结构操作。

【讨论】:

    【解决方案2】:

    其实你们很亲近。正如 Sylwester 提到的,问题在于 (push el acc) 仅修改 el 的本地绑定(其中每次调用 rflatten。正如 Rainer 提到的,它并不是传统意义上的累加器,所以我不会称它为 acc,而是 result。既然你'已经定义了一个本地函数,没有理由不在更广泛的范围内定义 result

    (defun flatten (lst)
      (let ((result '()))
        (labels ((rflatten (lst1)
                   (dolist (el lst1)
                     (if (listp el)
                       (rflatten el)
                       (push el result)))))
          (rflatten lst)
          (nreverse result))))
    

    实际上也有几种方法可以清理它。第一个是风格和品味的问题,但我会使用 &aux 变量来绑定 result,所以

    (defun flatten (lst &aux (result '()))
      ...)
    

    接下来是dolist 可以接受第三个参数,一种用于评估返回值的形式。这通常用于“推送创建列表,然后反转返回值”的成语,例如,

    (let ((result '()))
      (dolist (x list (nreverse result))
        ...
        (push ... result)))
    

    您不想在每个 dolist 之后进行反转,但您仍然可以从 dolistreturn result >,因此来自 rflatten。然后你可以简单地用 rflatten 的结果调用 nreverse

    (defun flatten (lst &aux (result '()))
      (labels ((rflatten (lst1)
                 (dolist (el lst1 result)
                   (if (listp el)
                     (rflatten el)
                     (push el result)))))
          (nreverse (rflatten lst))))
    

    【讨论】:

    • 不知道 &aux。谢谢。
    • 'acc' 并不是真正意义上的累加器(TCO 风格函数中的累加器),它基本上是结果值。因此,我将其命名为 flattened-list 或 result 或...
    • @JoshuaTaylor:那么完全尾递归版本会是什么样子 - 使用累加器? ;-)
    • @RainerJoswig this 怎么样?无论如何,您无法保证带有 CL 的 TCO,因此loop 版本会更好。我想知道那会是什么样子:-)
    • @ JoshuaTaylor 我已经添加了一个带有原始循环代码的答案,跟随 cmets 并从@Sylwester 上面评论的代码开始。没有 CPS,有很多 SETQ!
    【解决方案3】:

    push 更改范围内的符号绑定。因此,递归(rflatten el acc) 有它自己的acc,这是那里的结果,但你不对返回的结果做任何事情,它不会改变被调用者acc

    也许(setf acc (rflatten el acc)) 可以解决这个问题:

    (defun flatten (lst)
      (labels ((rflatten (lst1 acc)
                 (dolist (el lst1)
                   (if (listp el)
                       (setf acc (rflatten el acc))
                       (push el acc)))
                 acc))
        (reverse (rflatten lst nil))))
    

    【讨论】:

    • 修复了它。谢谢。
    • 然而,我觉得奇怪的是,common-lisp 中的函数调用会在其参数上创建一个范围。 Ruby 只是将对象的引用作为参数传递给被调用函数。
    • @ardsrk push 是一个宏(做 (macroexpand '(push a b))。在 ruby​​ 中,就像你有一个变量 acc 是本地的(在我们的例子中的参数),你做 @987654331 @。因此,如果您在此之前复制了 tmp = acc 之类的内容,您会注意到 acc 指向新的东西,但 tmp 指向它具有的旧值(对象)。我们没有更改对象,而是更改了引用. 我认为 Ruby 的范围规则与 CL 没有太大不同,只是它的范围比 CL 多。有一个 functional Ruby guide,CL 会更像那样。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多