欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

递归与循环的区别及应用

程序员文章站 2024-03-18 09:11:58
...

         递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。

   循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。      

递归的内涵

1、定义 (什么是递归?)

   在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。

2、递归思想的内涵(递归的精髓是什么?)

   正如上面所描述的场景,递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。

递归与循环的区别及应用

        更直接地说,递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。

递归的三要素

1). 明确递归终止条件

   我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。

2). 给出递归终止时的处理办法

   我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。

3). 提取重复的逻辑,缩小问题规模*

   我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。

模型一: 在递去的过程中解决问题

function recursion(大规模){
    if (end_condition){      // 明确的递归终止条件
        end;   // 简单情景
    }else{            // 在将问题转换为子问题的每一步,解决该步中剩余部分的问题
        solve;                // 递去
        recursion(小规模);     // 递到最深处后,不断地归来
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

模型二: 在归来的过程中解决问题

function recursion(大规模){
    if (end_condition){      // 明确的递归终止条件
        end;   // 简单情景
    }else{            // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
        recursion(小规模);     // 递去
        solve;                // 归来
    }
}

 在我们实际学习工作中,递归算法一般用于解决三类问题:

   (1). 问题的定义是按递归定义的(Fibonacci函数,阶乘,…);

   (2). 问题的解法是递归的(有些问题只能使用递归方法来解决,例如,汉诺塔问题,…);

   (3). 数据结构是递归的(链表、树等的操作,包括树的遍历,树的深度,…)。

 经典递归问题实战

  1. 第一类问题:问题的定义是按递归定义的

(1). 阶乘

public class Factorial {
    /**     
     *  阶乘的递归实现
     */
    public static long f(int n){
        if(n == 1)   // 递归终止条件 
            return 1;    // 简单情景

        return n*f(n-1);  // 相同重复逻辑,缩小问题的规模
    }

--------------------------------我是分割线-------------------------------------

    /**     
     * @description 阶乘的非递归实现  
     */
    public static long f_loop(int n) {
        long result = n;
        while (n > 1) {
            n--;
            result = result * n;
        }
        return result;
    }
}
  • 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
  • 38
  • 39
  • 40

(2). 斐波纳契数列

/** 
* Title: 斐波纳契数列 

* Description: 斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、…… 
* 在数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。 

* 两种递归解法:经典解法和优化解法 
* 两种非递归解法:递推法和数组法 
*

 * @author rico
 */
public class FibonacciSequence {

    /**
     * @description 经典递归法求解
     * 
     * 斐波那契数列如下:
     * 
     *  1,1,2,3,5,8,13,21,34,...
     * 
     * 那么,计算fib(5)时,需要计算1次fib(4),3次fib(3),3次fib(2)和两次fib(1),即:
     * 
     *  fib(5) = fib(4) + fib(3)
     *  
     *  fib(4) = fib(3) + fib(2) ;fib(3) = fib(2) + fib(1)
     *  
     *  fib(3) = fib(2) + fib(1)
     *  
     * 这里面包含了许多重复计算,而实际上我们只需计算fib(4)、fib(3)、fib(2)和fib(1)各一次即可,
     * 后面的optimizeFibonacci函数进行了优化,使时间复杂度降到了O(n).
     * 
     */
    public static int fibonacci(int n) {
        if (n == 1 || n == 2) {     // 递归终止条件
            return 1;       // 简单情景
        }
        return fibonacci(n - 1) + fibonacci(n - 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

——————————–我是分割线————————————-

/**     
 * @description 对经典递归法的优化
 * 
 * 斐波那契数列如下:
 * 
 *  1,1,2,3,5,8,13,21,34,...
 * 
 * 那么,我们可以这样看:fib(1,1,5) = fib(1,2,4) = fib(2,3,3) = 5
 * 
 * 也就是说,以1,1开头的斐波那契数列的第五项正是以1,2开头的斐波那契数列的第四项,
 * 而以1,2开头的斐波那契数列的第四项也正是以2,3开头的斐波那契数列的第三项,
 * 更直接地,我们就可以一步到位:fib(2,3,3) = 2 + 3 = 5,计算结束。 
 * 
 * 注意,前两个参数是数列的开头两项,第三个参数是我们想求的以前两个参数开头的数列的第几项。
 * 
    * 时间复杂度:O(n)
    */
    public static int optimizeFibonacci(int first, int second, int n) {
        if (n > 0) {
            if(n == 1){    // 递归终止条件
                return first;       // 简单情景
            }else if(n == 2){            // 递归终止条件
                return second;      // 简单情景
            }else if (n == 3) {         // 递归终止条件
                return first + second;      // 简单情景
            }
            return optimizeFibonacci(second, first + second, n - 1);  // 相同重复逻辑,缩小问题规模
        }
        return -1;
    }

--------------------------------我是分割线-------------------------------------

    /**
     * @description 非递归解法:有去无回
     */
    public static int fibonacci_loop(int n) {

        if (n == 1 || n == 2) {   
            return 1;
        }

        int result = -1;
        int first = 1;      // 自己维护的"栈",以便状态回溯
        int second = 1;     // 自己维护的"栈",以便状态回溯

        for (int i = 3; i <= n; i++) { // 循环
            result = first + second;
            first = second;
            second = result;
        }
        return result;
    }

--------------------------------我是分割线-------------------------------------

/**     
     * 使用数组存储斐波那契数列
     */
    public static int fibonacci_array(int n) {
        if (n > 0) {
            int[] arr = new int[n];   // 使用临时数组存储斐波纳契数列
            arr[0] = arr[1] = 1;

            for (int i = 2; i < n; i++) {   // 为临时数组赋值
                arr[i] = arr[i-1] + arr[i-2];
            }
            return arr[n - 1];
        }
        return -1;
    }
}
  • 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
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

(3). 杨辉三角的取值

  /**
    * Title: 杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数。
    * 它的一个重要性质是:三角形中的每个数字等于它两肩上的数字相加。
    * 
    * 例如,下面给出了杨辉三角形的前4行: 
    *    1 
    *   1 1
    *  1 2 1
    * 1 3 3 1
    * @description 递归获取杨辉三角指定行、列(从0开始)的值
    *              注意:与是否创建杨辉三角无关
    * @author rico 
    * @x  指定行
    * @y  指定列  
    */
    public static int getValue(int x, int y) {
        if(y <= x && y >= 0){
            if(y == 0 || x == y){   // 递归终止条件
                return 1; 
            }else{ 
                // 递归调用,缩小问题的规模
                return getValue(x-1, y-1) + getValue(x-1, y); 
            }
        }
        return -1;
    } 
}
  • 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

(4). 回文字符串的判断

/** 
* Title: 回文字符串的判断 
* Description: 回文字符串就是正读倒读都一样的字符串。如”98789”, “abccba”都是回文字符串 

* 两种解法: 
* 递归判断; 
* 循环判断; 
*

 * @author rico       
 */      
public class PalindromeString {

    /**     
     *  递归判断一个字符串是否是回文字符串  
     */
    public static boolean isPalindromeString_recursive(String s){
        int start = 0;
        int end = s.length()-1;
        if(end > start){   // 递归终止条件:两个指针相向移动,当start超过end时,完成判断
            if(s.charAt(start) != s.charAt(end)){
                return false;
            }else{
                // 递归调用,缩小问题的规模
                return isPalindromeString_recursive(s.substring(start+1).substring(0, end-1));
            }
        }
        return true;
    }

--------------------------------我是分割线-------------------------------------

    /**     
     *循环判断回文字符串
     */
    public static boolean isPalindromeString_loop(String s){
        char[] str = s.toCharArray();
        int start = 0;
        int end = str.length-1;
        while(end > start){  // 循环终止条件:两个指针相向移动,当start超过end时,完成判断
            if(str[end] != str[start]){
                return false;
            }else{
                end --;
                start ++;
            }
        }
        return true;
    }
}
  • 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
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

(5). 字符串全排列

递归解法 
/** 
* @description 从字符串数组中每次选取一个元素,作为结果中的第一个元素;然后,对剩余的元素全排列

     * @author rico
     * @param s
     *            字符数组
     * @param from
     *            起始下标
     * @param to
     *            终止下标
     */
    public static void getStringPermutations3(char[] s, int from, int to) {
        if (s != null && to >= from && to < s.length && from >= 0) { // 边界条件检查
            if (from == to) { // 递归终止条件
                System.out.println(s); // 打印结果
            } else {
                for (int i = from; i <= to; i++) {
                    swap(s, i, from); // 交换前缀,作为结果中的第一个元素,然后对剩余的元素全排列
                    getStringPermutations3(s, from + 1, to); // 递归调用,缩小问题的规模
                    swap(s, from, i); // 换回前缀,复原字符数组
                }
            }
        }
    }

    /**
     * 对字符数组中的制定字符进行交换
     */
    public static void swap(char[] s, int from, int to) {
        char temp = s[from];
        s[from] = s[to];
        s[to] = temp;
    }