SICP学习笔记(2.3.3)
SICP学习笔记(2.3.3)
周银辉
1,集合作为未排序的表
这基本上相当于在使用for或foreach语句完成所有的操作,效率自然不高。下面是采用这种方式实现的集合:
;=========集合作为未排序的表=======
(define (element-of-set? x set)
(cond ((null? set) #f)
((equal? x (car set)) #t)
(else (element-of-set? x (cdr set)))))
(define (adjoin-set x set)
(if (element-of-set? x set)
set
(cons x set)))
(define (intersection-set set1 set2)
(cond ((or (null? set1) (null? set2)) '())
((element-of-set? (car set1) set2)
(cons (car set1) (intersection-set (cdr set1) set2)))
(else (intersection-set (cdr set1) set2))))
(define (union-set set1 set2)
(cond ((null? set1) set2)
((not (element-of-set? (car set1) set2))
(cons (car set1) (union-set (cdr set1) set2)))
(else (union-set (cdr set1) set2))))
;test
(define mySet1 '(1 3 4 2 5 7))
(define mySet2 '(1 5 8 6))
(element-of-set? 8 mySet1)
(adjoin-set 8 mySet1)
(intersection-set mySet1 mySet2)
(union-set mySet1 mySet2)
我们注意到,element-of-set?将对集合做线性遍历,所以其时间复杂度为O(n),那么自然地,adjoin-set 也为O(n),而intersection-set与union-set对于set1中的每一个元素都将检查其是否在set2中,那么其计算步数大概是n*m, 其中,n和m假设为set1和set2的元素个数,所以其时间复杂度为O(n^2)
2,集合作为未排序的表,但集合为“多重集”
所谓多重集,就是集合中的元素可以重复,重复的最大次数称为集合的“重数”,明显地,我们向多重集中添加元素以及求集合的并集的时候是不需要检查元素是否已经存在的,少了检查也就少了遍历,这会提高效率。(但求交集时仍是需要检查的),下面是代码:
;=========集合作为未排序的表(多重集)=======
;about Multiset : http://en.wikipedia.org/wiki/Multiset
(define (element-of-set? x set)
(cond ((null? set) #f)
((equal? x (car set)) #t)
(else (element-of-set? x (cdr set)))))
(define (adjoin-set x set)
(cons x set))
(define (intersection-set set1 set2)
(cond ((or (null? set1) (null? set2)) '())
((element-of-set? (car set1) set2)
(cons (car set1) (intersection-set (cdr set1) set2)))
(else (intersection-set (cdr set1) set2))))
(define (union-set set1 set2)
(cond ((null? set1) set2)
(else (cons (car set1) (union-set (cdr set1) set2)))))
;test
(define mySet1 '(1 3 1 2 3 7))
(define mySet2 '(1 5 5 6))
(element-of-set? 2 mySet1)
(adjoin-set 2 mySet1)
(intersection-set mySet1 mySet2)
(union-set mySet1 mySet2)
3,集合作为排序的表(升序)
排序的好处是可以消除不必要的遍历,比如检查一个元素是否在集合中,其比该集合的第一个元素都小,那么它肯定不在几何中,没必要再和集合的其他元素比较了。下面是代码:
;=========集合作为排序的表(升序)=======
(define (element-of-set? x set)
(cond ((or (null? set) (< x (car set) )) #f)
((equal? x (car set)) #t)
(else (element-of-set? x (cdr set)))))
(define (adjoin-set x set)
(cond ((< x (car set)) (cons x set))
((> x (car set)) (cons (car set) (adjoin-set x (cdr set))))
(else set)))
(define (intersection-set set1 set2)
(if (or (null? set1) (null? set2))
'()
(let ((x1 (car set1)) (x2 (car set2)))
(cond ((= x1 x2)
(cons x1
(intersection-set (cdr set1)
(cdr set2))))
((< x1 x2)
(intersection-set (cdr set1) set2))
((< x2 x1)
(intersection-set set1 (cdr set2)))))))
(define (union-set set1 set2)
(cond ((null? set1) set2)
((null? set2) set1)
(else
(let ((x1 (car set1)) (x2 (car set2)))
(cond ((= x1 x2)
(cons x1
(union-set (cdr set1) (cdr set2))))
((< x1 x2)
(cons x1
(union-set (cdr set1) set2)))
((< x2 x1)
(cons x2
(union-set (cdr set2) set1))))))))
;test
(define mySet1 '(1 3 5 7 9))
(define mySet2 '(1 2 4 6 8))
(element-of-set? 9 mySet1)
(adjoin-set 8 mySet1)
(adjoin-set 5 mySet1)
(intersection-set mySet1 mySet2)
(union-set mySet1 mySet2)
简单解释一下:
对于element-of-set?,如果集合为空或者待检测元素x比集合的第一个元素都小,那么x肯定不再集合中;如果x与集合的第一个元素相等,那么x肯定在集合中(废话~~);否则,就检测x是否在(cdr set)中,即检测其是否在除去第一个元素后的子集中。这虽然也是一个线性遍历,复杂度为O(n),当相比于不排序而言,其不必遍历完集合的所有元素,所以工作量会小很多。说得更直白一点就是:对于未排序的集合,当你遍历该集合时,如果在某一时刻未找到一个元素,你只能说“到目前为止还没找到”,并不代表将来找不到,所以你只有遍历完整个集合后才能下结论说“的确没有”,而排序后的集合,当你达到某个上限时,你便可以断定“即便找下去也不会有的”,所以干脆不找了。
对于adjoin-set,向集合追加元素x时,如果元素x比集合的第一个元素小,那么直接将x放置到集合的最前面;如果x比集合的第一个元素大,那么其应该追加到(cdr set)这个子集中(别搞丢了第一个元素);如果x和集合的第一个元素相等,那么不追加,直接将集合返回。
对于intersection-set,求集合set1和集合set2的交集,如果set1为空或set2为空,那么交集为空;设set1与set2的第一个元素分别为x1和x2,如果x1等于x2,那么将x1或者x2纳为交集元素,然后继续求(cdr set1)和(cdr set2)的交集;如果x1小于x2,那么很明显x1被淘汰了,所以继续拿(cdr set1)和set2做交集,同理,如果x2小于x1,那么x2被淘汰。
对于union-set,求集合set1和集合set2的并集,原理和求交集差不多,只不过不是较小的值被淘汰,而是刚好相反,较小值被纳如并集中。
很明显,上面的求交集和并集的操作的计算步骤大概是(m+n),其中m,n是set1和set2的长度,所以这两个操作的时间复杂度是O(n)
4, 集合作为二叉树
其实不能一概而论地说这种方式好还是不好,因为这取决于具体的代码实现,因为在遍历二叉树的时候很可能一不小心就写出了一个“双递归”算法,导致遍历速度超慢,但毕竟SICP上提到的二叉树是“平衡二叉树”,是经过“精心排序”的,利用得好的话,则效率颇高,先贴代码,再解释:
;=========集合作为二叉树=======
(define (entry tree) (car tree))
(define (left-branch tree) (cadr tree))
(define (right-branch tree) (caddr tree))
(define (make-tree entry left right)
(list entry left right))
(define (element-of-tree? x set)
(cond ((null? set) #f)
((= x (entry set)) #t)
((< x (entry set))
(element-of-tree? x (left-branch set)))
(else
(element-of-tree? x (rigth-branch set)))))
(define (adjoin-tree x set)
(cond ((null? set)
(make-tree x '() '()))
((= x (entry set))
set)
((< x (entry set))
(make-tree (entry set)
(adjoin-tree x (left-branch set))
(right-branch set)))
(else
(make-tree (entry set)
(left-branch set)
(adjoin-tree x (right-branch set))))))
;intersection-set 来自于“集合作为排序的表(升序)”
(define (intersection-set set1 set2)
(if (or (null? set1) (null? set2))
'()
(let ((x1 (car set1)) (x2 (car set2)))
(cond ((= x1 x2)
(cons x1
(intersection-set (cdr set1)
(cdr set2))))
((< x1 x2)
(intersection-set (cdr set1) set2))
((< x2 x1)
(intersection-set set1 (cdr set2)))))))
; union-set 来自于“集合作为排序的表(升序)”
(define (union-set set1 set2)
(cond ((null? set1) set2)
((null? set2) set1)
(else
(let ((x1 (car set1)) (x2 (car set2)))
(cond ((= x1 x2)
(cons x1
(union-set (cdr set1) (cdr set2))))
((< x1 x2)
(cons x1
(union-set (cdr set1) set2)))
((< x2 x1)
(cons x2
(union-set (cdr set2) set1))))))))
(define (union-tree tree1 tree2)
(list->tree (union-set (tree->list-2 tree1) (tree->list-2 tree2))))
(define (intersection-tree tree1 tree2)
(list->tree (intersection-set (tree->list-2 tree1) (tree->list-2 tree2))))
;这个算法是一个“树形递归”
(define (tree->list-1 tree)
(if (null? tree)
'()
(append (tree->list-1 (left-branch tree))
(cons (entry tree)
(tree->list-1 (right-branch tree))))))
;这个算法是一个“尾递归”,也就是说其是“迭代的”
(define (tree->list-2 tree)
(define (copy-to-list tree result-list)
(if (null? tree)
result-list
(copy-to-list (left-branch tree)
(cons (entry tree)
(copy-to-list (right-branch tree)
result-list)))))
(copy-to-list tree '()))
(define (list->tree elements)
(car (partial-tree elements (length elements))))
(define (partial-tree elts n)
(if (= n 0)
(cons '() elts)
(let ((left-size (quotient (- n 1) 2)))
(let ((left-result (partial-tree elts left-size)))
(let ((left-tree (car left-result))
(non-left-elts (cdr left-result))
(right-size (- n (+ left-size 1))))
(let ((this-entry (car non-left-elts))
(right-result (partial-tree (cdr non-left-elts)
right-size)))
(let ((right-tree (car right-result))
(remaining-elts (cdr right-result)))
(cons (make-tree this-entry left-tree right-tree)
remaining-elts))))))))
;-------------------test----------------------------
(define myTree1
(make-tree 7
(make-tree 3
(make-tree 1 '() '())
(make-tree 5 '() '()))
(make-tree 9
'()
(make-tree 11 '() '()))))
(define myTree2
(make-tree 3
(make-tree 1 '() '())
(make-tree 7
(make-tree 5 '() '())
(make-tree 9
'()
(make-tree 11 '() '())))))
(define myTree3
(make-tree 5
(make-tree 3
(make-tree 1 '() '())
'())
(make-tree 9
(make-tree 7 '() '())
(make-tree 11 '() '()))))
(tree->list-1 myTree1)
(tree->list-2 myTree1)
(tree->list-1 myTree2)
(tree->list-2 myTree2)
(tree->list-1 myTree3)
(tree->list-2 myTree3)
(list->tree '(1 3 5 7 9 11))
(list->tree '(1 3 5 7 8))
(define myTree4 (list->tree '(1 3 5 7 9 11)))
(define myTree5 (list->tree '(1 3 5 7 8)))
(tree->list-2 (intersection-tree myTree4 myTree5))
(union-tree myTree4 myTree5)
element-of-tree?函数和adjoin-tree函数与“集合作为排序的表(升序)”的算法是一样的,因为这里的平衡二叉树,实际上也是排序的,求并集和交际则是将平衡二叉树转化成排序的表,然后利用排序表来计算。
tree->list-1和tree->list-2的不同之处在于前者是一个双递归,后者是尾递归(迭代的),所以两者的效率不在一个数量级上,关于这个嘛,在SICP的1.2.2节讲“树形递归”的时候就已经说过了。
5,练习2.59~2.65
答案上文找
6,练习2.66
这个和 element-of-tree?函数的算法几乎是一样的:
(define (lookup given-key set-of-records)
(if (null? set-of-records)
false
(let ((r (entry set-of-records)))
(let ((k (key r)))
(cond ((= given-key k) r)
((< given-key k)
(lookup given-key (left-branch set-of-records)))
((> given-key k)
(lookup given-key (right-branch set-of-records))))))))
注:这是一篇读书笔记,所以其中的内容仅 属个人理解而不代表SICP的观点,并随着理解的深入其中 的内容可能会被修改