在方案中获取一棵树的有序叶子

问题描述

我正在做一个练习,以获取方案中嵌套列表的“叶子”(来自 SICP)。这是练习的输入输出

(define x (list (lis 1 2) (list 3 4)))
(fringe x)
; (1 2 3 4)

(fringe (list x x))
; (1 2 3 4 1 2 3 4)

现在,我为此提出了两个答案:一个是递归的,一个是迭代的。下面是我的两个实现:

(define (fr lst)
  (cond ((null? lst) '())
        ((not (pair? (car lst))) (cons (car lst) (fr (cdr lst))))
        (else (append (fr (car lst)) (fr (cdr lst))))))
(define (add-element-to-list lst elem)
  (if (null? lst)
      (list elem)
      (cons (car lst) (add-element-to-list (cdr lst) elem))))

(define (fringe lst)
 (define L '())
  (define (iter lst)
    (if (not (pair? (car lst)))
        (set! L (add-element-to-list L (car lst))) ; update the list if it's a leaf
        (iter (car lst)))                          ; otherwise recurse
    (if (not (null? (cdr lst))) (iter (cdr lst)))  ; and if we have a cdr,recurse on that
    L
    )
  (iter lst)
)
(fringe x)
(fr x)
(fr (list x x))
(fringe (list x x))
; (1 2 3 4)
; (1 2 3 4)
; (1 2 3 4 1 2 3 4)
; (1 2 3 4 1 2 3 4)
; OK

对我来说的问题是,这个练习让我一直在用大量的头脑来弄清楚(而且在我写这篇文章时我仍然很难“明白”)。以下是我遇到的一些问题,看看是否有任何关于如何在方案中处理这些问题的建议:

  1. 我最初认为有两种情况。正常/标量情况和嵌套情况。不过,好像真的是三个!有正常情况,嵌套情况,然后是空情况——内部列表也有空情况!是否有一个很好的通用模式或其他东西来解释 null 情况?这是经常出现的事情吗?
  2. 在迭代的情况下,为什么我必须在最后返回 L?为什么 (iter lst) 不直接返回(即,如果我删除L 函数底部的 Standalone-iter)。
  3. 最后,是否有一种“更干净”的方式来实现迭代案例?我的代码似乎太多了,可能还有待改进。

解决方法

出现三种情况的原因是您从其他语言导入了一些标量/向量区别:Scheme 没有它,也没有帮助。相反,列表是一个递归定义的对象:列表要么是空列表,要么是一对事物和一个列表。这意味着有两个区别,而不是一个:一个对象是一对,一个对象是空列表:

(define (lyst? o)
  (or (null? o)
      (and (pair? o) (lyst? (cdr o)))))

这与向量/标量的区别完全不同。我不知道你是从什么语言得到这个的,但是想想它的数学是如何工作的:向量是在一些标量场上定义的,并且没有向量也是一个标量。但是对于列表,一个不是一对的列表。停止考虑向量和标量:这不是考虑列表、对和空列表的有用方法。

迭代版本太可怕了:SICP 还没有引入 set! 是有原因的。

首先,它实际上并不是迭代的:就像网络上针对这个问题的大多数“迭代”解决方案一样,它看起来好像是这样,但事实并非如此。不是的原因是 iter 函数的骨架看起来像

  1. 如果等等
    • 递归对列表的第一个元素
    • 否则做别的事情
  2. 如果其他废话
    • 迭代列表的其余部分

关键是 (1) 和 (2) 总是发生,所以对列表的 car 的调用不是尾调用:它是一个成熟的递归调用。

话虽如此,您可以做得更好:做这种事情的绝对标准方法是使用累加器:

(define (fringe l)
  (define (fringe-loop thing accum)
    (cond
      ((null? thing)
       ;; we're at the end of the list or an element which is empty list
       accum)
      ((pair? thing)
       ;; we need to look at both the first of the list and the rest of the list
       ;; Note that the order is rest then first which means the accumulator
       ;; comes back in a good order
       (fringe-loop (car thing)
                    (fringe-loop (cdr thing) accum)))
      (else
       ;; not a list at all: collect this "atomic" thing
       (cons thing accum))))
  (fringe-loop l '()))

请注意,这是自下而上构建边缘(线性)列表,这是使用递归构建线性列表的自然方式。为了实现这一点,它稍微有点狡猾地排列它看待事物的方式,以便结果以正确的顺序出现。还要注意,这也不是迭代的:它是递归的,因为 (fringe-loop ... (fringe-loop ...)) 调用。但这一次要清楚得多。

它不是迭代的原因是搜索(树状,Lisp)列表的过程不是迭代的:这就是 SICP 所说的“递归过程”,因为(Lisp 的树状)列表在两者中递归定义他们的第一场和休息场。您所做的任何事情都不会使过程迭代。

但是您可以通过显式管理堆栈使代码在实现级别出现迭代,从而将其转换为尾递归版本。计算过程的性质并没有改变:

(define (fringe l)
  (define (fringe-loop thing accum stack)
    (cond
      ((null? thing)
       ;; ignore the () sentinel or () element
       (if (null? stack)
           ;; nothing more to do
           accum
           ;; continue with the thing most recently put aside
           (fringe-loop (car stack) accum (cdr stack))))
      ((pair? thing)
       ;; carry on to the right,remembering to look to the left later
       (fringe-loop (cdr thing) accum (cons (car thing) stack)))
      (else
       ;; we're going to collect this atomic thing but we also need 
       ;; to check the stack
       (if (null? stack)
           ;; we're done
           (cons thing accum)
           ;; collect this and continue with what was put aside
           (fringe-loop (car stack) (cons thing accum) (cdr stack))))))
  (fringe-loop l '() '()))

这是否值得取决于您认为递归调用的成本以及是否有任何递归限制。然而,显式管理接下来要执行的操作的一般技巧通常很有用,因为它可以更轻松地控制搜索顺序。

(当然,请注意,您可以对任何程序执行这样的技巧!)

,

这是关于类型。原则性开发遵循类型。然后它变得容易

Lisp 是一种无类型语言。这就像类固醇上的汇编程序。没有类型,对您可以编码的内容没有限制。

没有语言强制的类型,但仍然有类型,概念上。我们对类型进行编码,我们处理类型,我们为给定的规范生成值,即某些类型的值,以便更大系统的各个部分正确连接,我们编写的函数可以正常工作,等等。

我们要构建的 fringe 是什么?是“列表”吗?

什么是“列表”?是吗

(define (list? ls)
  (or (null? ls)
      (and (pair? ls)
           (list? (cdr ls)))))

是我们正在构建的fringe吗?为什么它没有说明事物的 car,我们是否要忽略 car 中的任何内容?为什么,不,当然不是。我们不会转换列表。我们实际上是在改造一棵

(define (tree? ls)
  (or (null? ls)
      (and (pair? ls)
           (tree? (car ls))
           (tree? (cdr ls)))))

仅能在其中包含 () 真的就足够了吗?可能不会。

是吗

(define (tree? ls)
  (or (null? ls)
      (not (pair? ls))   ;; (atom? ls) is what we mean
      (and ;; (pair? ls)
           (tree? (car ls))
           (tree? (cdr ls)))))

1 一个 tree? 显然是的,但让我们暂时把它放在一边。

我们在这里拥有的是一种结构化的、有原则的方法,可以将一段数据视为属于某种类型。或者如某些人所说,数据类型

那么我们只需遵循数据类型定义/谓词的相同骨架,编写一个函数,以某种特定方式处理所述类型的值(这是 Sterling 和 Shapiro 的 提倡的方法” Prolog 的艺术").

(define (tree-fringe ls)

那么,生产什么?它的叶子中的原子列表,就是这样。

  (cond 
      ((null? ls)

() 已经是 list?

                   ls)
      ((not (pair? ls))   ;; (atom? ls) is what we mean
           (handle-atom-case ls))

让我们暂时搁置这个。进入下一个案例,

      (else
           ;; (tree? (car ls))
           ;; (tree? (cdr ls))

carcdrls 都是 tree?。如何处理它们,我们已经知道了。这是

          (let ((a (tree-fringe (car ls)))
                (b (tree-fringe (cdr ls)))

我们如何处理这两部分?我们将它们拼凑在一起。先从左边去边缘,然后从右边去。简单:

            (append   a   b  )))))

(define (handle-atom-case ls)  
      ;; bad name,inline its code inside 
      ;; the `tree-fringe` later,when we have it

那么,append 在它的两个参数中期望什么类型的数据?再次list?

这就是我们必须为原子“树”生成的内容。这样的“树”是它自己的边缘。除了,

    ;;  tree:       1         2
    ;; fringe:    ( 1 )     ( 2 )

它必须是 list?。将原子数据(任何数据)转换为包含该数据的 list? 实际上非常简单。

      ........ )

这是我们必须在这里提出的唯一重要的事情,以找到解决方案。

递归是将东西分解成与整个事物相似的子部分,用我们正在尝试编写的相同过程转换那些,然后组合以某种简单明了的方式得出结果。

如果一个 tree? 包含两个较小的 trees?,那么,我们已经中了大奖——我们已经知道如何处理它们了!

当我们有结构化数据类型时,我们已经有办法将它们分开了。无论如何,它们都是这样定义的。


也许我稍后会回答你的第二个问题。