SICP学习笔记(2.2.3)
SICP学习笔记(2.2.3)
周银辉
1,练习2.33
用累积的思想定义Map
先看看累积的定义:
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
其将序列的每一个元素利用操作op累积到initial上
再回过头看看map的定义:
(define (map proc sequence)
(if (null? sequence)
nil
(cons (proc (car items) (map proc (cdr items))))))
其将操作proc应用到序列的每一个元素,并将结果重新构造成一个列表(或者说累积到一个空表上)
那么将两者对比一下,很明显,要将结果累积成一个表,那么这个累积操作op实际上就是cons,所以完整代码如下:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (map2 p sequence)
(accumulate (lambda (x y) (cons (p x) y)) nil sequence))
;test
(define (proc a ) (+ a a))
(map2 proc (list 1 2 3))
下一个,用累积的思想定义append,比较简单:
(define (append2 seq1 seq2)
(accumulate cons seq2 seq1))
;test
(append2 (list 1 2 3) (list 4 5 6))
最后一个,用累积思想定义length,其是将长度值累计到一个初始值为0的整数上,那么很明显这个累积操作实际上就是算术加法,而每次累积的长度为1,所以代码:
(define (length2 sequence)
(accumulate (lambda (x y) (+ 1 y)) 0 sequence))
;test
(length2 (list 1 3 5 7 9))
2,练习2.34
先介绍一个这个看上去简单,但却能大大减少运算量的Horner规则:
假设有这么一个多项式
1 + 3x + 5x^3 + x^5
我们可以将它看成:
1 + 3x + 0x^2 + 5x^3 + 0x^4 + x^5
为了避免x^n这样的重复运行,我们可以多项式提取公因式
1 + x ( 3 + 0x + 5x^2 + 0x^3 + x^4)
同样的道理,对于( 3 + 0x + 5x^2 + 0x^3 + x^4)我们还可以继续提取,我们发现,每一次都提取一个x出来,原来幂指数为1的项目将变成常数项,并且该常数项最后将被累加到结果中,既然是连续累加,就可以很简单地利用我们的accumulate :
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (horner x sequence)
(accumulate (lambda (this-coeff higher-terms) (+ this-coeff (* x higher-terms)))
0
sequence))
;test
(horner 2 (list 1 3 0 5 0 1))
题外话,之所以说Horner规则减少了运算量,你对比一下采用和不采用Horner规则时乘法以及加法的运算次数就明白了,Horner规则将多项式求值的时间复杂度由O(n^2)降到了O(n)
3,练习2.35
一不小心写成了这个版本,发现里面没有map,呵呵呵:
(define (count-leaves t)
(accumulate (lambda (x y) (+ (if (not (pair? x)) 1 (count-leaves x)) y)) 0 t))
包含map的版本是这样的:
(define (count-leaves2 t)
(accumulate + 0 (map (lambda (x) (if (not (pair? x)) 1 (count-leaves2 x))) t)))
4,练习2.36
比较有意思的一道题,为了从序列 ((1 2 3) (4 5 6) (7 8 9) (10 11 12)) 得到 结果序列(22 26 30) 我们将前一个序列的写成下面的形式:
1 2 3
4 5 6
7 8 9
10 11 12
结果中的22为第一列各数之和,26为第二列之和,以此类推… 然后将每列之和在重新组成一个序列,看看题目给我们的提示:
(define (accumulate-n op init seqs)
(if (null? (car seqs))
nil
(cons (accumulate op init <??> )
(accumulate-n op init <??>))))
看到那个cons我们就应该明白(accumulate op init <??> )是求的第一列之和,而其中的<??> 肯定是一个序列,而该序列中的元素就是seqs中各个子序列的第一个元素,那么,要求得<??> 说表示的序列,问题就转化成:求出seqs中各个子序列的第一个元素所构成的序列,很自然地,要从一个序列求出另外一个序列,map就自然派上用场了:(map (lambda (x) (car x)) seqs)
而要求(accumulate-n op init <??>)中的问号部门嘛,明显这是在递归,我不知道如何用自然语言进行表达了,完整的程序在下面:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (accumulate-n op init seqs)
(if (null? (car seqs))
nil
(cons (accumulate op init (map (lambda (x) (car x)) seqs))
(accumulate-n op init (map (lambda (x) (cdr x)) seqs)))))
;test
(define theList '((1 2 3) (4 5 6) (7 8 9) (10 11 12)))
(accumulate-n + 0 theList)
5,练习2.37
矩阵乘以向量,等于将矩阵的每一行与向量做点积,由每次点积结果构成新向量:
(define (matrix-*-vector m v)
(map (lambda (x) (dot-product v x)) m))
矩阵翻转,等于将矩阵的每一列累积到一个空表上,由每次累积的表构成一个新矩阵:
(define (transpose m)
(accumulate-n cons '() m))
矩阵乘法,等于将第二个矩阵翻转后的矩阵与第一个矩阵的每一行做矩阵与向量乘法,由每次矩阵与向量乘法的结果构成一个新的矩阵:
(define (matrix-*-matrix m n)
(let ((cols (transpose n)))
(map (lambda (x) (matrix-*-vector cols x)) m)))
完整的代码如下:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (accumulate-n op init seqs)
(if (null? (car seqs))
nil
(cons (accumulate op init (map (lambda (x) (car x)) seqs))
(accumulate-n op init (map (lambda (x) (cdr x)) seqs)))))
(define (dot-product v w)
(accumulate + 0 (map * v w)))
(define (matrix-*-vector m v)
(map (lambda (x) (dot-product v x)) m))
(define (transpose m)
(accumulate-n cons '() m))
(define (matrix-*-matrix m n)
(let ((cols (transpose n)))
(map (lambda (x) (matrix-*-vector cols x)) m)))
;test
(define m '((1 2 3 4) (4 5 6 6) (6 7 8 9)))
(define n '((1 2) (3 4) (5 6) (7 8)))
(define v '( 1 2 3 4))
(matrix-*-vector m v)
(transpose m)
(matrix-*-matrix m n)
6,练习2.38
结果是这样的:
1 1/2
1/6
(1 (2 (3 ())))
(((() 1) 2) 3)
op满足的性质嘛:其左结合和右结合应该是相等的
7,练习2.39
(define nil '())
(define (fold-right op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (fold-right op initial (cdr sequence)))))
(define (fold-left op initial sequence)
(define (iter result rest)
(if (null? rest)
result
(iter (op result (car rest))
(cdr rest))))
(iter initial sequence))
(define (reverse2 sequence)
(fold-right (lambda (x y) (append y (list x))) nil sequence))
(define (reverse3 sequence)
(fold-left (lambda (x y) (cons y x)) nil sequence))
;test
(reverse2 '(1 2 3 4))
(reverse3 '(1 2 3 4))
8,嵌套映射
下面是嵌套映射题目——求小于等于N的,前两个元素之和为素数的所有三元组——的完整代码:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (enumerate-interval low high)
(if (> low high)
nil
(cons low (enumerate-interval (+ low 1) high))))
(define (flatmap proc seq)
(accumulate append nil (map proc seq)))
(define (smallest-divisor n)
(find-divisor n 2))
(define (square a) (* a a))
(define (divides? a b)
(= (remainder b a) 0))
(define (find-divisor n test-divisor)
(cond ((> (square test-divisor) n) n)
((divides? test-divisor n) test-divisor)
(else (find-divisor n (+ test-divisor 1)))))
(define (prime? n)
(= n (smallest-divisor n)))
(define (prime-sum? pair)
(prime? (+ (car pair) (cadr pair))))
(define (make-pair-sum pair)
(list (car pair) (cadr pair) (+ (car pair) (cadr pair))))
(define (filter predicate sequence)
(cond ((null? sequence) nil)
((predicate (car sequence))
(cons (car sequence)
(filter predicate (cdr sequence))))
(else (filter predicate (cdr sequence)))))
(define (prime-sum-pairs n)
(map make-pair-sum
(filter prime-sum?
(flatmap
(lambda (i)
(map (lambda (j) (list i j))
(enumerate-interval 1 (- i 1))))
(enumerate-interval 1 n)))))
;test
(prime-sum-pairs 100)
9,练习2.40
unique-pair其实就是上面flatmap部分:
(define (unique-pair n)
(flatmap (lambda (i)
(map (lambda (j) (list i j))
(enumerate-interval 1 (- i 1))))
(enumerate-interval 1 n)))
首先,上帝说要有由n产生出1到n的数列,于是便有了(enumerate-interval 1 n),然后上帝说要有数列中的每一个元素i说对应的比他小的数所构成的子序列,于是便有了(enumerate-interval 1 (- i 1)),然后上帝又说需要数列中的每一个元素i与子序列中的元素j构成序对,于是便有了(list i j),最后我说需要将这些序对组合成一个大列表,于是便有了flatmap
简化后的prime-sum-pairs就如下所示了:
(define (prime-sum-pairs n)
(map make-pair-sum
(filter prime-sum?
(unique-pair n))))
10,练习2.41
基本思路是这样的:先产生出所有的三元组,然后再利用过滤器把那些三个元素之和为指定值的过滤出来。
首先写一个过程,它能产生出所有的三元组,并且三元组中的元素小于等于指定的整数n,以及三元组中的元素是彼此不同的。我们知道,可以在二元组后面追加一个元素而得到一个三元组,所以我们可以借助练习2.40中的unique-pair来产生二元组,基于此产生三元组:
(define (triads n)
(flatmap (lambda (i)
(map (lambda (j) (cons i j)) (unique-pair (- i 1))))
(enumerate-interval 1 n)))
然后,我们需要定义过滤器以及过滤条件,过滤器在上面的“嵌套映射”中已经有现成的代码了,那么我们定义过滤条件吧,非常简单,三元组的各个元素之和是否为指定值:
(define (triads-sum? triads s)
(= s (+ (car triads) (cadr triads) (car (cddr triads)))))
然后我们题目所需要的函数大概就是这样的:
(define (F n s)
(filter triads-sum? (triads n)))
可是,细心的朋友可能会发现,不那么幸运的是,我们的过滤条件函数是有问题的:triads-sum需要接受两个参数,可看看我们的过滤器定义是这样的:
(define (filter predicate sequence)
(cond ((null? sequence) nil)
((predicate (car sequence))
(cons (car sequence)
(filter predicate (cdr sequence))))
(else (filter predicate (cdr sequence)))))
很明显,我们会发现过滤条件函数predicate 是只接受一个参数的。这时,你可以选择重新定义一个能让过来条件函数接受两个参数的过滤器。但,大可不必,我们只需要玩一点点Currying技巧,让triads-sum接受一个参数而非两个:
(define (triads-sum2? s)
(lambda (triads) (triads-sum? triads s)))
然后,题目要求的函数便是这样的(注意s的放置位置):
(define (F n s)
(filter (triads-sum2? s) (triads n)))
下面是完整的代码:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence) (accumulate op initial (cdr sequence)))))
(define (enumerate-interval low high)
(if (> low high)
nil
(cons low (enumerate-interval (+ low 1) high))))
(define (flatmap proc seq)
(accumulate append nil (map proc seq)))
(define (filter predicate sequence)
(cond ((null? sequence) nil)
((predicate (car sequence))
(cons (car sequence)
(filter predicate (cdr sequence))))
(else (filter predicate (cdr sequence)))))
(define (unique-pair n)
(flatmap (lambda (i)
(map (lambda (j) (list i j))
(enumerate-interval 1 (- i 1))))
(enumerate-interval 1 n)))
(define (triads n)
(flatmap (lambda (i)
(map (lambda (j) (cons i j)) (unique-pair (- i 1))))
(enumerate-interval 1 n)))
(define (triads-sum? triads s)
(= s (+ (car triads) (cadr triads) (car (cddr triads)))))
(define (triads-sum2? s)
(lambda (triads) (triads-sum? triads s)))
(define (F n s)
(filter (triads-sum2? s) (triads n)))
;test
(F 6 8)
11,练习2.42
基本思想SICP已经讲得比较清楚了,直接贴代码吧:
(define nil '())
(define (accumulate op initial sequence)
(if (null? sequence)
initial
(op (car sequence)
(accumulate op initial (cdr sequence)))))
(define (enumerate-interval from to)
(if (> from to)
nil
(cons from (enumerate-interval (+ from 1) to))))
(define (filter predicate sequence)
(cond ((null? sequence) nil)
((predicate (car sequence))
(cons (car sequence)
(filter predicate (cdr sequence))))
(else (filter predicate (cdr sequence)))))
(define (flatmap proc seq)
(accumulate append nil (map proc seq)))
(define empty-board nil)
(define (adjoin-position row k qs)
(cons (list k row) qs))
(define (safe? col positions)
(define (safe-simple? col row pos)
(let ((r (cadr pos))
(c (car pos)))
(let ((lu (- r c))
(ll (+ r c)))
(and (not (= lu (- row col)))
(not (= ll (+ row col)))
(not (= r row))))))
(define (all p ls)
(if (null? ls)
#t
(and (p (car ls)) (all p (cdr ls)))))
(let ((hd (car positions))
(tl (cdr positions)))
(all (lambda (pos) (safe-simple? col (cadr hd) pos)) tl)))
(define (queens board-size)
(define (queen-cols k)
(if (= k 0)
(list empty-board)
(filter
(lambda (positions) (safe? k positions))
(flatmap
(lambda (rest-of-queens)
(map (lambda (new-row)
(adjoin-position new-row k rest-of-queens))
(enumerate-interval 1 board-size)))
(queen-cols (- k 1))))))
(queen-cols board-size))
;test
(queens 8)
12,练习2.43
按照题目中的做法,的确慢得出奇,皇后数小于等于6还可以忍受,再多点就不行了。关键点我觉得在那个
map方法,在练习2.42中,map遍历的是1~n个自然数,而在2.43中交换了嵌套映射的顺序后,将对(queen-cols (- k 1))的遍历放到了嵌套映射的内部,而且queen-cols是递归的,这样时间复杂度就大大增加了,举一个简单的C#例子就更明白了:
using System.Collections.Generic;
namespace TempConsoleApplication
{
class Program
{
private static int counter;
static void Main(string[] args)
{
Console.WriteLine("Get method1 output:{0}", Method1());
Console.WriteLine("count : {0}", counter);
counter = 0;
Console.WriteLine("Get method2 output:{0}", Method2());
Console.WriteLine("count : {0}", counter);
Console.Read();
}
private static int Method1()
{
int output = 0;
for (int i = 0; i < 5; i++)
{
List<int> list = GetList(5);
foreach (int num in list)
{
output += num + i;
}
}
return output;
}
private static int Method2()
{
int output = 0;
List<int> list2 = GetList(5);
foreach (int num in list2)
{
for (int i = 0; i < 5; i++)
{
output += num + i;
}
}
return output;
}
private static List<int> GetList(int length)
{
List<int> list = new List<int>(length);
for (int i = 0; i < length; i++)
{
list.Add(GetNumber(i));
}
return list;
}
private static int GetNumber(int seed)
{
counter++;
int result = 1;
while (seed > 0)
{
result += GetNumber(seed - 1);
seed--;
}
return result;
}
}
}
运行结果:
Get method1 output:205
count : 155
Get method2 output:205
count : 31
我们可以发现,虽然两种方法得到的结果都是205,但前者GetNumber方法被调用了155次,后者却只调用了31次(发现了吗,155刚好是31的5倍),我们书上的这道题道理是一样的。
注:这是一篇读书笔记,所以其中的内容仅 属个人理解而不代表SICP的观点,并随着理解的深入其中 的内容可能会被修改