【笔记】初读《SICP》:递归和迭代

1. 前言

递归【recursion】和迭代【iteration】是计算过程生成的两种“形状”
深入的理解对于我们理解程序的计算至关重要
(能够通过观察想象计算所产生的结果)
注:这篇文章是参考《SICP》后而写的,里面可能会出现一些lisp代码,不过后面附加了等价的c语言实现

2. 线性递归

首先,我们考虑计算一个数 n n n的阶乘: n ! = n ∗ ( n − 1 ) ∗ ( n − 2 ) ∗ . . . ∗ 2 ∗ 1 n! = n*(n-1)*(n-2)*...*2*1 n!=n(n1)(n2)...21
这是最普通也是最直观的计算方法了,但是我们从简单的式子中也可以发现一些不一样的想法:从 n n n往后的所有项 ( n − 1 ) ∗ ( n − 2 ) ∗ . . . ∗ 2 ∗ 1 (n-1)*(n-2)*...*2*1 (n1)(n2)...21可以写成 ( n − 1 ) ! (n-1)! (n1)!
那么,我们就可以将原式改写为
n ! = n ∗ [ ( n − 1 ) ∗ ( n − 2 ) ∗ . . . ∗ 2 ∗ 1 ] = n ∗ ( n − 1 ) ! n! = n*[(n-1)*(n-2)*...*2*1] = n*(n-1)! n!=n[(n1)(n2)...21]=n(n1)!
可以看见,原来的问题( n ! n! n!)被我们转化成了一个更小规模的问题( ( n − 1 ) ! (n-1)! (n1)!),这便是递归的思想,这样不断地进行下去,最终一定会收敛到一个最简单的情况【base case】,即 1 ! = 1 1! = 1 1!=1

代码的实现

lisp

(define (factorial n)
	(if (= n 1)
	1
	(* n (factorial (- n 1)))))

c

int factorial (int n){
	if(n == 1)	return 1;
	return n * factorial(n-1);
}

观察上面的代码,两种语言实现的递归算法都非常简单直观,十分符合我们之前的分析思路,接下来看一下一个实现阶乘的递归算法具体的运行轨迹
(以计算 6 ! 6! 6!为例)

运行轨迹跟踪

在这里插入图片描述

3. 迭代

现在,我们换一个视角来思考阶乘 n n n的计算
n ! = 1 ∗ 2 ∗ . . . ∗ ( n − 2 ) ∗ ( n − 1 ) ∗ n n! = 1*2*...*(n-2)*(n-1)*n n!=12...(n2)(n1)n
首先设置一个累加器product,初始值为1;
再设置一个计数器counter,初始值也为1,然后将按照如下的规则进行反复执行:
(1)product := product * counter
(2)counter := counter + 1
直到counter的值超过了给定的n,我们停止执行,这时候的product的值就自然为 n ! n! n!

代码的实现

lisp

(define (factorial n)
	(fact-iter 1 1 n))
(define (fact-iter product counter max-count)
	(if (> counter max-count)
	product
	(fact-iter (* counter product)
				(+ counter 1)
				max-count)))

可能有人会说,这明明调用了fact-iter函数自身啊,怎么会是迭代呢?这其实是lisp语言的特性,这里的尾递归【tail-recursion】实现在c语言中可以等价地写成迭代的形式

c

int factorial(int max-count){
	int product,counter;
	product = 1;
	counter = 1;
	while(counter <= max-count){
		product = product * counter;
		counter = counter + 1;
	}
	return product;
}

运行轨迹跟踪

在这里插入图片描述

3. 两者的区别

3.1

首先,我们考虑线性递归的运行轨迹
不难看出,递归的过程是一个不断扩张【expansion】和收缩【contraction】的过程。在扩张过程中,不断地延迟乘法操作;在收缩过程中,则不断执行乘法操作(这也正是“递”和“归”的体现)
由于递归过程中会有一些“隐藏”的信息被解释器(或编译器)暂存,因此随着递归的深度越深,被保存的信息也就越多。而递归时栈是由系统管理的,如果递归深度太深,就会导致爆栈问题

3.2

接下来考虑迭代的运行轨迹
迭代的过程和递归有很大不同,它并没有扩张和收缩的现象
在迭代中,我们要做的就是跟踪所有的中间变量的值,不断更新它们,直到满足停止条件后,我们将得到结果,迭代过程也会结束
从另一种角度来说,这些中间变量完全地提供了计算过程的状态描述(如阶乘计算中,product和counter的更新体现了当前的累加器和计数器的变化),而整个运行过程是在一个固定的空间中完成的

4. 树形递归

不同于线性递归,另一种递归的形式叫做树形递归【tree recursion】
一个最常见的树形递归的例子就是斐波那契数列
F i b o n a c c i ( n ) = { 0 , n = 0 1 , n = 1 F i b o n a c c i ( n − 1 ) + F i b o n a c c i ( n − 2 ) , n > 1 Fibonacci(n) = \begin{cases}0, &n=0 \cr 1, &n = 1 \cr Fibonacci(n-1) + Fibonacci(n-2),&n>1\end{cases} Fibonacci(n)=0,1,Fibonacci(n1)+Fibonacci(n2),n=0n=1n>1

代码的实现

lisp

(define (fib n)
	(cond ((= n 0) 0)
		((= n 1) 1)
		(else (+ (fib (- n 1))
				 (fib (- n 2))))))

c

int fib(int n){
	if(n == 0)	return 0;
	else if(n == 1)	 return 1;
	else 
		return fib(n-1) + fib(n-2);
}

运行轨迹跟踪

在这里插入图片描述
从图中我们可以清楚地看见,计算 f i b   5 fib\ 5 fib 5的过程中
重复计算:2次 f i b   3 fib\ 3 fib 3、3次 f i b   2 fib\ 2 fib 2
这样的重复计算必然会带来高额的开销,特别是n非常大的时候
虽然树形递归在时间效率上不那么尽如人意,但却很容易阐述其思想并理解

一种优化树形递归的方法就是利用一张表来暂存计算的中间值,这样每次要用的时候就可以查表获取,而不用每次都再重复计算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值