递归函数
- 递归介绍
递归就是一个函数在它的函数体内调用它自身。执行递归函数将反复调用其自身,每调用一次就进入新的一层。递归函数必须有结束条件。
当函数在一直递推,直到遇到墙后返回,这个墙就是结束条件。
所以递归要有两个要素,结束条件与递推关系
注:
递归的时候,每次调用一个函数,计算机都会为这个函数分配新的空间,这就是说,当被调函数返回的时候,调用函数中的变量依然会保持原先的值,否则也不可能实现反向输出。
- 例如:
|
|
|
|
- 其中这个树的调用顺序是先纵后横,fib5的下一级的fib4的调用顺序为fib5->fib4->fib3->fib2->fib1->返回1->fib0->返回->fib1(与左侧fib2同级的fib1)->fib2(与左侧fib3同级的fib2)->返回到fib5。
|
|
其中这个就相当于在1~n和第n个之间切了一刀,这样比那个在第一个和第二个到第n-1个之间切一刀好,前者是等价的,并且减小了问题的规模。
这三个柱子的角色再变,首先是C协助,后来B协助,再后来A协助、
|
|
- 递归的特点
- 每一级函数调用时都有自己的变量,但是函数代码并不会得到复制,如计算5的阶乘时每递推一次变量都不同;
- 每次调用都会有一次返回,如计算5的阶乘时每递推一次都返回进行下一次;
- 递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序;
就是在递归函数前后的语句的执行顺序 - 递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反;
- 递归函数中必须有终止语句。
效率
系统栈(也叫核心栈、内核栈)
是内存中属于操作系统空间的一块区域,其主要用途为: (1)保存中断现场,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出; (2)保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。用户栈
是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。
我们编写的递归程序属于用户程序,因此使用的是用户栈。栈溢出
函数调用的参数是通过栈空间来传递的,在调用过程中会占用线程的栈资源。而递归调用,只有走到最后的 结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放,如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,导致程序的异常退出。
- 每一个递归程序都遵循相同的基本步骤:
- (1) 初始化算法。递归程序通常需要一个开始时使用的种子值(seed value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数, 这个函数是非递归的,但可以为递归计算设置种子值。
- (2) 检查要处理的当前值是否已经与基线条件相匹配。如果匹配,则进行处理并返回值。
- (3) 使用更小的或更简单的子问题(或多个子问题(多个子问题就是斐波那锲那种的形式,这样与切蛋糕有些区别)(子问题就是重复的那部分,不过就是规模变小了))来重新定义答案。
- (4) 对子问题运行算法。
- (5) 将结果合并入答案的表达式。
- (6) 返回结果。
例如:
|
|
- 函数所做的最后一件事情是一个函数调用(递归的或者非递归的),这被称为 尾部调用(tail-call)。使用尾部调用的递归称为 尾部递归。当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
在尾部调用之后除去栈结构的方法称为 尾部调用优化 。
- (1) 函数在尾部被调用之后,还需要使用哪个本地变量?哪个也不需要。
- (2) 会对返回的值进行什么处理?什么处理也没有。
- (3) 传递到函数的哪个参数将会被使用?哪个都没有。
- 好像一旦控制权传递给了尾部调用的函数,栈中就再也没有有用的内容了。虽然还占据着空间,但函数的栈结构此时实际上已经没有用了,因此,尾部调用优化就是要在尾部进行函数调用时使用下一个栈结构 覆盖 当前的栈结构,同时保持原来的返回地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
int test1(){ int a = 3; test1(); /* recursive, but not a tail call. We continue */ /* processing in the function after it returns. */ a = a + 4; return a; } int test2(){ int q = 4; q = q + 5; return q + test1(); /* test1() is not in tail position. * There is still more work to be * done after test1() returns (like * adding q to the result*/ } int test3(){ int b = 5; b = b + 2; return test1(); /* This is a tail-call. The return value * of test1() is used as the return value * for this function.*/ } int test4(){ test3(); /* not in tail position */ test3(); /* not in tail position */ return test3(); /* in tail position */ }
- 好像一旦控制权传递给了尾部调用的函数,栈中就再也没有有用的内容了。虽然还占据着空间,但函数的栈结构此时实际上已经没有用了,因此,尾部调用优化就是要在尾部进行函数调用时使用下一个栈结构 覆盖 当前的栈结构,同时保持原来的返回地址。
递归实现全排列
输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。
递归思想:
假如针对abc的排列,可以分成 (1)以a开头,加上bc的排列 (2)以b开头,加上ac的排列 (3)以c开头,加上ab的排列
|
|
下面这道题就是知道最后n=7时的结果,来推最初的那个结果 的题目???
- 一个人赶着鸭子去每个村庄卖,每经过一个村子卖去所赶鸭子的一半又一只。这样他经过了七个村子后还剩两只鸭子,问他出发时共赶多少只鸭子?经过每个村子卖出多少只鸭子
- 题目分析:
递归终止的条件是当达到第7个村庄时递归停止,设经过的村庄数为n则有剩余的鸭子为总数为每次剩余的鸭子数位sum = sum-(sum/2+1) 算法构造:当 n=7 时 sum = 2;当 0<n<7 时 sum =2*sum+2;,结束条件是n<=0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <iostream.h> class Questionone{ public: int answer(int n, int sum){ if(n>0){ sum = 2*sum+2; if(n-1>0){ cout<<"第"<<n-1<<"个村庄"<<"卖出"<<2*sum+2-sum<<endl; } n--; return answer(n,sum); }else{ return sum; } } }; void main(){ int SUM = 2; int N = 7; Questionone question; cout<<"总数:"<<question.answer(N,SUM)<<endl; }
日本著名数学游戏专家中村义作教授提出这样一个问题:父亲将2520个桔子分给六个儿子。分完 后父亲说:“老大将分给你的桔子的1/8给老二;老二拿到后连同原先的桔子分1/7给老三;老三拿到后连同原先的桔子分1/6给老四;老四拿到后连同原先的桔子分1/5给老五;老五拿到后连同原先的桔子分1/4给老六;老六拿到后连同原先的桔子分1/3给老大”。结果大家手中的桔子正好一 样多。问六兄弟原来手中各有多少桔子?
题目分析:解决此问题主要使用递归运算。由题目可以看出原来手中的加上得到的满足关系式:StartNum = 420 (n -2)/(n - 1) 分给下一个人的橘子数:GiveNum = AfterGetNum / n; 下一个人的橘子数:nextStartNum = 420(n-1)/(n-2) - GiveNum; 下一个人加上之前得到的橘子的总数:afterGetNum = nextStartNum + GiveNum; 以此使用递归算法可以算出各个孩子原来手中的橘子数。
|
|
- 八皇后问题 ?????
问题描述:经典的八皇后问题,即在一个8*8的棋盘上放8个皇后,使得这8个皇后无法互相攻击( 任意2个皇后不能处于同一行,同一列或是对角线上),输出所有可能的摆放情况。
|
|