2.2.2 层次结构
序列的表示是用列表的方式,序列的元素本身也能是序列。例如,我们能够把
对象((1 2) 3 4) 以如下的方式组装起来。
(cons (list 1 2) (list 3 4))
作为有三个元素的列表,第一个元素是一个列表(1,2).的确,用解释器打印出结果,是
有建设性的。图2。5是以数对的形式表示了这个结构。
(3 4 )
|
!
((1 2 ) 3 4 ) ---> | @ | @ | ----------->| @ | @ |----->| @ | /|
| | |
! ! !
(1 2 ) ---------> | @ | @ |->| @ | /| | 3 | | 4 |
| |
! !
| 1 | | 2 |
图2.5 (cons (list 1 2) (list 3 4))形成的结构
对序列的元素仍是序列的另一种看法是树。序列的元素是树的分支,元素也就是它们本身的序列是子树。
图2.6显示了图2.5中的结构的树形。
((1 2) 3 4)
/ | \
/ | \
/ | \
(1 2) 3 4
/ \
/ \
1 2
图2.6 把图2.5中的列表结构显示为一棵树。
处理树形结构,递归是一种常用的工具,因为我们能够经常归纳树形操作,
到子树上的操作,再归纳到子树的子树等等。直到达到了树的叶子。作为一个例子,
比较2.2.1部分中的length程序和count-leaves程序,这个程序返回树的叶子的数量。
(define x (cons (list 1 2) (list 3 4)))
(length x)
3
(count-leaves x)
4
(list x x)
(((1 2) 3 4) ((1 2) 3 4))
(length (list x x))
2
(count-leaves (list x x))
8
为了实现count-leaves程序,重调用为了计算长度的递归计划。
一个列表的长度等于1加上列表的cdr的长度。
空列表的长度为0
count-leaves程序是相似的,对于空列表的值是相同的,也是0
* 空列表的叶子数为0
但是在递归的步骤中,当我们抽取出列表的car部分时,我们必须
认真对待car部分本身是一棵树,这种树的叶子需要我们计算的情况。
所以适合的递归步骤如下:
* 一棵树的叶子数等于树的car部分的叶子数加上树的cdr部分的叶子数
最后,通过cars,我们到达了实际的叶子结点,我们另一个基本的事实
* 一个叶子结点的叶子数为1。
为了辅助写针对树形结构的递归程序,Scheme提供了一个原生的判断式pair?,
它测试它的参数是否是一个数对,这是完整的程序如下:
(define (count-leaves x)
(cond ((null? x) 0)
((not (pair? x)) 1)
(else (+ (count-leaves (car x))
(count-leaves (cdr x))))
)
)
练习2.24
假定我们评估表达式(list 1 (list 2 (list 3 4))).给出解释器的打印结果。
相应的盒子与指针的结构,作为树的解释(正如在图2.6)
练习2.25
从下面的表达式中取出7,给出cars和cdrs的组合。
(1 3 (5 7) 9)
((7))
(1 (2 (3 (4 (5 (6 7))))))
练习2.26
假定我们定义 x和y是两个列表
(define x (list 1 2 3))
(define y (list 4 5 6))
解释器在评估以下的表达式时,返回的相应的打印结果是什么?
(append x y)
(cons x y)
(list x y)
练习2.27
修改你的在练习2.18中的reverse程序,生成一个deep-reverse程序,
这个接受一个列表为参数,返回一个列表作为它的结果。它的元素是倒序的。
所有的子列表也是深层次的倒序的。例如
(define x (list (list 1 2) (list 3 4)))
x
((1 2) (3 4))
(reverse x)
((3 4) (1 2))
(deep-reverse x)
((4 3) (2 1))
练习2.28
写一个程序fringe,实现的是,以一个树作为参数,树被表示为一个列表,返回
一个列表它的所有的元素是树的所有的叶子,以左到右的顺序的排列。例如:
(define x (list (list 1 2) (list 3 4)))
(fringe x)
(1 2 3 4)
(fringe (list x x))
(1 2 3 4 1 2 3 4)
练习2.29
一个二叉树由两个分支组成。一个是左分枝,另一个是右分支。
任何一个分支有一个定长的内容,它或者是一个权重值,或者是另一个
二叉树。我们能表示一个二叉树,使用复合的数据结构。通过使用两个分支组装它。
例如使用列表。
(define (make-mobile left right)
(list left right))
一个分支由一个长度值和一个结构组装而成。这个结构可能是一个简单的权值或者是另一个
树。
(define (make-branch length structure)
(list length structure))
任务如下;
a. 写相应的选择子left-branch ,right-branch, 它们返回一个二叉树的分支。
branch-length branch-structure 它们返回一个分支的组件。
b. 使用你的选择子,定义一个程序total-weight返回一个二叉树的总权值。
c. 如果一个二叉树的顶级左分叉的力矩等于顶级右分支的力矩,并且子二叉树也是平衡的,那么
这个二叉树是平衡的。 力矩的含义(左子树的长度乘以权值 或者是 右子树的长度乘以权值)
设计一个判断式来测试一个二叉树是否是平衡的。
d.假设我们改变了二叉树的表示,组装子写成了如下的内容。
(define (make-mobile left right)
(cons left right))
(define (make-branch length structure)
(cons length structure))
为了转到新的表示方法上,你需要修改你的程序有多少呢?
*在树上的映射操作
正如map对于处理序列是一个强有力的抽象,map结合递归在处理树时,也是一个强有力的抽象。
例如,scale-tree程序与2.2.1部分中的scale-list是相似的,参数是一个数据因子,
和一个叶子结点是数据的树。它返回一个相同的结构的树,它的叶子上的数据都乘以一个相同的因子。
对于程序scale-tree的递归计划与 count-leaves的内容相似。
(define (scale-tree tree factor)
(cond ((null? tree) null)
((not (pair? tree)) (* tree factor))
(else (cons (scale-tree (car tree) factor) (scale (cdr tree) factor)))
)
)
(scale-tree (list 1 (list 2 (list 3 4) 5) (list 6 7)) 10)
(10 (20 (30 40) 50) (60 70))
实现scale-tree的另一种方式是把树看作是子树的序列,然后使用map.
我们让map序列,缩放任何一个子树,返回一个结构的列表。
在基本的案例是,树是一个叶子,我们简单地乘以一个因子。
(define (scale-tree tree factor)
(map (lambda (sub-tree)
(if (pair? sub-tree)
(scale-tree sub-tree factor)
(* sub-tree factor)
)
) tree
)
)
许多的树的操作能用与序列操作和递归相似的结合来实现。
练习2.30
定义一个square-tree程序与练习2.21的square-list相似。
这个square-tree应用有如下的行为:
(square-tree (list 1 (list 2 (list 3 4) 5) (list 6 7)))
(1 (4 (9 16) 25) (36 49))
定义square-tree 直接定义不用任何高层次程序 或者使用map和递归。
练习2.31
抽象你的练习2.30的答案以产生程序tree-map,使得square-tree可以被定义成如下的样子。
(define (square-tree tree) (tree-map square tree))
练习2.32
我们能表示一个集合作为一个元素不重复的列表,我们也能表示集合的所有的子集合组成的集合
作为列表的列表。例如 如果集合是(1 2 3),那么所有的子集合的集合是(() (1) (2) (3) (1 2)
(1 3) (2 3) (1 2 3)). 完成一个程序的如下的定义,生成一个集合的所有的子集组成的集合。
再给出一个清晰的解释为什么它能工作?
(define (subsets s)
(if (null? s)
(list nil)
(let ((rest (subsets (cdr s))))
(append rest (map <??> rest))
)
)
)