1、递归程序设计技术,学习程序设计需要注意规律性的东西,算法与数据结构补充,本章内容,递归与循环 递归函数的执行过程 递归函数效率,循环与递归,循环程序 用于描述需要重复进行计算 高级语言里,也常见用递归来实现重复的计算。 递归recursion, recursive algorithm 函数或过程调用自身 C语言允许递归,可以在函数内调用自身,常常使程序更简单清晰。,1. 阶乘和乘幂,例:定义计算整数阶乘的函数 12(n - 1)n 本例中,乘的次数依赖于n,计算所需的次数定义时无法确定。 这是一种典型循环情况 计算“次数”依赖某些参数的值。,程序,long fact1(long n) long
2、 fac, i;for (fac = 1, i = 1; i = n; i+)fac *= i;return fac; ,阶乘函数的精确定义,是一种递归定义的形式 要解决规模为n的问题,要先解决规模为n-1的子问题,依此类推。 如果高级语言允许递归定义函数,就可以直接翻译为程序。 C允许递归定义 在函数定义内直接或间接地调用被定义函数本身。,写成递归函数,long fact (long n) return n = 0 ? 1 : n * fact(n-1); long fact(long n) if (n = 1)return 1; return n * fact(n - 1); ,long
3、fact(1) if (1 = 1)return 1; return 1 * fact(1 - 1); ,long fact(2) if (2 = 1)return 1; return 2 * fact(2 - 1); ,long fact(3) if (3 = 1)return 1; return 3 * fact(3 - 1); ,main() printf(“%d”, fact(3); ,蓝线:函数调用线路 黄线:函数内部执行路线 红线:函数执行结束返回主调函数的路线,long fact(long n) if (n = 1)return 1;return n * fact(n - 1);
4、 ,递归与计算过程,包含递归的程序产生的计算过程和性质更复杂,能完成很复杂的工作。 递归调用只有一个调用表达式或语句,但是可能要许多步才能完成。 实际参数的不同,会实际产生的递归调用次数(步数)也会有很大的不同。 递归程序理解的理解比较困难 递归的函数定义需要有条件语句去控制递归过程的最终结束 直接给出结果的时候,递归结束; 把对较复杂情况的计算归结为对更简单情况的计算,需要进行递归处理。,递归和循环,基本运算、关系判断、条件表达式,加函数定义和递归定义构成了一个(理论上)“足够强的”的程序语言。 循环程序可以改成递归实现 递归程序也可以改成循环实现,。,2. Fibonacci序列,计算与时
5、间,定义,Fibonacci(斐波那契)序列的递归定义 F0 = 1, F1 = 1 Fn = Fn - 1 + Fn - 2 (n 1) 1, 1, 2, 3, 5, 8, 13, 21, 34, 65, 99, ,用递归程序实现,long fib (int n) return n 2 ? 1 : fib(n - 1) + fib(n - 2); ,问题分析:这个程序好不好? 一方面,很好!程序与数学定义的关系很清晰,正确性容易确认,定义易读易理解,例fib(5)调用过程,fib(5),fib(4),fib(3),fib(3),fib(2),fib(2),fib(1),fib(1),fib(
6、0),fib(1),fib(0),fib(2),fib(1),fib(1),fib(0),存在什么问题?,问题,存在大量重复计算,参数越大重复计算越多。 有关系吗? 随着参数增大,计算中重复增长迅速,最快的微机上一分钟大约可以算出fib(45) 参数加1,fib多用近一倍时间(指数增长)。最快的微机一小时算不出fib(55),算fib(100)要数万年 计算需要时间,复杂计算需要很长时间。这是计算机的本质特征和弱点。说明它不是万能,有些事情“不能”做。,计算复杂度,人们发现了许多实际问题,理论上说可用计算机解决(可写出计算它的程序),但对规模大的情况(“大的参数 n”),人类永远等不到计算完成
7、。 这时能说问题解决了吗? 计算中有一大类问题被称为的“难解问题”,其中有许多很实际的问题,如规划、调度、优化等。 解决这些问题,需要理论和实际技术的研究。 另外,对于许多问题的实用的有效算法,有极大的理论价值和实际价值。 计算复杂性,难解问题,“P = NP?”问题。,阅读材料:NP问题 http:/ problem is assigned to the NP (nondeterministic polynomial time) class if it is solvable in polynomial time by a nondeterministic Turing machine. A
8、 P-problem (whose solution time is bounded by a polynomial) is always also NP. If a problem is known to be NP, and a solution to the problem is somehow known, then demonstrating the correctness of the solution can always be reduced to a single P (polynomial time) verification. If P and NP are not eq
9、uivalent, then the solution of NP-problems requires (in the worst case) an exhaustive search. Linear programming, long known to be NP and thought not to be P, was shown to be P by L. Khachian in 1979. It is an important unsolved problem to determine if all apparently NP problems are actually P. A pr
10、oblem is said to be NP-hard if an algorithm for solving it can be translated into one for solving any other NP-problem. It is much easier to show that a problem is NP than to show that it is NP-hard. A problem which is both NP and NP-hard is called an NP-complete problem.,为计算过程计时,统计程序或程序片段的计算时间有助于理解
11、程序性质。许多语言或系统都提供了内部计时功能。 有关函数在time.h,统计程序时间时程序头部应写 #include 在程序里计时,通常写表达式 clock() / CLOCKS_PER_SEC 得到从程序开始到表达式求值时所经历的秒数。,确定计算fib(45)所需要的时间的程序 #include #include long fib (int n) return n=1 ? 1 : fib(n-1)+fib(n-2); int main () double x;x = clock() / CLOCKS_PER_SEC;fib(45);x = clock() / CLOCKS_PER_SEC -
12、 x;printf(“Timing fib(45): %f.n“, x);return 0; ,Fibonacci数的迭代计算,Fibonacci数的递推计算,易见 1)f1和f2是1 2)知道fn-2和fn-1连续两个Fibonacci数,就可算出下一个fn 递推计算方式 逐个往后推,可用循环实现,递推方案,long fib1 (int n) long f1 = 1, f2 = 1, f3, i;if (n = 1) return 1;for (f3 = f2 + f1, i = 2; i n; +i) f1 = f2; f2 = f3; f3 = f1 + f2;return f3; ,做
13、一次递推,fn,fn-1,fn-2,程序分析,for (f3 = f2 + f1, i = 2; i n; +i) f1 = f2; f2 = f3; f3 = f1 + f2; 循环结束时i等于n,这时c的值是fn。 要得到此结论,可设法证明:每次判断 i 的值时f3正是 fi。,归纳证明,第一次判断时 i 的值是 2,f3 的值2,正是 fi(且 f1 的值是fi-1 ,f2 的值是fi-2 ) 若某次判断时 i 值是 k(小于n),循环体中的语句使f1变成fk-1 ,f2变成fk ,f3变成fk+1 。 i 值增 1 使我们又有f1为fi-2 ,f2变成fi-1 ,f3变成fi 根据归纳
14、法,每次判断 i 的值时f3正是 fi。,如何保证循环的正确执行,循环实现重复性计算,循环体可能执行多次。如何保证对各种数据都能正确完成计算? 循环中变量不断变化。写循环要考虑变量间的关系,保证某些关系在循环中不变:循环的不变关系。 写循环时最重要的就是想清循环中应维持变量间的什么关系才能保证循环结束时变量能处在所需状态。写完循环后应仔细检查是否满足要求。 循环不变关系(循环不变量)是理解循环、写好循环的关键。,问题,本例中用循环的函数比用递归定义的好吗? 新函数在计算时间上有极大优越性。计算时间由循环次数确定。循环体执行次数大致为n。fib(100)只需约100次循环,几乎察觉不到所花费时间
15、。 新函数定义较复杂,有复杂的循环。要理解程序意义,确认函数对任何参数都算出Fibonacci值,需要借助“循环不变关系”的概念和细致分析。,注意:这个例子并不是说明递归比循环的效率低。完全可以写出计算fib的同样高效的递归定义的函数,最大公约数,求两个整数的最大公约数(greatest common divisor,GCD),写函数 long gcd(long, long) 解法1 从某个数开始,逐个判断当前数是否能同时整除m和n,在这个过程中记录下能同时整除m和n的最大整数。 需要用一个辅助变量k记录当前需要判断的数。 用一个循环实现k顺序取值 初值设为1 每次判断完后增1 直到k大于m和
16、n中其中的一个为止 记下循环过程中出现的新的m和n的公约数,作为新的最大公约数 用变量d表示当前的最大公约数 初值1(是公约数),遇到新的公约数(一定更大)时记入d,程序,有了d及其初值,k可以从2开始循环。函数定义 long gcd (long m, long n)long d = 1, k = 2;for ( ; k = m 参数互素时初值1会留下来,能保证正确,计算过程示例,特殊情况处理,一些特殊情况需要处理 1)m和n都为0需特殊处理。令函数返回值0; 2)若m和n中一个为0,gcd是另一个数。函数的返回值正确。也可直接判断处理; 3)m、n为负时函数返回1,可能不对。 应在循环前加语
17、句 if (m = 0 ,可能方式2,换个思路 令k从某个恰当的大数开始递减,找到的第一个公约数就是最大公约数。 k初值可取m和n中小的一个。 结束条件 k值达到1或找到了公约数。1总是公约数。 程序主要部分可写为: for (k = (m n ? n : m); /把k设为n的较小者m % k != 0 | n % k != 0; k-); /* 空循环体 */ return k; /*循环结束时k是最大公约数 */,过程示例,两种方式比较,本方法比前一方法简单一些。 两种方法的共同点是重复测试。 这类方法的缺点是效率较低,参数大时循环次数很多。,解法2 辗转相除法,求GCD有著名的欧几里德
18、算法(欧氏算法,辗转相除法)。最大公约数的递归定义:,例,例1 gcd1(70, 30) m = 70, n = 30 m % n 10 gcd(30, 10) m = 30, n = 10 m % n 0 例2 gcd1(65, 15) m = 65, n = 15 m % n 5 gcd1(15, 5) m = 15, n = 5 m % n 0,递归程序解决,函数定义与数学定义直接对应long gcd (long m, long n)return m % n = 0 ? n : gcd(n, m % n);假设第二个参数非0,且参数都不小于0。 对欧氏算法的研究保证了本函数能结束,对较大
19、的数计算速度也很快,远远优于顺序检查。,加入特殊情况处理,long gcd(long m, long n) if (m 0) m = -m;if (n 0) n = -n;return n = 0 ? m : gcd(m, n); ,循环方法,辗转相除就是反复求余数,也是重复性工作,可可用循环结构实现。 出发点m和n;循环判断m % n是否为0 若是则n为结果; 否则更新变量:令m取n的原值,n取m%n的原值。 为正确更新需用辅助变量r,正确的更新序列: r = m % n; m = n; n = r;,非递归函数定义,long gcd2 (long m, long n) long r;if
20、(n = 0) return m;for (r = m % n; r != 0; r = m % n) m = n;n = r;return n; 参数是局部变量,可在函数体里使用和修改。,河内塔(梵塔问题),河内塔( hanoi塔,梵塔)问题,问题出自古印度(一说西藏) 某神庙有三根细柱,64个大小不等、中心有孔的金盘套在柱上,构成梵塔。 僧侣日夜不息地将圆盘从一柱移到另一柱。 规则 每次只移一个盘,大盘不能放到小盘上。 开始时圆盘从大到小套在一根柱上,据说所有圆盘都搬到另一根柱时世界就要毁灭。,图示,要求,要求写程序模拟搬圆盘过程,打印出搬动指令序列。 为方便,分别将三根圆柱命名为a、b和
21、c,假定开始时所有圆盘都在a上,要求最终搬到b。,问题分析,初看问题似乎没规律。求解的关键在于看到问题的“递归性质”。 搬64个盘的问题可归结为两次搬63个盘。 搬n个圆盘的问题可以归结为搬n-1个圆盘,把n个盘从柱A搬到柱B的工作可以如下完成 从柱A借助柱B将n-1个圆盘搬到柱C; 将最大圆盘从柱A搬到柱B; 从柱3借助柱A将n-1个圆盘搬到柱B;,从柱A借助柱B将3个圆盘搬到柱C,A,B,C,A,B,C,从柱A将最大的圆盘移动B柱,从柱C借助柱A将3个圆盘搬到柱B,void moveone (char from, char to) printf(“%c - %cn“, from, to);
22、 void henoi(int n,char from,char to,char by) if (n = 1) moveone(from, to);elsehenoi(n-1, from, by, to);moveone(from, to);henoi(n-1, by, to, from); ,moveone定义为函数是为了方便。函数调用: henoi(6, a, b, c);,hanio(3, a, b, c);hanio(2, a, c, b);moveone(a, b);hanio(2, c, b, a);,hanio(2, a, c, b)hanio(1, a, b, c);moveo
23、ne(a, c);hanio(1, b, c, a);,hanio(1, a, b, c)moveone(a, b);,hanio(1, b, c, a)moveone(b, c);,hanio(2, c, b, a)hanio(1, c, a, b);moveone(c, b);hanio(1, a, b, c);,hanio(1, c, a, b)moveone(c, a);,hanio(1, a, b, c)moveone(a, b);,常见方法,设法确认程序对最基本的情况能正常工作 解决了基本情况后再考虑更复杂的情况 设法找出出错的规律性,检查出错时数据经过的执行流,逐步缩小可疑范围 在程序中加入输出语句,检查重要变量的值的变化情况 利用 IDE 的排错功能,本章要求,掌握递归的概念 递归函数的编写方法 理解递归函数的效率,本章结束,