问题描述
我正在做一个练习,以获取方案中嵌套列表的“叶子”(来自 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
对我来说的问题是,这个练习让我一直在用大量的头脑来弄清楚(而且在我写这篇文章时我仍然很难“明白”)。以下是我遇到的一些问题,看看是否有任何关于如何在方案中处理这些问题的建议:
- 我最初认为有两种情况。正常/标量情况和嵌套情况。不过,好像真的是三个!有正常情况,嵌套情况,然后是空情况——内部列表也有空情况!是否有一个很好的通用模式或其他东西来解释 null 情况?这是经常出现的事情吗?
- 在迭代的情况下,为什么我必须在最后返回
L
?为什么(iter lst)
不直接返回(即,如果我删除了L
函数底部的 Standalone-iter
)。 - 最后,是否有一种“更干净”的方式来实现迭代案例?我的代码似乎太多了,可能还有待改进。
解决方法
出现三种情况的原因是您从其他语言导入了一些标量/向量区别:Scheme 没有它,也没有帮助。相反,列表是一个递归定义的对象:列表要么是空列表,要么是一对事物和一个列表。这意味着有两个区别,而不是一个:一个对象是一对,一个对象是空列表:
(define (lyst? o)
(or (null? o)
(and (pair? o) (lyst? (cdr o)))))
这与向量/标量的区别完全不同。我不知道你是从什么语言得到这个的,但是想想它的数学是如何工作的:向量是在一些标量场上定义的,并且没有向量也是一个标量。但是对于列表,是一个不是一对的列表。停止考虑向量和标量:这不是考虑列表、对和空列表的有用方法。
迭代版本太可怕了:SICP 还没有引入 set!
是有原因的。
首先,它实际上并不是迭代的:就像网络上针对这个问题的大多数“迭代”解决方案一样,它看起来好像是这样,但事实并非如此。不是的原因是 iter
函数的骨架看起来像
- 如果等等
- 递归对列表的第一个元素
- 否则做别的事情
- 如果其他废话
- 迭代列表的其余部分
关键是 (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))
car
的 cdr
和 ls
都是 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?
,那么,我们已经中了大奖——我们已经知道如何处理它们了!
当我们有结构化数据类型时,我们已经有办法将它们分开了。无论如何,它们都是这样定义的。
也许我稍后会回答你的第二个问题。