SICP学习笔记(2.2.2)
周银辉
1,包含序对的序对
作者在讲解这种结构时主张将其看做“树”,序对的序对结构看做二叉树,列表的列表自然看做普通树“多叉树”了。但要注意到与我们在数据结构中对树的理解有所不同,看看下面这幅图就明白了:
在数据结构中,我们见上面的树看着是由一个根节点和三棵子树构成;在SICP中,按照序对的形式,上面的树看做两棵子树而成(相当于cons的car和cdr两部分)
2,练习2.24
运行结果 (1 (2 (3 4))) ,指针的结构图就省略了哈
3,练习2.25
做这道题只需要注意一点:对序对(cons a b)求car和cdr得到的是 a 与 b, 对列表 (list a b) 求car和cdr得到的是 a 与 (b),所以如果想要得到列表中的b,则需要写成 (car (cdr (list a b)))
解答如下:
(define a '(1 3 (5 7) 9))
(define b '((7)))
(define c '(1 (2 (3 (4 (5 (6 7)))))))
(car (cdr (car (cdr (cdr a)))))
(car (car b))
(car (cdr(car (cdr(car (cdr(car (cdr(car (cdr (car (cdr c))))))))))))
4,练习2.26
(define x (list 1 2 3))
(define y (list 4 5 6))
先说简单的:
(append x y) 表示将列表y的元素追加到列表x的元素后面构成新列表,所以结果为 (1 2 3 4 5 6)
再说“玄妙”点的:
假设我们给出这样的解答:
(cons x y) 表示一个序对,其中序对的第一个元素是列表x,第二个元素是列表y,所以这个序对就应该表示成 ((1 2 3) (4 5 6))
(list x y) 表示一个列表,其中列表的第一个元素是列表x,第二个元素是列表y,所以这个列表就应该写成 ((1 2 3) (4 5 6))
whoops!照我们这样分析,cons 和 list 得到的结果是一样的(虽然序对的确可以看做是列表的特例)。很抱歉,代码的实际运行结果告诉我们,关于 (cons x y) 的解答我们写错了。
我们平时生活中对序列的定义是“它由两个元素组成,序对为这两个元素保留了两个位置,然后我们将第一个元素放在序对的第一个位置,第二个元素放在序对的第二个位置”。这是这种定义让我们得出了上面的解答。可是Scheme可不这么理解,Scheme对序列的定义可能是“它由两个元素组成,如果将car应用到该序对,则可以取出第一个元素;如果将cdr应用到该序对,则可以取出第二个元素(关于这个观点,可以阅读SICP2.1.3:数据意味着什么)”。而cons用于构造序对时时按这样的规则执行的:(cons a b) 则将a插入到列表b的第一个位置,特别地,当b不是列表时,则在构成序对时在a和b之间加一个小数点。所以, (cons 5 6) 得到的结果是 (5 . 6) , (cons 5 '(6))得到的结果是 (5 6) , 所以本题中(cons x y) 的结果应该为 ((1 2 3) 4 5 6)
关于这些基本操作,建议看看王咏刚dr5rs试译稿
5,练习2.27
先回忆一下我们的Reverse函数,它可以翻转一个列表,但不会翻转其内部列表(为了避免与系统内置的Reverse发生冲突,我改名成Reverse2了):
(define (reverse2 theList)
(if(null? theList)
'()
(append (reverse2 (cdr theList)) (list (car theList)))))
我们将上面的稍作修改,变可以成为题目的deepReverse: 当列表长度为1时,将列表作普通翻转,所以:
(define (deepReverse theList)
(if (null? theList)
'()
(if (= (depth theList) 1)
(list (reverse2 theList))
(append (deepReverse (cdr theList)) (deepReverse (car theList))))))
其中,depth是求列表深度的函数,比如 (1 2)的深度为1 , ( 1 2 (3))的深度为2:
(define (depth theList)
(if (not (pair? theList))
0
(max (+ 1 (depth (car theList)))
(depth (cdr theList)))))
后来我发现reverse2 可以重构一下,让其变成非递归的:
(define (reverse2 theList)
(if(null? theList)
'()
(list (car (cdr theList)) (car theList))))
那么同样的,我们的deepReverse也可以重构一下,变成:
(define (deepReverse theList)
(if (null? theList)
'()
(if (= (depth theList) 1)
(list (list (car (cdr theList)) (car theList)))
(append (deepReverse (cdr theList)) (deepReverse (car theList))))))
其中 (list (list (car (cdr theList)) (car theList))) 这句话只所以如此晦涩,是因为它用到了练习2.25的思想:如果想要得到列表中的b,则需要写成 (car (cdr (list a b)))
6,练习2.28
求列表的叶子节点所组成的列表:如果列表的深度小于2,那么其叶子节点的列表也就是该列表本身,否则它等于car的叶子和cdr的叶子的并集:
(define (fringe theList)
(if (< (depth theList) 2)
theList
(append (fringe (car theList)) (fringe (cdr theList)))))
7,练习2.29
第一个小问比较简单,用car 和cdr就可以搞定:
(define (make-mobile left right)
(list left right))
(define (make-branch length structure)
(list length structure))
(define (left-branch mobile)
(car mobile))
(define (right-branch mobile)
(car (cdr mobile)))
(define (branch-length branch)
(car branch))
(define (branch-structure branch)
(car (cdr branch)))
第二个小问,活动题的总重量,等于其左分支的重量加上右分支的重量;而分支重量嘛,如果它上面挂的不是活动体而是重物的话,则直接是这个重物的重量,否则等于它上面所挂活体的重量:
(define (branch-weight branch)
(if (not (pair? (branch-structure branch)))
(branch-structure branch)
(branch-weight (branch-structure branch))))
(define (total-weight mobile)
(+ (branch-weight (left-branch mobile)) (branch-weight (right-branch mobile))))
第三个小问,活动体要平衡则必须满足三个条件: 左分支的力矩等于右分支力矩,右分支平衡, 左分支平衡;而关于分支平衡嘛,如果该分支上挂的不是活动体而是重物的话,则其一定平衡(这跟说“如果它是瞎子的话它一定看不见”一样),否则其是否平衡则取决于上面所挂的活动体是否平衡:
(define (branch-moment branch)
(if (not (pair? (branch-structure branch)))
(* (branch-length branch) (branch-structure branch))
(* (branch-length branch) (branch-weight (branch-structure branch)))))
(define (branch-balanceable? branch)
(if (not (pair? (branch-structure branch)))
#t
(mobile-balanceable? (branch-structure branch))))
(define (mobile-balanceable? mobile)
(and (= (branch-moment (left-branch mobile))
(branch-moment (right-branch mobile)))
(branch-balanceable? (left-branch mobile))
(branch-balanceable? (right-branch mobile))))
8,练习2.30
比较简单,直接贴答案了:
(define (square-tree tree)
(cond ((null? tree) tree)
((not (pair? tree)) (square tree))
(else (cons (square-tree (car tree))
(square-tree (cdr tree))))))
或者
(define (square-tree tree)
(map (lambda (subtree)
(if (pair? subtree)
(square-tree subtree)
(square subtree)))
tree))
9,练习2.31
(define (tree-map proc tree)10,练习2.32
(cond ((null? tree) tree)
((not (pair? tree)) (proc tree))
(else (cons (tree-map proc (car tree))
(tree-map proc (cdr tree))))))
这个题目比较有意思:集合的子集所构成的集合等于除去第一个元素外的剩余元素构成的集合的子集的集合并上一定包含第一个元素的集合所构成的集合。
(我快被自己写的这句话搞晕了,不过它却是正确的)
要想很简单地理解这道题,我们得复习一下1.2.2节中的“换零钱问题”:我们说, 换取零钱的总方式数 等于 不包含第一种面值的硬币时的方式数 加上 一定包含第一种面值时的方式数 ,这是因为任何一种方式中那么包含第一种面值,要么不包含,所以可以以这个为依据将换零钱方式归为两类,这两类方式之和则是总的方式数。
用数学公式表示一下:
S(L, a) = S(M, a) + P(L, a)
其中S表示求换零钱方式的函数,P表示必须包含第一种面值时换零钱方式的函数,L表示全体面值的集合(或列表) , M表示除去第一中面值后由集合剩余元素构造的集合,a表示需要被换成零钱的现金总额。我们会发现在P(L, a) 中,“将数量为a的现金换成必须包含第一种面值时换零钱方式数” 和 “将数量为(a-d)的现金换成有可能包含第一种面值时的换零钱方式数”是相等的,其中d是第一种面值(的数量值),因为我们可以将前者的包含了第一种面值的方式中都拿出一颗第一种面值的硬币。
那么上面的公式就可以改写成:
S(L, a) = S(M, a) + S(L, a-d)
这个公式也就是SICP课本中在换零钱问题上所提到的那个计算方法。
至此,换零钱问题你应该完全理解了。
OK,回到集合问题上来。
与换零钱问题很相似:在所有的子集中,这些子集那么包含第一个元素,要么不包含,他们的并集构成了我们所要的答案。在看题目给出的代码片段似乎在暗示我们所说的观点是正确的:
(define (subsets s)
(if (null? s)
(list '())
(let ((rest (subsets (cdr s))))
(append rest (map <???> rest)))))
代码片段中的 rest 也就是我们上面所说的“不包含第一个元素”的子集所构成的集合。而 (append rest (map <???> rest))) 要片段要在其后追加一些东西,稍稍想以下,集合的append似乎和“并集”比较神似,对,其就是要将那些“一定包含了第一个元素”的子集所构成的集合追加到后面。
那么如何求“一定包含了第一个元素”的子集所构成的集合呢?
又回到换换零钱问题上进行类比,我们将一定包含了第一种面值的方式中取了一颗第一种面值的硬币出来,其就变成了可能包含第一种面值,那么反过来,在任何一种方式中,如果我们放入一颗第一种面值的硬币,那么它一定包含了第一种面值(废话)。呵呵呵,应用到这个问题上就是:在 rest 中的每一个集合中 追加原集合的第一个元素,那么它就“一定包含了第一个元素”,这就是我们的解法,所以,代码如下:
(define (subsets s)
(if (null? s)
(list '())
(let ((rest (subsets (cdr s))))
(append rest (map
(lambda (x)
(let ((firstOne (list (car s))))
(append firstOne x)))
rest)))))
;test
(define theSet '(1 2 3))
(subsets theSet)
注:这是一篇读书笔记,所以其中的内容仅 属个人理解而不代表SICP的观点,并随着理解的深入其中 的内容可能会被修改