【问题标题】:Getting the ordered leaves of a tree in scheme在方案中获取树的有序叶子
【发布时间】:2021-05-26 00:14:44
【问题描述】:

我正在通过一个练习来获取方案中嵌套列表的“叶子”(来自 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. 我最初认为有两种情况。正常/标量情况和嵌套情况。然而,好像真的有三个!有普通的情况,嵌套的情况,然后是空的情况——内部列表也有空的情况!是否有一个很好的一般模式或什么来解释空情况?这是经常出现的事情吗?
  2. 在迭代的情况下,为什么我必须在最后返回L?为什么(iter lst) 不直接返回它(即,如果我删除了iter 函数底部的standalone-L)。
  3. 最后,是否有一种“更简洁”的方式来实现迭代案例?看来我的代码太多了,可能还有待改进。

【问题讨论】:

  • 不要使用set!。 SICP 尚未引入它是有充分理由的。

标签: scheme lisp racket sicp


【解决方案1】:

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

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

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

迭代版本太可怕了,想不通: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 的树状)列表是递归定义的他们的第一个和休息场。您无法做任何事情都不会使流程迭代。

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

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

这是否值得取决于您认为递归调用的成本以及是否存在任何递归限制。然而,明确管理下一步要做什么的一般技巧通常很有用,因为它可以更容易控制搜索顺序。

(当然,请注意,您可以对任何程序做这样的把戏!)

【讨论】:

  • 在 SICP 中,他们使用“递归过程”和“迭代过程”的措辞。使用显式堆栈不会使其成为“迭代过程”。由于该过程本质上是递归的,因此无法完成。
  • @Sylwester:是的,你是对的(我认为你的意思是“......本质上是递归的”)。我已经改变了措辞,希望它变得更好。谢谢。
  • @tfb 感谢您提供如此详尽的回答。几个问题。我正在使用#lang sicp。 (1) 我假设firstcar 的别名,而restcdr 的别名? (2) 括号有什么作用?例如,[[(null? thing) accum]。我以前从未见过帽子。最后,(3)为什么使用set! 很可怕?或者你只是说我在迭代版本中的代码很糟糕,set! 很好?
  • @David542: 是的,我在考虑处理列表时使用 first / rest,在考虑 conses 时使用 car / cdr,但它们是相同。对于[...],据我所知,它们与 Racket 中的括号相同,但仅相互匹配,人们(和 DrRacket)可能喜欢使用它们使代码“更具可读性”。尽管我强烈建议使用 first / restcar / cdr 区别,但我已经将两者都删掉了。至于set!,不,我对使用赋值而不是你的代码很粗鲁。我认为这是函数式编程人员的普遍看法。
  • 我认为如果它是 O(n) 那么它“迭代过程”。树节点枚举是 O(n) (其中 n 是节点数,但不是叶子)。我认为“固有递归”意味着双重递归(或更糟),就像天真的斐波那契一样。
【解决方案2】:

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

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

语言没有强制执行类型,但仍然存在 类型,在概念上。我们对类型进行编码,我们处理类型,我们为给定的规范生成值,即某些类型的值,这些值是为了让更大的系统能够正确连接,我们编写的函数可以正常协同工作,等等。

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

什么是“列表”?是吗

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

是我们正在构建的fringe 的内容吗?为什么它对car 的事情一无所知,我们是否要忽略car 中的任何内容?为什么,不,当然不是。我们不是在转换 list。我们实际上是在改造一棵

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

只有()s 真的就够了吗?应该不会吧。

是吗

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

1tree? 显然是,但我们先把它放在一边。

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

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

(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?s。如何处理它们,我们已经知道了。这是

          (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?,那么我们中了大奖——我们已经知道如何处理它们了!

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


也许我稍后会解决你的第二个问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多