递归算法讲解

一. 引子

   大师 L. Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:人理解迭代,神理解递归。毋庸置疑地,递归确实是一个奇妙的思维方式。对一些简单的递归问题,我们总是惊叹于递归描述问题的能力和编写代码的简洁,但要想真正领悟递归的精髓、灵活地运用递归思想来解决问题却并不是一件容易的事情。在正式介绍递归之前,我们首先引用知乎用户李继刚(https://www.zhihu.com/question/20507130/answer/15551917)对递归和循环的生动解释:

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

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

   上面的比喻形象地阐述了递归与循环的内涵,那么我们来思考以下几个问题:

什么是递归呢? 
递归的精髓(思想)是什么? 
递归和循环的区别是什么? 
什么时候该用递归? 
使用递归需要注意哪些问题? 
递归思想解决了哪些经典的问题? 
这些问题正是笔者准备在本文中详细阐述的问题。

二. 递归的内涵

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

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

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

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

                    这里写图片描述

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

3、用归纳法来理解递归

   数学都不差的我们,第一反应就是递归在数学上的模型是什么,毕竟我们对于问题进行数学建模比起代码建模拿手多了。观察递归,我们会发现,递归的数学模型其实就是 数学归纳法,这个在高中的数列里面是最常用的了,下面回忆一下数学归纳法。

   数学归纳法适用于将解决的原问题转化为解决它的子问题,而它的子问题又变成子问题的子问题,而且我们发现这些问题其实都是一个模型,也就是说存在相同的逻辑归纳处理项。当然有一个是例外的,也就是归纳结束的那一个处理方法不适用于我们的归纳处理项,当然也不能适用,否则我们就无穷归纳了。总的来说,归纳法主要包含以下三个关键要素:

步进表达式:问题蜕变成子问题的表达式 
结束条件:什么时候可以不再使用步进表达式 
直接求解表达式:在结束条件下能够直接计算返回值的表达式 
事实上,这也正是某些数学中的数列问题在利用编程的方式去解决时可以使用递归的原因,比如著名的斐波那契数列问题。

4、递归的三要素

   在我们了解了递归的基本思想及其数学模型之后,我们如何才能写出一个漂亮的递归程序呢?笔者认为主要是把握好如下三个方面:

1、明确递归终止条件;

2、给出递归终止时的处理办法;

3、提取重复的逻辑,缩小问题规模。
  • 1
  • 2
  • 3
  • 4
  • 5

1). 明确递归终止条件

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

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

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

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

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

5、递归算法的编程模型

   在我们明确递归算法设计三要素后,接下来就需要着手开始编写具体的算法了。在编写算法时,不失一般性,我们给出两种典型的递归算法设计模型,如下所示。

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

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6、递归的应用场景

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

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

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

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

  在下文我们将给出递归算法的一些经典应用案例,这些案例基本都属于第三种类型问题的范畴。

三. 递归与循环

   递归与循环是两种不同的解决问题的典型思路。递归通常很直白地描述了一个问题的求解过程,因此也是最容易被想到解决方式。循环其实和递归具有相同的特性,即做重复任务,但有时使用循环的算法并不会那么清晰地描述解决问题步骤。单从算法设计上看,递归和循环并无优劣之别。然而,在实际开发中,因为函数调用的开销,递归常常会带来性能问题,特别是在求解规模不确定的情况下;而循环因为没有函数调用开销,所以效率会比递归高。递归求解方式和循环求解方式往往可以互换,也就是说,如果用到递归的地方可以很方便使用循环替换,而不影响程序的阅读,那么替换成循环往往是好的。问题的递归实现转换成非递归实现一般需要两步工作:

   (1). 自己建立“堆栈(一些局部变量)”来保存这些内容以便代替系统栈,比如树的三种非递归遍历方式;

   (2). 把对递归的调用转变为对循环处理。

   特别地,在下文中我们将给出递归算法的一些经典应用案例,对于这些案例的实现,我们一般会给出递归和非递归两种解决方案,以便读者体会。

四. 经典递归问题实战

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

(1). 阶乘

/**
 * Title: 阶乘的实现 
 * Description:
 *      递归解法
 *      非递归解法
 * @author rico
 */
public class Factorial {
    /**     
     * @description 阶乘的递归实现
     * @author rico       
     * @created 2017年5月10日 下午8:45:48     
     * @param n
     * @return     
     */
    public static long f(int n){
        if(n == 1)   // 递归终止条件 
            return 1;    // 简单情景

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

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

    /**     
     * @description 阶乘的非递归实现
     * @author rico       
     * @created 2017年5月10日 下午8:46:43     
     * @param n
     * @return     
     */
    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).
     * 
     * @author rico
     * @created 2017年5月10日 下午12:00:42
     * @param n
     * @return
     */
    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,计算结束。 
 * 
 * 注意,前两个参数是数列的开头两项,第三个参数是我们想求的以前两个参数开头的数列的第几项。
 * 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
    * 时间复杂度:O(n)
     * 
     * @author rico       
     * @param first 数列的第一项
     * @param second 数列的第二项
     * @param n 目标项
     * @return     
     */
    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 非递归解法:有去无回
     * @author rico
     * @created 2017年5月10日 下午12:03:04
     * @param n
     * @return
     */
    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;
    }

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

/**     
     * @description 使用数组存储斐波那契数列
     * @author rico       
     * @param n
     * @return     
     */
    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). 杨辉三角的取值

/**     
 * @description 递归获取杨辉三角指定行、列(从0开始)的值
 *              注意:与是否创建杨辉三角无关
  • 1
  • 2
  • 3
  • 4
    * @author rico 
     * @x  指定行
     * @y  指定列    
     */
  /**
    * 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 {

    /**     
     * @description 递归判断一个字符串是否是回文字符串
     * @author rico       
     * @created 2017年5月10日 下午5:45:50     
     * @param s
     * @return     
     */
    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;
    }

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

    /**     
     * @description 循环判断回文字符串
     * @author rico       
     * @param s
     * @return     
     */
    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); // 换回前缀,复原字符数组
                }
            }
        }
    }

    /**
     * @description 对字符数组中的制定字符进行交换
     * @author rico
     * @param s
     * @param from
     * @param to
     */
    public static void swap(char[] s, int from, int to) {
        char temp = s[from];
        s[from] = s[to];
        s[to] = temp;
    }
  • 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

非递归解法(字典序全排列) 
/** 
* Title: 字符串全排列非递归算法(字典序全排列) 
* Description: 字典序全排列,其基本思想是: 
* 先对需要求排列的字符串进行字典排序,即得到全排列中最小的排列. 
* 然后,找到一个比它大的最小的全排列,一直重复这一步直到找到最大值,即字典排序的逆序列. 

* 不需要关心字符串长度 

* @author rico 
*/ 
public class StringPermutationsLoop {

/**
 * @description 字典序全排列
 * 
 * 设一个字符串(字符数组)的全排列有n个,分别是A1,A2,A3,...,An
 * 
 * 1. 找到最小的排列 Ai
 * 2. 找到一个比Ai大的最小的后继排列Ai+1
 * 3. 重复上一步直到没有这样的后继
 * 
 * 重点就是如何找到一个排列的直接后继:
 * 对于字符串(字符数组)a0a1a2……an,
 * 1. 从an到a0寻找第一次出现的升序排列的两个字符(即ai < ai+1),那么ai+1是一个极值,因为ai+1之后的字符为降序排列,记 top=i+1;
 * 2. 从top处(包括top)开始查找比ai大的最小的值aj,记 minMax = j;
 * 3. 交换minMax处和top-1处的字符;
 * 4. 翻转top之后的字符(包括top),即得到一个排列的直接后继排列
 * 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  * @author rico
     * @param s
     *            字符数组
     * @param from
     *            起始下标
     * @param to
     *            终止下标
     */
    public static void getStringPermutations4(char[] s, int from, int to) {

        Arrays.sort(s,from,to+1);  // 对字符数组的所有元素进行升序排列,即得到最小排列 
        System.out.println(s);    

        char[] descendArr = getMaxPermutation(s, from, to); // 得到最大排列,即最小排列的逆序列

        while (!Arrays.equals(s, descendArr)) {  // 循环终止条件:迭代至最大排列
            if (s != null && to >= from && to < s.length && from >= 0) { // 边界条件检查
                int top = getExtremum(s, from, to); // 找到序列的极值
                int minMax = getMinMax(s, top, to);  // 从top处(包括top)查找比s[top-1]大的最小值所在的位置
                swap(s, top - 1, minMax);  // 交换minMax处和top-1处的字符
                s = reverse(s, top, to);   // 翻转top之后的字符
                System.out.println(s);
            }
        }
    }

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

    /**     
     * @description 获取序列的极值
     * @author rico       
     * @param s 序列
     * @param from 起始下标
     * @param to 终止下标
     * @return     
     */
    public static int getExtremum(char[] s, int from, int to) {
        int index = 0;
        for (int i = to; i > from; i--) {
            if (s[i] > s[i - 1]) {
                index = i;
                break;
            }
        }
        return index;
    }

    /**     
     * @description 从top处查找比s[top-1]大的最小值所在的位置
     * @author rico       
     * @created 2017年5月10日 上午9:21:13     
     * @param s
     * @param top 极大值所在位置
     * @param to
     * @return     
     */
    public static int getMinMax(char[] s, int top, int to) {
        int index = top;
        char base = s[top-1];
        char temp = s[top];
        for (int i = top + 1; i <= to; i++) {
            if (s[i] > base && s[i] < temp) {
                temp = s[i];
                index = i;
            }
            continue;
        }
        return index;
    }

    /**     
     * @description 翻转top(包括top)后的序列
     * @author rico       
     * @param s
     * @param from
     * @param to
     * @return     
     */
    public static char[] reverse(char[] s, int top, int to) {
        char temp;
        while(top < to){
            temp = s[top];
            s[top] = s[to];
            s[to] = temp;
            top ++;
            to --;
        }
        return s;
    }

    /**     
     * @description 根据最小排列得到最大排列
     * @author rico       
     * @param s 最小排列
     * @param from 起始下标
     * @param to 终止下标
     * @return     
     */
    public static char[] getMaxPermutation(char[] s, int from, int to) {
        //将最小排列复制到一个新的数组中
        char[] dsc = Arrays.copyOfRange(s, 0, s.length);
        int first = from;
        int end = to;
        while(end > first){  // 循环终止条件
            char temp = dsc[first];
            dsc[first] = dsc[end];
            dsc[end] = temp;
            first ++;
            end --;
        }
        return dsc;
    }
  • 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
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123

(6). 二分查找

/** 
* @description 二分查找的递归实现 
* @author rico 
* @param array 目标数组 
* @param low 左边界 
* @param high 右边界 
* @param target 目标值 
* @return 目标值所在位置 
*/

public static int binarySearch(int[] array, int low, int high, int target) {

        //递归终止条件
        if(low <= high){
            int mid = (low + high) >> 1;
            if(array[mid] == target){
                return mid + 1;  // 返回目标值的位置,从1开始
            }else if(array[mid] > target){
                // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
                binarySearch(array, low, mid-1, target);
            }else{
                // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
                binarySearch(array, mid+1, high, target);
            }
        }
        return -1;   //表示没有搜索到
    }

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

/**     
     * @description 二分查找的非递归实现
     * @author rico       
     * @param array 目标数组
     * @param low 左边界
     * @param high 右边界
     * @param target 目标值
     * @return 目标值所在位置
     */
public static int binarySearchNoRecursive(int[] array, int low, int high, int target) {

        // 循环
        while (low <= high) {
            int mid = (low + high) >> 1;
            if (array[mid] == target) {
                return mid + 1; // 返回目标值的位置,从1开始
            } else if (array[mid] > target) {
                // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
                high = mid -1;
            } else {
                // 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
                low = mid + 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
  1. 第二类问题:问题解法按递归算法实现

(1). 汉诺塔问题

/** 
* Title: 汉诺塔问题 
* Description:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上。 
* 有一个和尚想把这64个盘子从A座移到C座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下, 
* 小盘在上。在移动过程中可以利用B座。要求输入层数,运算后输出每步是如何移动的。 
*

 * @author rico
 */
public class HanoiTower {

    /**     
     * @description 在程序中,我们把最上面的盘子称为第一个盘子,把最下面的盘子称为第N个盘子
     * @author rico       
     * @param level:盘子的个数
     * @param from 盘子的初始地址
     * @param inter 转移盘子时用于中转
     * @param to 盘子的目的地址
     */
    public static void moveDish(int level, char from, char inter, char to) {

        if (level == 1) { // 递归终止条件
            System.out.println("从" + from + " 移动盘子" + level + " 号到" + to);
        } else {
            // 递归调用:将level-1个盘子从from移到inter(不是一次性移动,每次只能移动一个盘子,其中to用于周转)
            moveDish(level - 1, from, to, inter); // 递归调用,缩小问题的规模
            // 将第level个盘子从A座移到C座
            System.out.println("从" + from + " 移动盘子" + level + " 号到" + to); 
            // 递归调用:将level-1个盘子从inter移到to,from 用于周转
            moveDish(level - 1, inter, from, to); // 递归调用,缩小问题的规模
        }
    }

    public static void main(String[] args) {
        int nDisks = 30;
        moveDish(nDisks, 'A', 'B', 'C');
    }
  • 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
  1. 第三类问题:数据的结构是按递归定义的

(1). 二叉树深度

/** 
* Title: 递归求解二叉树的深度 
* Description: 
* @author rico 
* @created 2017年5月8日 下午6:34:50 
*/

public class BinaryTreeDepth {

    /**     
     * @description 返回二叉数的深度
     * @author rico       
     * @param t
     * @return     
     */
    public static int getTreeDepth(Tree t) {

        // 树为空
        if (t == null) // 递归终止条件
            return 0;

        int left = getTreeDepth(t.left); // 递归求左子树深度,缩小问题的规模
        int right = getTreeDepth(t.left); // 递归求右子树深度,缩小问题的规模

        return left > right ? left + 1 : right + 1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

(2). 二叉树深度

/**
 * @description 前序遍历(递归)
 * @author rico
 * @created 2017年5月22日 下午3:06:11
 * @param root
 * @return
 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
 public String preOrder(Node root) {
        StringBuilder sb = new StringBuilder(); // 存到递归调用栈
        if (root == null) {   // 递归终止条件
            return "";     // ji 
        }else { // 递归终止条件
            sb.append(root.data + " "); // 前序遍历当前结点
            sb.append(preOrder(root.left)); // 前序遍历左子树
            sb.append(preOrder(root.right)); // 前序遍历右子树
            return sb.toString();
        }       
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
个人分类:  c/c++
  • weixin_39382874
    weixin_39382874 2018-05-01 10:16:32 #4楼
    你好,请问类似于全排列生成之类的递归程序的重复逻辑是怎样的呢。全排列函数在下一次递归时并没有产生下一个排列,而是逐步生成了本个排列,在回溯时才解决下一个排列,对于这种递归语句前有solve,递归语句后也有solve的递归不是很理解,能请你再解释下吗
  • sinat_37756716
    sinat_37756716 2018-04-19 14:26:56 #3楼
    二分法查找递归那里,应该是return binarySearch(array, low, mid-1, target);和return binarySearch(array, mid+1,high, target).否则结果无法传回
    查看回复(1)
  • qq_41949009
    qq_41949009 2018-04-03 16:33:19 #2楼
    那么,计算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(3)计算3次啊
  • qq_38342614
    何须低头 2018-03-20 14:31:38 #1楼
    楼猪,可以转吗?看的好头晕!
    查看回复(2)
查看 7 条热评

递归算法深入浅出一:递归理论概述和常见递归算法罗列

递归可以说是很多基础程序员甚至有一定开发能力程序员包括我这个小菜的痛点和痒点!但有一句话很经典: 出来混,迟早是要还的!   将我自己的一些总结写出来和大家分享下,有意见、建议的朋友麻烦高抬贵手...

nthack5730 nthack5730 

2017-03-24 01:54:00 

阅读数:19574 

怎么更好地终极理解递归算法

递归真是个奇妙的思维方式。对一些简单的递归问题,我总是惊叹于递归描述问题和编写代码的简洁。但是总感觉没能融会贯通地理解递归,有时尝试用大脑去深入“递归”,层次较深时便常产生进不去,出不来的感觉。这种状...

StruggleShu StruggleShu 

2016-04-03 17:31:35 

阅读数:14066 

深入理解递归算法 - CSDN博客

递归算法是一种经典的算法,在很多时候可以使代码变得非常简洁,但它也有一个很大的缺点,就是效率比较低。不管怎么说,这一算法在实际编码中还是有非常大的作用,以前...

2018-5-19

算法递归实现 C(m,n) - CSDN博客

https://blog.csdn.net/user_longling/article/details/9003071 文章标签: C 算法递归 个人分类: 【Algorithm】 想对作者说点什么? 我来说一句 ...

2018-5-3

递归(一)几个简单的递归例子

刚接触递归的同学,可能难以理解递归,难以理解的点可能很多,例如: 1.函数为什么可以在自己的内部又调用自己呢? 2.既然可以自己调用自己,那么递归运行过程中一定回有很多层相互嵌套,到底什么时候不再嵌套...

ten_sory ten_sory 

2017-03-20 16:15:27 

阅读数:26613 

递归算法的原理 - CSDN博客

递归算法:顾名思义,递和归;其实际也是根据栈的原理,后进先出,保证函数的返回值正确; 就拿斐波那契数列进行举例说明: 斐波那契数列算法,如:1,1,2,3,5,8,13...

2018-5-2

递归算法 - CSDN博客

一、基本概念            递归算法是一种直接或者间接调用自身函数或者方法的算法。Java递归算法是基于Java语言实现的递归算法递归算法的实质...

2018-5-16

递归入门

写在前面: 对于强大的递归。要想做到灵活运用,是需要花时间进行练习并总结。往往递归学习的入门也是难度也比较大,常常会处于看得明,却写不出的"尴尬"情况。 本人也是一名编程菜鸟,也常处于尴尬中。正因...

cbs612537 cbs612537 

2012-11-23 17:35:45 

阅读数:8184 

递归算法及经典递归例子代码实现

一、什么叫做递归? 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法; 递归函数就是直接或间接调用自身的函数,也就是自身调用自己; 二、一般什么时候使用递归?    递归时...

ikownyou ikownyou 

2017-03-24 17:28:19 

阅读数:36775 

递归算法详解

递归算法详解,更详细的理解,欢迎下载哦,谢谢啦,... 经典算法八皇后问题的详解以及回溯(递归)代码示例 时间:2018-5-2 递归算法示例 讲解 Java 时间:2018-2-27...

2018-5-7

Java归并算法递归实现 - CSDN博客

举报内容: Java归并算法递归实现 举报原因: 色情 政治 抄袭 广告 招聘 骂人 其他 原文地址: 原因补充: 最多只允许输入30个字加入...

2018-5-4

递归算法的特点

递归算法解决问题的特点:                 1)递归就是方法里调用自身。                 2)在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。      ...

shellingfordisme shellingfordisme 

2015-10-31 08:10:31 

阅读数:3951 

递归算法 - CSDN博客

算法细讲(ACM) 阅读量:1442111 篇 codeforces 阅读量:1500956 篇 C++学习 ...递归算法 yijian6149:栈是后进先出吧,博主第二条建议笔误了吧 ACM新手入门历...

2018-5-8

递归算法详解 - CSDN博客

递归算法详解 C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。     许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常...

2018-5-20

递归算法终极理解—用人脑理解递归算法

递归真是个奇妙的思维方式。自打我大二学习递归以来,对一些简单的递归问题,我总是惊叹于递归描述问题和编写代码的简洁。但是总感觉没能融会贯通地理解递归,有时尝试用大脑去深入“递归”,层次较深时便常产生进不...

u011995233 u011995233 

2014-04-24 20:51:39 

阅读数:6017 

关于递归实现过程的详解

最近在学数据结构的时候,碰到了递归,但由于自己一直对递归一知半解,所以不能全面的理解递归的过程到底是怎样实现的,下来研究了一下,觉得还是有所收获的。  假设我们用递归实现一个数的阶乘。int fun...

qq_32998153 qq_32998153 

2016-11-16 21:36:04 

阅读数:3502 

算法大总结之一:递归算法! - CSDN博客

上课,同学们好! 今天开始,我将讲解算法大总结。那么让我们开始吧。首先,我今天要讲的是:递归算法。 我觉得讲课要讲得透彻,必须让学生知其所以然。所以,我第一...

2018-5-18

undefined

递归函数工作原理

递归(recursion)就是子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己,是一种描述问题和解决问题的基本方法。      递归通常用来解决结构自相似的问题。所谓结构自相似,是指...

zhangchao3322218 zhangchao3322218 

2011-09-29 19:39:02 

阅读数:12324 

递归算法

目录: 1.简单递归定义 2.递归与循环的区别与联系 3.递归的经典应用 1.简单递归定义 什么叫递归?(先定义一个比较简单的说法,为了理解,不一定对) 递归:无限调用自身这个函数,每次调...

feizaoSYUACM feizaoSYUACM 

2017-02-08 00:20:28 

阅读数:12470 

递归算法的经典运用

递归(recursion):程序调用自身的编程技巧 递归满足两个条件: (1)有反复执行的过程(调用自身) (2)有跳出反复执行过程的条件(递归出口)递归例子(常用的地方): (1)阶乘 n...

OREO_GO OREO_GO 

2016-04-01 09:54:29 

阅读数:4789 

【算法】递归(recursion)+经典例题个人分析

定义(个人理解) 1.自己调用比自己小一个规模的自己。 2.有结束条件。 3.对问题的细化。 ps: 大家可以通过这个效应感性的感受一下递归。 德罗斯特效应: ******...

wait_for_taht_day5 wait_for_taht_day5 

2015-12-04 14:31:50 

阅读数:9797 

五大经典算法一 递归与分治

递归算法:直接或者间接不断反复调用自身来达到解决问题的方法。要求原始问题可以分解为相同问题的子问题。、 需要: 1 递归边界 2 自身调用 特点分析: 递归思路简单清晰,如果分析出将很快得到结果;递归...

yanerhao yanerhao 

2017-03-21 12:06:30 

阅读数:2045 

java递归算法总结

1.何为递归 个人理解就是自己调用自己,直到满足一个条件结束自己调用自己的过程,这个就是递归。举一个通俗的点的例子: 假设你在一个电影院,你想知道自己坐在哪一排,但是前面人很多,你懒得去数了,于是你问...

tomcat_2014 tomcat_2014 

2016-04-10 17:30:43 

阅读数:12690 

递归算法练习

题目:一个人赶着鸭子去每个村庄卖,每经过一个村子卖去所赶鸭子的一半又一只。这样他经过了七个村子后还剩两只鸭子,问他出发时共赶多少只鸭子?经过每个村子卖出多少只鸭子? 题目分析:经过7个村庄后还剩两只...

thegreatmz thegreatmz 

2017-05-21 14:12:26 

阅读数:1512 

递归算法学习

汉诺塔问题 如图,汉诺塔问题是指有三根杆子A,B,C。C杆上有若干碟子,把所有碟子从A杆上移到C杆上,每次只能移动一个碟子,大的碟子不能叠在小的碟子上面。求最少要移动多少次? 当n=1时:...

miaoca miaoca 

2017-03-14 09:18:28 

阅读数:877 

递归算法小结

写下这个题目,心里还是有点儿发虚的,因为自己作为一个算法新手,在这个地方大谈递归算法实在是有点儿不知道天高地厚的感觉。 先说这篇文章的性质以及适合人群,这篇文章是个人学习算法过程中的一个总结,没有太多...

cyfcsd cyfcsd 

2015-11-11 11:33:26 

阅读数:2889 

浅析递归算法的运行原理

递归,即程序(函数)通过直接或者间接调用自己的一个过程。 递归算法主要有四个特点: 1. 必须有可达到的终止条件,不然程序(函数)将陷入死循环(死锁); 2. 子过程可通过再次递归的方式调用求解或...

oooyou oooyou 

2017-03-17 20:58:01 

阅读数:1319 

C++递归算法经典实例详解

小白博客,记录多于科普

junloin junloin 

2017-03-02 22:25:03 

阅读数:9656 

编程思想之递归

我之前写过关于递归算法的博文,但作为编程思想系列的文章不得不再对它进行进一步深入的剖析。因为它是一种简单、常用又重要的一种编程思想。什么叫递归?举一个通俗的例子:有一个8俩重的苹果要你切成重量相等的若...

luoweifu luoweifu 

2014-12-24 00:03:46 

阅读数:15510 

个人资料

关注
原创
85
粉丝
19
喜欢
15
评论
11
等级:
访问:
2万+
积分:
1072
排名:
4万+
勋章:

最新文章

你可能感兴趣的:(数据结构和算法,c/c++)