递归 recursive functions

递归函数

  • 递归介绍

  递归就是一个函数在它的函数体内调用它自身。执行递归函数将反复调用其自身,每调用一次就进入新的一层。递归函数必须有结束条件。
当函数在一直递推,直到遇到墙后返回,这个墙就是结束条件。
所以递归要有两个要素,结束条件与递推关系

注:
递归的时候,每次调用一个函数,计算机都会为这个函数分配新的空间,这就是说,当被调函数返回的时候,调用函数中的变量依然会保持原先的值,否则也不可能实现反向输出。

  • 例如:
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
28
29
30
31
32
33
34
35
计算n的阶乘
#include <stdio.h> 
int factorial(int n)
{
    int result;
    if (n<0)                  //	判断例外
    {
        printf("输入错误!\n");
        return 0;
    } 
    else if (n==0 || n==1)
    {
        result = 1;            //回推墙 ,就是结束条件
    }
    else
    {
        result = factorial(n-1) * n;  //递推关系,这个数与上一个数之间的关系。
    }
    return result;
	}

    int main(){
    int n = 5;                                              //输入数字5,计算5的阶乘
    printf("%d的阶乘=%d",n,factorial(n));
    return 0;
}
其中阶乘用循环来实现:
int factorial(int n){
    int product = 1;
    while(n>0){
        product *= n;
        n--;
    }
    return product;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
斐波那契数列
斐波那契数列指的是这样一个数列:
0, 1, 1, 2, 3, 5, 8, 13, 21, ···
这个数列从第三项开始,每一项都等于前两项之和.

#include <stdio.h>
long fibonacci( long num ){
    if ( num == 0 || num == 1 ){
        return num;        //这就是结束条件
    }
    else{
        return fibonacci( num -1 ) + fibonacci( num -2 );
                          //这是循环条件
    }
}
void mian(){
    long number;
    puts("请输入一个正整数: ");
    scanf("%ld", &number);
    printf("斐波那契数列第%ld项为: %ld\n", number, fibonacci( number ) );
}
  • 其中这个树的调用顺序是先纵后横,fib5的下一级的fib4的调用顺序为fib5->fib4->fib3->fib2->fib1->返回1->fib0->返回->fib1(与左侧fib2同级的fib1)->fib2(与左侧fib3同级的fib2)->返回到fib5。
    upload successful
1
2
3
如果只有 1 个盘子,则不需要利用B塔,直接将盘子从A移动到C。
如果有 2 个盘子,可以先将盘子1上的盘子2移动到B;将盘子1移动到C;将盘子2移动到C。这说明了:可以借助B将2个盘子从A移动到C,当然,也可以借助C将2个盘子从A移动到B。
如果有3个盘子,那么根据2个盘子的结论,可以借助c将盘子1上的两个盘子从A移动到B;将盘子1从A移动到C,A变成空座;借助A座,将B上的两个盘子移动到C。

upload successful
其中这个就相当于在1~n和第n个之间切了一刀,这样比那个在第一个和第二个到第n-1个之间切一刀好,前者是等价的,并且减小了问题的规模。
upload successful
这三个柱子的角色再变,首先是C协助,后来B协助,再后来A协助、

1
2
3
4
5
6
7
8
9
10
11
12
  ***以此类推,上述的思路可以一直扩展到 n 个盘子的情况,将将较小的 n-1个盘子看做一个整体,也就是我们要求的子问题***,以借助B塔为例,可以借助空塔B将盘子A上面的 n-1 个盘子从A移动到B;将A最大的盘子移动到C,A变成空塔;借助空塔A,将B塔上的 n-2 个盘子移动到A,将C最大的盘子移动到C,B变成空塔…
  根据以上的分析,不难写出程序:
  void Hanoi (int n, char A, char B, char C){
    if (n==1){ //end condition
        move(A,B);//‘move’ can be defined to be a print function
    }
    else{
        Hanoi(n-1,A,C,B);//move sub [n-1] pans from A to B
        move(A,C);//move the bottom(max) pan to C
        Hanoi(n-1,B,A,C);//move sub [n-1] pans from B to C
    }
}

upload successful

  • 递归的特点
    1. 每一级函数调用时都有自己的变量,但是函数代码并不会得到复制,如计算5的阶乘时每递推一次变量都不同;
    2. 每次调用都会有一次返回,如计算5的阶乘时每递推一次都返回进行下一次;
    3. 递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序;
      就是在递归函数前后的语句的执行顺序
    4. 递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反;
    5. 递归函数中必须有终止语句。
  • 效率

    • 系统栈(也叫核心栈、内核栈)  
      是内存中属于操作系统空间的一块区域,其主要用途为: (1)保存中断现场,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出; (2)保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。

    • 用户栈  
      是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。
      我们编写的递归程序属于用户程序,因此使用的是用户栈。

    • 栈溢出  
      函数调用的参数是通过栈空间来传递的,在调用过程中会占用线程的栈资源。而递归调用,只有走到最后的 结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放,如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,导致程序的异常退出。

  • 每一个递归程序都遵循相同的基本步骤
    • (1) 初始化算法。递归程序通常需要一个开始时使用的种子值(seed value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数, 这个函数是非递归的,但可以为递归计算设置种子值。
    • (2) 检查要处理的当前值是否已经与基线条件相匹配。如果匹配,则进行处理并返回值。
    • (3) 使用更小的或更简单的子问题(或多个子问题(多个子问题就是斐波那锲那种的形式,这样与切蛋糕有些区别)(子问题就是重复的那部分,不过就是规模变小了))来重新定义答案。
    • (4) 对子问题运行算法。
    • (5) 将结果合并入答案的表达式。
    • (6) 返回结果。

例如:

1
2
3
4
5
6
7
8
9
10
/*
f1(n):求n的阶乘
*找重复:n*(n-1)的阶乘,求n-1的阶乘是原问题的重复(规模更小),即子问题
*找变化:找重复中的变化量,作为参数
*找边界:通过参数变化的趋势找出口 
*/
int f1(int n){
	if(n==1)return 1;
	return n*f1(n-1);
}
  • 函数所做的最后一件事情是一个函数调用(递归的或者非递归的),这被称为 尾部调用(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的排列

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
28
29
30
31
32
#include<bits/stdc++.h> 
using namespace std;
//产生排列组合的递归写法 
void pai(char t[],int k,int n){
		if(k==n-1){   //输出这个排列,k=n-1就是 递归出口
			for(int i=0;i<n;i++){
				cout<<t[i]<<" ";
			}
			cout<<endl;
		}else{
			for(int i=k;i<n;i++){
				 //一次挑选n个元素中的一个,和前位置替换
				int tmp=t[i];t[i]=t[k];t[k]=tmp;
				//再对其余的n-1个字母一次挑选
				pai(t,k+1,n);
				//在换回到原来的状态 
				tmp=t[i];t[i]=t[k];t[k]=tmp;
			}
		}
	}
int main (){ 
	char t[3]={'a','b','c'};  //t为数组 
	int k=0;  //k为起始排列值 
	int n=3;   //n为数组长度,是数组声明的时候的下标 
	cout<<"第一种方法:"<<endl; 
	pai(t,0,n);
	cout<<"第二种方法:"<<endl;
	do{
		cout<<t[0]<<" "<<t[1]<<" "<<t[2]<<endl;
	}while(next_permutation(t,t+3));//参数3指的是要进行排列的长度 
    //如果存在t之后的排列,就返回true。如果a是最后一个排列没有后继,返回false,每执行一次,t就变成它的后继;若排列本来就是最大的了没有后继,则next_permutation执行后,会对排列进行字典升序排序,相当于循环
}

下面这道题就是知道最后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;	
    }
    

upload successful

  • 日本著名数学游戏专家中村义作教授提出这样一个问题:父亲将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; 以此使用递归算法可以算出各个孩子原来手中的橘子数。

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
28
public class questionfour {
/**
 * 
 * @param n  表示第几个儿子
 * @param befor  表示为分配之前就的桔子数
 * @param After	表示分配之后的桔子数
 * @param m		分配的比例
 * @return
 */
    ???????
public int answer(int n,int befornum, int afternum,int m ){
	if(n>6){
		return 0;
	}else{
		System.out.println("老"+n+"原有的桔子数"+befornum);
		//分给下一个人的桔子数
		int givenum = afternum/m;
		//下一个人的桔子数
		int nextBeforenum = 420*(m-1)/(m-2)-givenum;
		//下一人加上之前的桔子数的总数
		int afterGetnum = nextBeforenum+givenum;
		return answer(n+1,nextBeforenum,afterGetnum,m-1);
	}
}
public static void main(String[] args){
	questionfour question4 = new questionfour();
	question4.answer(1, 240, 240, 8);
}

upload successful

  • 八皇后问题 ?????

问题描述:经典的八皇后问题,即在一个8*8的棋盘上放8个皇后,使得这8个皇后无法互相攻击( 任意2个皇后不能处于同一行,同一列或是对角线上),输出所有可能的摆放情况。

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
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;
int c[20],n=8,cnt=0;
void print(){ //打印
    for(int i=0;i<n;++i){
        for(int j=0;j<n;++j){
            if(j==c[i])cout<<"1 ";
            else cout<<"0 ";
        }
        cout<<endl;
    }
    cout<<endl;
}
void search(int r){
    if(r==n){ //当已经排到最后一个皇后的时候位置一定固定。【条件2】
        print();
        ++cnt;
        return;
    }
    for(int i=0;i<n;++i){ //r表示第几行  标号1
        c[r]=i; //c[r]=i,表示第r行第i列
        int ok=1;
        for(int j=0;j<r;++j)
            if(c[r]==c[j]||r-j==c[r]-c[j]||r-j==c[j]-c[r]){ 
            //判断是否在同一列,是否在主对角线或副对角线上【条件1】
                ok=0;
                break; 
            //这会跳出离这个最近的for循环,所以本题中会执行标号1的语句
            }
        if(ok)search(r+1); //r+1必定不在同一行,故不用判断是否在一行
    }
}
int main(){
    search(0);
    cout<<cnt<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值