递归
【练习2】因数分解
题目描述 各位在小学时都学过因数分解,都了解怎么样用纸笔计算 出结果,现在由你来教电脑做因数分解。因数分解就是把 一个数,切分为若干个质数的乘积,如 12=2^2 * 3 ,其中, 次方的符号以 ^ 来表示. 输入 一个整数n, 大于1 且 小于等于 1000000 输出 一个字串为整数n的因数分解答案,格式如样例。 样例输入 20 样例输出 2^2*5
2.
斐波那契(Fibonacci)数列用递归过程描述为: Funtion fb(n:integer):integer; Begin if n<3 then fb:=1 else fb:=fb(n-1)+fb(n-2); End; 上面的递归过程,调用一次产生二个新的调用,递归次数呈指数增长,时 间复杂度为O(2n),把它改为非递归: x:=1;y:=1; for i:=3 to n do begin z:=y;y:=x+y;x:=z; end; 修改后的程序,它的时间复杂度为O(n)。 我们在编写程序时是否使用递归算法,关键是看问题是否适合用递归算法 来求解。由于递归算法编写的程序逻辑性强,结构清晰,正确性易于证明, 程序调试也十分方便,在NOIP中,数据的规模一般也不大,只要问题适合 用递归算法求解,我们还是可以大胆地使用递归算法。
在调用过程或函数之前,系统需完成三件事: ⑴为被调用过程的局部变量分配存储区; ⑵将所有的实在参数、返回地址等信息传递给被调用过程保存; ⑶将控制转移到被调过程的入口。 从被调用过程返回调用过程之前,系统也应完成三件工作: ⑴保存被调过程的计算结果; ⑵释放被调过程的数据区; ⑶依照被调过程保存的返回地址将控制转移到调用过程。
递归的概念
• 直接或间接地调用自身的算法称为递归算法。 用函数自身给出定义的函数称为递归函数。
从前有座山,山上有座庙,庙里有一个 老和尚在给小和尚讲故事,老和尚说: 从前有座山,山上有座庙,庙里有一个 老和尚在给小和尚讲故事,老和尚说: 从前有座山……
递归的概念
例1 阶乘函数 阶乘函数可递归地定义为:
汉诺塔问题
• 只有一个盘子的移动:AC • 两个盘子的移动:AB,AC,BC • n个盘子的移动(n>2): o 将上面的n-1个盘子看成一个整体,则此问题被转换 为两个盘子的移动 o 而n-1个盘子的移动规则与此相同
汉诺塔游戏演示
汉诺塔问题的递归描述
• 当只移动一个盘子的时候,直接从起始柱移动到目标柱, 否则,执行如下操作: 1. 将前n-1盘子以相同的方法从起始柱移动到临时柱; 2. 将第n个盘子直接移动到目标柱;
递归
信息学奥赛解题三步曲: 1.看 (分析问题) 看清楚已知什么,求什么,也就是输入什么,输出什么; 准确区分问题的主要条件和次要条件; 把握数据规模,迅速提炼问题模型和原型; 2.想 (算法设计) 要先考虑人脑怎么做,再考虑如何编程让电脑去做; 要从时间复杂性和空间复杂性两方面对算法进行评价,尽量优化; 3.动手(代码编写) 选择一种语言,编写程序并调试运行; 样例数据、边界数据和极限数据都要调试; 经典算法的核心思想基本上是不会变化的,但是竞赛时候解决具体问题 的方法却千变万化。要想利用“不变”的算法,解决“万变”的问题, 不仅需要深刻理解经典算法的精髓思想、还要有丰富的解题经验、和一 定的分析问题,构建模型能力。 任何时候都是根据问题选择算法,不应该用算法来选择问题,本末不能 倒置。
• 因此,我们可以得到递归公式: F(n)=F(n-1)+F(n-2) • 而递归的终止条件则是: n=1或n=2时,F(n)=1
function F(k: integer): longint; begin if (k = 1) or (k = 2) then F := 1 else F := F(k - 1) + F(k - 2) end; begin read(n); writeln(F(n)); end.
n=3,f(3)=3*f(2)
思考:输入一串以‘!’结束的字符,按逆序输出。
program ex2; procedure rever; var c:char; begin read(c); if c<>’!’ then rever; write(c); end; begin {主程序} rever; end.
应用举例—Hanoi塔问题
例 Hanoi塔问题 设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆盘,这 些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号 为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上,并仍 按同样顺序叠置。在移动圆盘时应遵守以下移动规则: 规则1:每次只能移动1个圆盘; 规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘之上; 规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中 任一塔座上。
兔子出生演示
1 1
1月
2月
2
2 3
1
1
3月 4月
5
2
3
4
1
5月
• 从上面的分析可以看到,第n个月后兔子的数目有 两部分构成:1、上月的所有兔子;2、上月的所 有老兔子所生的小兔子。 • 因此,若我们记第n个月的兔子总数为F(n)、老兔 子数为G(n)的话,那么F(n)=F(n-1)+G(n-1)。 • 那么G(n)怎么得到呢?通过分析上图,我们可以 发现,第n个月的老兔子数等于第n-1个月的兔子 总数,因此上式中的G(n-1)=F(n-2)。
输入 hey! 输出 !yeh。
c=‘!’
procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end;
hey!
c=‘y’
c=‘e’ c=‘h’
procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end; procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end;
小结与思考
递归算法简单直观,是整个计算机算法和程序设计领 域一个非常重要的方面,必须熟练掌握和应用它。递 归算法在计算机中的执行过程比较复杂,需要用系统 栈进行频繁的进出栈操作和转移操作。递归转化为非 递归后,可以解决一些空间上不够的问题,但程序太 复杂。所以,并不是一切递归问题都要设计成非递归 算法。实际上,很多稍微复杂一点的问题(如汉诺塔 问题、二叉树的遍历、图的遍历、快速排序等),不 仅很难写出它们的非递归过程,而且即使写出来也非 常累赘和难懂。在这种情况下,编写出递归算法是最 佳选择。
第n个Fibonacci数可递归地计算如下: Function fib(n:integer):integer; Begin If n=0 then fib:=0 Else if n=1 then fib:=1 else fib:=fib(n-1)+fib(n-2) End;
应用举例
• 有一种兔子,出生后一个月就可以长大,然后再过一个月 一对长大的兔子就可以生育一对小兔子且以后每个月都能 生育一对。 现在,我们有一对刚出生的这种兔子,那么,n个月过后, 我们会有多少对兔子呢?假设所有的兔子都不会死亡。 • 输入:5 • 输出:5
递归算法的优缺点
优点:结构清晰,可读性强,而且容易用 数学归纳法来证明算法的正确性,因此它 为设计算法、调试程序带来很大方便。
缺点:递归算法的运行效率较低,无论是 耗费的计算时间还是占用的存储空间都比 非递归算法要多,递归的深度受到系统栈 空间的限制
递归算法转非递归算法
1. 递归转化为递推:当递归算法所涉及的数据定义形式是 递归的情况下,通常可以将递归算法转化为递推算法, 用递归的边界条件做为递推的边界条件。比如求阶乘、 斐波那契数列等。求阶乘的递推算法如下: s := 1; for i := 1 to n do s := s * i; writeln(s); 递归转化为回溯:对于可以用回溯算法解决的问题,也 可以用非递归的回溯来实现。如n皇后问题。
【练Байду номын сангаас1】阅读程序:
const num = 5; var n: integer; function r(n : integer) : integer; var i : integer; begin if n <= num then begin r := n; exit; end; for i :=1 to num do if r(n-i) < 0 then begin r:=i; exit; end; r:=-1; end; begin readln(n); writeln(r(n)); end. 输入 16 输出:__________
边界条件
n0 1 n! n(n 1)! n 0
递归方程 边界条件与递归方程是递归函数的二个要素,递归函 数只有具备了这两个要素,才能在有限次计算后得出 结果。
如何设计递归算法
1.确定递归公式 2.确定边界(终了)条件
Function f (n :integer) : longint; Begin if n = 0 then f:=1 else f:=n*f(n-1) End;
procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end;
程序中,c 是过程rever的局部变量。 每一次递归调用,都要为局部变量重 新分配单元,因此各层的变量c实际上 是不同的变量,它们分别用于保存执 行本层调用时的输入值。
CH10-递归(Recursion)
If the last-executed statement of a function is a recursive call to the function itself, then this call can be eliminated by reassigning the calling parameters to the values specified in the recursive call, and then repeating the whole function.(可以用循环来完成,修改参数值)
基本的部分(基例)不需要递归)
②A general method that reduces a particular case to one or
more of the smaller cases, thereby making progress toward
eventually reducing the problem all the way to the base case.
Tail Recursion
Hanoi Without Tail Recursion
void move(int count, int start, int finish, int temp) { int swap; // temporary storage to swap towers while (count > 0) { // Replace the if statement with a loop. move(count - 1, start, temp, finish); // first recursive call cout << "Move disk " << count << " from " << start << " to " << finish << "." << endl; count--; // Change parameters to mimic the second recursive call. swap = start; start = temp; temp = swap; } }
递归
1
递归算法
什么是递归 递归调用的实现原理 递归经典例题
递归定义
在定义一个过程或函数时出现调用本过 程或本函数的成分,称之为递归。 若调用自身,称之为直接递归。 若过程或函数p调用过程或函数q,而q又调 用p,称之为间接递归。
何时用到递归 以下三种情况常常用到递归方法。
1 定义是递归的 2 数据结构是递归的 3 问题的解法是递归的
int main() { int a[1000]; f(6,a,0); return 0; }
经典问题:整数划分
要求:如果对上一个题改一下要求,求解一共多少种划分方 法,如果不用上一种思路,请再尝试另一种递归解决方案
经典问题:整数划分
整数划分问题是算法中的一个经典命题之一,有关这个问题的讲述在 讲解到递归时基本都将涉及。所谓整数划分,是指把一个正整数n写成 如下形式: n=m1+m2+...+mi; (其中mi为正整数,并且1 <= mi <= n) ,则{m1,m2,...,mi}为n的一个划分。 如果{m1,m2,...,mi}中的最大值不超过m,即 max(m1,m2,...,mi)<=m,则称它属于n的一个m划分。这里我们记 n的m划分的个数为f(n,m); 例如但n=4时,他有5个划分, {4},{3,1},{2,2},{2,1,1},{1,1,1,1}; 注意4=1+3 和 4=3+1被认为是同一个划分。 该问题是求出n的所有划分个数,即f(n, n)。下面我们考虑求f(n,m) 的方法; 1.递归法:
15
f (int a[], int begin, int n) if (begin==n) return 0; else return a[begin]+f(a,begin+1,n);
递归的概念递归过程与递归工作栈递归与回溯广义表
} }
递归过程与递归工作栈
递归过程在实现时,需要自己调用自己。 层层向下递归,退出时的次序正好相反:
}
递归找含x值的结点
f
x
fff
问题的解法是递归的
例如,汉诺塔(Tower of Hanoi)问题的解法: 如果 n = 1,则将这一个盘子直接从 A 柱移到
C 柱上。否则,执行以下三步: ① 用 C 柱做过渡,将 A 柱上的 (n-1) 个盘子移 到 B 柱上: ② 将 A 柱上最后一个盘子直接移到 C 柱上; ③ 用 A 柱做过渡,将 B 柱上的 (n-1) 个盘子移 到 C 柱上。
递归调用
n! (n-1)! (n-2)!
1! 0!=1
返回次序
主程序第一次调用递归过程为外部调用;
递归过程每次递归调用自己为内部调用。
它们返回调用它的过程的地址不同。
递归工作栈
每一次递归调用时,需要为过程中使用的 参数、局部变量等另外分配存储空间。
每层递归调用需分配的空间形成递归工作 记录,按后进先出的栈组织。
while ( n >= 0 ) { cout << "value " << A[n] << endl;
n--;
} }
递归与回溯 常用于搜索过程
n皇后问题 在 n 行 n 列的国际象棋棋盘上,
递归的定义及应用
• (2)式给出了fun(n)的值与fun(n-1)的值之间的关系, 称为递归体。
9
递归求n!
int fun(int n) {
if (n==1)
/*语句1*/
return 1; /*语句2*/
分解过程
else
/*语句3*/
return fun(n-1)*n; /*语句4*/
n=3
n=2
} n=1
fuc(3) ﹕
s
s为空串
f(s)=
•
Concat(f(SubStr(s,2,StrLength(s)-1)),SubStr(s,1,1)) 其他
15
SqString invert(SqString &s) {
SqString s1,s2; if(StrLength(s)>0) { s1=invert(SubStr(s,2,StrLength(s)-1));
• 体现了分而治之的算法思想
3
6.1.2 何时使用递归
• 在以下3种情况下,常常要用到递归的方法。
• 1. 定义是递归的 • 2. 数据结构是递归的
求n!和Fibonacci数列
例如,单链表就是一种递归的数据结构,其结
点类型定义如下:
typedef struct LNode
{
ElemType data;
else return(head->data+Sum(head->next));
}
5
3. 问题的求解方法是递归的
• 汉诺塔问题是印度的一个古老的传说。 • 在世界中心贝拿勒斯(在印度北部)的圣庙里, 一块黄铜板上插着三根宝石针。印度教的主神梵天在 创造世界的时候,在其中一根针上从下到上地穿好了 由大到小的64片金片,这就是所谓的汉诺塔。 • 不论白天黑夜,总有一个僧侣在按照下面的法则 移动这些金片:一次只移动一片,不管在哪根针上, 小片必须在大片上面。 • 僧侣们预言,当所有的金片都从梵天穿好的那根 针上移到另外一根针上时,世界就将在一声霹雳中消 灭,而梵塔、庙宇和众生也都将同归于尽。
递归的作用与意义
递归的作用与意义递归是一种常见的编程技术,它在解决问题时具有重要的作用与意义。
递归是指一个函数或过程在执行过程中不断调用自身的过程。
通过递归,可以将复杂的问题分解成更小的子问题,并通过解决子问题来解决原始问题。
递归的作用之一是简化问题。
对于一些复杂的问题,我们可以使用递归的方式将其划分为更小的子问题。
这样一来,我们只需要解决每个子问题,然后将它们的解合并起来,就能得到原始问题的解。
递归的这种特性使得问题的解决变得更加清晰和简明。
递归还可以提高代码的可读性和可维护性。
使用递归的方式可以将代码分解成多个独立的函数,每个函数只负责解决一个子问题。
这样一来,我们可以更加专注地思考每个子问题的解决方案,而不需要同时考虑多个问题。
这种模块化的设计使得代码更易于理解和修改,从而提高了代码的可读性和可维护性。
除了简化问题和提高代码的可读性和可维护性之外,递归还可以解决一些特定的问题。
例如,在树的遍历中,递归可以帮助我们实现前序遍历、中序遍历和后序遍历等操作。
在图的搜索中,递归可以帮助我们实现深度优先搜索和广度优先搜索等算法。
递归的这种特性使得它成为解决这些问题的有效工具。
然而,递归也存在一些潜在的问题和限制。
首先,递归的实现需要消耗额外的内存空间,因为每次递归调用都需要保存函数的状态和局部变量。
如果递归的层数很深,可能会导致栈溢出的问题。
其次,递归的效率通常较低,因为每次递归调用都需要进行函数的调用和返回操作,而这些操作会带来额外的开销。
因此,在一些性能要求较高的场景中,可能需要考虑使用其他的解决方案。
递归在编程中具有重要的作用与意义。
它可以简化问题的解决过程,提高代码的可读性和可维护性,并解决一些特定的问题。
然而,递归也存在一些潜在的问题和限制。
因此,在使用递归时需要仔细考虑问题的性质和场景的要求,以确保递归的有效性和有效性。
只有在合适的场景下,递归才能发挥其最大的作用和意义。
希望通过本文的介绍,读者能够更好地理解递归的作用与意义,并在实际的编程中灵活运用。
c语言的递归
c语言的递归递归,作为一种编程技巧和思维方式,是计算机科学中一个重要的概念。
在C语言中,递归可以被广泛应用于各种算法和问题的解决过程中。
本文将介绍C语言中递归的基本原理、优缺点以及一些常见的递归应用场景。
一、递归的基本原理递归是指在一个函数的定义中调用函数本身的过程。
在C语言中,递归函数的定义通常包括两个部分:基准情况(base case)和递归情况(recursive case)。
基准情况是指满足特定条件时递归停止,递归情况则是通过不断调用函数本身来实现问题的拆解和解决。
下面是一个简单的例子,演示了如何使用递归来实现计算n的阶乘:```cint factorial(int n) {// 基准情况if (n == 0 || n == 1) {return 1;}// 递归情况return n * factorial(n - 1);}int main() {int n = 5;int result = factorial(n);printf("%d的阶乘是:%d\n", n, result);return 0;}```在上面的代码中,当n等于0或者1时,递归停止,返回结果1。
否则,函数将n与factorial(n-1)相乘,并返回结果。
通过逐步拆解n的值,直到满足基准情况,递归函数可以完成阶乘的计算。
二、递归的优缺点递归在解决某些问题时可以提供简洁、优雅的解决方案,但同时也存在一些缺点。
1. 优点:简洁清晰:递归能够将问题分解为更小的子问题,代码结构更加清晰易懂。
解决复杂问题:某些问题如果采用迭代的方式来解决会非常复杂,而递归提供了一种更加直观的解决思路。
2. 缺点:性能开销:递归往往需要调用函数本身多次,会产生额外的函数调用开销,导致性能下降。
内存占用:递归的过程中需要保存每一次函数调用的上下文,可能会导致内存占用较大。
三、递归的应用场景递归可以用于解决一些常见的问题和算法,比如树的遍历、图的搜索、排列组合等。
递归
17
n
n-1 n-1
n-1
……
n-2 n-2
n-2
n-2 n-2
n-2
…… 例4 排列问题
1
n0
F
(n)
1
n 1
F (n 1) F (n 2) n 1
边界条件 递归方程
非递归定义:
F(n)
1 5
1
2
5 n1
1
2
5
n1
7
第n个Fibonacci数可递归地计算如下: int fibonacci(int n)
{ if (n <= 1) return 1; return fibonacci(n-1)+fibonacci(n-2);
q(6,1) q(6,2) q(6,3) q(6,4) q(6,5) q(6,6)
28
用栈模拟q(6,6)的递归计算过程。
q(4,1) q(4,2) q(6,2) q(6,3) q(6,4) q(6,5) q(6,6)
29
用栈模拟q(6,6)的递归计算过程。
q( q(6,5) q(6,6)
第2章 递归与分治策略
学习要点: • 理解递归的概念。 • 掌握设计有效算法的分治策略。 • 通过下面的范例学习分治策略设计技巧。 (1)二分搜索技术; (2)大整数乘法; (3)Strassen矩阵乘法; (4)棋盘覆盖; (5)合并排序和快速排序; (6)线性时间选择; (7)最接近点对问题; (8)循环赛日程表。
递归的三个条件
递归的三个条件
1. 基线条件
基线条件指的是递归函数的退出条件,也就是递归最底层的情况,通常是能够被直接求解或解决的情况。
2. 递归条件
递归条件指的是递归函数中,解决问题所需要的步骤,这些步骤通常需要调用函数本身。
递归条件应该包含递归操作,这样递归才能一步步向基线条件靠近。
3. 向基线条件靠近
向基线条件靠近指的是递归过程中每次调用函数时,问题规模会不断减小,最终会到达基线条件。
如果递归函数不能向基线条件靠近,则递归会陷入无穷循环,导致程序崩溃。
什么是递归
什么是递归?递归(Recursion)是一种编程技术,其中一个函数调用自身来解决问题。
递归是通过将问题分解成更小的子问题来解决复杂问题的一种方法。
这种自我调用的过程在每次调用中都会处理一个更小的问题,直到达到基本情况(终止条件),然后逐步返回结果,最终解决整个问题。
递归的核心思想是将复杂问题分解成更小规模的相同问题。
每次递归调用都会将问题的规模减小,直到问题达到一定的规模,可以直接解决。
这个基本情况通常是一个简单且不再需要递归的情况。
递归的实现通常包括以下几个要素:1. 基本情况:基本情况是递归调用的终止条件。
当问题的规模减小到一定程度,可以直接解决时,递归将停止。
在基本情况下,函数通常直接返回结果而不再进行递归调用。
2. 递归调用:递归函数在解决问题的过程中会调用自身来处理更小规模的子问题。
递归函数通过传递问题的子集或更小的输入来减小问题的规模。
递归调用将重复进行,直到达到基本情况。
3. 问题的规模减小:递归函数通过每次调用将问题的规模减小来逐步解决问题。
这通常涉及到在每次递归调用中使用不同的输入或参数。
问题的规模必须在每次递归调用中减小,否则递归将无法终止,导致无限循环(无穷递归)。
递归的一个典型示例是计算阶乘。
阶乘是指从1到某个正整数之间所有整数的乘积。
使用递归,我们可以将阶乘问题分解为更小的子问题,直到达到基本情况。
例如,计算5的阶乘(5!)可以使用递归的方式来实现:```factorial(n):if n == 0: // 基本情况return 1else:return n * factorial(n-1) // 递归调用```在上面的示例中,当n等于0时,递归调用将停止,返回1作为结果。
否则,函数将n与n-1的阶乘相乘,并继续递归调用,直到达到基本情况。
递归函数需要小心处理,确保递归调用能够终止,并且问题的规模在每次递归调用中减小。
否则,递归可能会导致堆栈溢出或无限循环的问题。
递归在许多算法和数据结构问题中都有应用,例如树的遍历、图的搜索、排序算法等。
递归的优化方法
递归(Recursion)是一种强大的编程技术,但在某些情况下,它可能会导致性能问题,特别是当递归深度很大时。
以下是一些优化递归的方法:1.尾递归优化(Tail Recursion Optimization):尾递归是一种特殊的递归,其中递归调用是函数体中的最后一个操作。
许多现代的编译器和解释器都可以对尾递归进行优化,将其转换为迭代,从而避免栈溢出和提高性能。
2.记忆化递归(Memoization):这是一种通过存储已经计算过的结果来避免重复计算的技术。
当函数被递归调用时,它首先检查是否已经计算过该结果。
如果是,则直接返回存储的结果,而不是重新计算。
3.动态规划(Dynamic Programming):这是一种解决递归问题的更通用的技术。
动态规划将问题分解为子问题,并存储子问题的解,以便在需要时重用它们。
这可以避免重复计算,并显著提高性能。
4.转换为迭代:如果可能,将递归转换为迭代可以提高性能。
迭代通常比递归更有效,因为它不需要在每次递归调用时创建新的栈帧。
5.限制递归深度:在某些情况下,你可以通过设置递归深度的上限来避免栈溢出。
这可以防止递归过深,但可能无法解决所有的问题。
6.减少重复计算:优化你的递归算法以减少重复的计算。
例如,如果你在计算一个大的递归树,你可能会发现有很多子树被重复计算。
你可以通过缓存这些子树的结果来避免重复计算。
请注意,以上优化方法可能并不适用于所有情况,具体取决于你的递归算法和问题。
在优化递归时,重要的是要理解你的算法,并确定哪种优化方法最适合你的情况。
