数学归纳法和递归函数

1.什么是数学归纳法?

数学归纳法用于证明在自然数上的一些断言是否成立。

怎么证明断言对所有自然数成立?
第一步:证明N=1是成立的
第二步:证明N>1时,如果对于N-1成立,那么对于N成立

(第二步不是直接证明,而是先假设N-1成立,再利用这个结论证明N是成立的)

例子:
用数学归纳法证明 1+2+3+…+n = n(n+1)/2
第一步:
1 = 1*2/2
第二步:
①假设对于n-1的情况下成立
1+2+3+…+(n-1) = (n-1)n/2
②利用假设结论带进去
1+2+3…+(n-1)+n = (n-1)n/2+n = n(n+1)/2

2.什么是递归函数?

当一个函数直接或间接地调用自身定义时就称为递归(recursive)。在思想上递归类似于数学归纳法。

编写递归程序的时候,关键是要牢记递归的四条基本法则:

1.基准情形。 必须有某些基准情形不用递归就能求解。
2.不断推进。 对于那些需要递归求解的情形。递归调用必须总能朝着基准情形的方向迈进。
3.设计法则。 假设所有的递归调用都能运行。
4.合成效益法则。 在求解一个问题的同一实例时,切勿在不同的递归调动中做重复性的工作。(摊还分析)

实现递归的三要素

1.方法中出现自己调用自己
2.要有分支
3.要有结束条件

3.递归的实现的例子

爬楼梯算法

已知一个楼梯有n个台阶,每次可以选择迈上一个或者两个台阶,求走完一共有多少种不同的走法。

	public static int climbStairs(int n) {

        if(n<=0)
            return 0;
        if(n==1){
            return 1;
        }
        if(n==2){
            return 2;
        }
        else
            return climbStairs(n-1)+climbStairs(n-2);
    }

分析一下这个算法:
A:如果有0个台阶,那么有0种走法,这个不用多说;
B:如果有1个台阶,那么有1种走法;
C:如果有2个台阶,那么有2种走法(一次走1个,走两次;一次走两个);
以上的B和C就是基础情形。
D:接下来就是递归了,如果台阶数目多于2个,那么首先第一步就有两种选择:第一次走1个,或者第一次走两个。这样除了第一次后边的走法就有了两种情形:climbStairs(n-1)和climbStairs(n-2)。这样一直递归下去,直到出现到了基础情形(即n=1或n=2的情形),递归到这个地方(基础情形),然后开始回溯 ,这就是所说的和递归密切相关的“回溯”了。回溯,顾名思义就是从结果倒着回去,找到整个过程,进而分析这个路径或者说是实现的过程。

需要注意的是,这个算法实现思路上简单,但是复杂度并没有降低,还牵扯回溯保存堆栈问题(其实递归的设计尽量避免这种嵌套两个的递归方式(climb(n)中包含climb(n-1)和climb(n-2)),这种操作会使得堆栈开辟空间随着n的增大以指数型增长,最终程序很容易崩溃),而且在台阶数目多到一定数量的时候会越界(走法次数会超出int的范围),所以递归程序很大程度上就是思想实现设计上简单理解一些。

汉诺塔问题

一次只能移动一个盘子;不能把大盘子放在小盘子上;除去盘子在两个柱子之间移动的瞬间,盘子必须都在柱子上。(在这三点要求下把盘子从起始柱子A全部移动到目标柱子C上)

在这里插入图片描述

代码如下:

基础情形:n==1的时候终止递归,进行回溯。

/**
 * 汉诺塔问题
 *
 *  n个的移动次数=(n-1)的移动次数+1+(n-1)的移动次数
 *
 *  1个的时候是1次
 *  2个的时候是 2*1+1
 *  3个的时候是 2*(2*1+1)+1
 *  ...
 *  n个的时候是 2^n-1
 *
 */
public class Hanoi {

    /**
     *
     * @param n 盘子的数目
     * @param origin 源座
     * @param assist 辅助座
     * @param destination 目的座
     */
    public void hanoi(int n, char origin, char assist, char destination) {
        if (n == 1) {
            move(origin, destination);
        } else {
            hanoi(n - 1, origin, destination, assist);
            move(origin, destination);
            hanoi(n - 1, assist, origin, destination);
        }
    }

    // Print the route of the movement
    private void move(char origin, char destination) {
        System.out.println("Direction:" + origin + "--->" + destination);
    }

    public static void main(String[] args) {
        Hanoi hanoi = new Hanoi();
        hanoi.hanoi(4, 'A', 'B', 'C');
    }


}

4.递归和循环

如果我们要重复地多次计算相同的问题,通常可以选择用递归或者循环两种不同的方法。递归是在一个函数的内部调用这个函数自身。而循环这是通过设置计算的初始值及终止条件,在一个范围内重复计算。比如求1+2+3+…+n,我们可以用递归或者循环两种方式求出结果。对应的代码如下:

 public  int AddFrom1ToN_Recursive(int n) {
    return n <= 0 ? 0 :n + AddFrom1ToN_Recursive(n - 1);
}

public int AddFrom1ToN_Iternative(int n) {
    int result = 0;
    for (int i = 0; i <= n; ++i)
        result += i;
    return result;
}

5.递归的缺点

递归虽然有简洁的优点,但它同时也有显著的缺点。

递归由于是函数调用自身,而函数调用是有空间和时间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回的地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。这就不难理解上述的例子中递归实现的效率不如循环。

另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。

除了效率以外,递归还有可能引起更严重的问题:调用栈溢出。前面分析中提到需要为每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致栈溢出。在上述的例子中,如果输入的参数比较小,如10,它们都能返回结果55.但如果输入的参数很大,比如5000,那么递归代码在运行的时间就会出错,但运行循环的代码能得到正确的结果12502500.
  
参考
https://www.cnblogs.com/vincently/p/4191734.html
https://blog.csdn.net/ares_xxm/article/details/68957829

猜你喜欢

转载自blog.csdn.net/u013728021/article/details/84590011