Java中的递归以及一些经典递归问题

一、递归的含义与理解:
在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。

递归包含了两重意思:递去和归来。有去且有回的一种思想。

递去:将大规模问题分解成若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。
归来:问题是从大到小的演化过程,但是分解小问题的过程是有一个临界点的,一旦到达终点就不再继续分解问题,而是按照问题的分解路线原路返回。

递归的三要素:
1、明确的终止条件。
2、给出递归终止的解决办法。
3、提取重复的逻辑,解决从大规模到小规模的问题。

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

二、递归的经典问题:

1、阶乘:实现 n!
阶乘的算法分析:
n的阶乘实质上也就是n乘n-1的阶乘。而n-1的阶乘也就是n-1乘n-2的阶乘…如此往复,最后会得到3的阶乘就是3乘2的阶乘,2的阶乘也就是2乘1的阶乘,也就是1。
阶乘的递归三要素:
(1)、终止条件:n等于2(或者n等于1)。
(2)、终止条件的解决方法:返回2。
(3)、提取重复的逻辑:返回n乘f(n-1)。

public static int factoryByRecur(int n){      
//让n=2作为终止条件给出
    if(n == 2){
        //终止条件的解决办法
        return 2;
    }
        //提取重复的逻辑
    return n * factoryByRecur(n-1);
}

整个递归运算阶乘的过程就是从n的阶乘拆解到n-1的阶乘,以此类推逐步拆解到2的阶乘得到结果2,然后这时整个算式中没有函数式,直接从2按照拆解过程进行一个原路返回的乘法运算,从2一直乘到n这样就计算了n的阶乘。

2、斐波那契数列的实现:
斐波那契数列是一个前两项均为1的从第3项开始,每一项都等于前两项之和的数列。
现在要实现输入一个数n,求出斐波那契数列在第n项的值。

斐波那契数列的递归三要素:
(1)、终止条件:n为1或者2,因为斐波那契数列的这两项的值都是1。
(2)、终止条件的解决方法:返回1,因为这两项的值都是1。
(3)、提取重复的逻辑:f(n)=f(n-1)+f(n-2),同时f(n-1)=f(n-2)+f(n-3),f(n-2)=f(n-3)+f(n-4)…以此类推直到得到f(3)=f(2)+f(1)。

public static int fibonacciByRecur(int n){
    //n=1或n=2作为终止条件     
    if(n == 1 || n == 2){
        //满足终止条件的解决办法
        return 1;
    }
        //提取重复的逻辑
    return fibonacciByRecur(n-1) + fibonacciByRecur(n-2);
}

整个递归的过程就是:
递去:从f(n)=f(n-1)+f(n-2),f(n-1)=f(n-2)+f(n-3),f(n-2)=f(n-3)+f(n-4)这样逐步的进行拆解,直到拆解出f(3)=f(2)+f(1),这时由于f(2)和f(1)的数值已知,所以递去停止,开始归来。
归来:因为f(2)和f(1)的数值已知,所以就从f(3)=f(2)+f(1)开始计算得到f(3)的值,然后因为f(2)和f(3)的数值已知,所以f(4)=f(3)+f(2)计算f(4)的值。以此类推,可以沿之前递去拆解的步骤逆向去逐步计算出结果。

面试重点:斐波那契数列的递归算法优化。
根据上文我们可以知道,方法的调用是会占用计算机性能的,在实际程序编写的过程中,我们需要写的调用方法的次数应当越少越好。所以在可以完成任务的条件下递归的次数越少越好。
而根据我们上边的代码可以发现,从f(n-2)到f(3)都被重复调用方法运算了,这样对于程序的性能效率是不利的。

优化思想:将斐波那契数列运算的结果保存起来,这样在下一次需要的时候直接使用保存好的结果即可。

斐波那契数列:1 1 2 3 5 8 13 21 34…
以斐波那契数列中的5为例,从斐波那契的前两项看,5在第五个;从2开始,5在第三个。根据这样的思路,我们可以将斐波那契数列的某两个数作为前两项,然后根据前两项计算出数列的第三项,然后再把第二项当做方法递归的第一项把刚算出的第三项当做递归的第二项计算出第四项。以此类推,我们可以一直计算直到我们要求的那一项。

优化求斐波那契数列第n项递归的三要素:
(1)、终止条件:n为1或者2,因为这两项是我们要给出的
(2)、终止条件的解决方法:返回我们所给出的第一项和第二项的值
(3)、提取重复的逻辑:参数n,第一项和第二项计算第三项;参数n变为n-1(因为第二项当做新的第一项时,原来的第n项现在就是第n-1项。)第二项和第三项计算第四项;参数变为n-2(同理),第三项和第四项计算第五项…以此类推直到计算完成。

以第五项为例:f(1,1,5)=f(1,2,4)=f(2,3,3)=5

 public static int optimizedFibonacciByRecur(int first, int second, int n){
     if(n <= 0)  return 0; //参数合法性
  
     if(n == 1){
         return first;//给出终止条件 满足终止条件的解决办法
     }else if(n == 2){
         return second;//给出终止条件 满足终止条件的解决办法
     }else if(n == 3){
         return first + second;//给出终止条件 满足终止条件的解决办法
     }
        //提取重复的逻辑
     return optimizedFibonacciByRecur(second, first+second, n-1);
}

通过对这个优化算法的观察和理解我们可以发现对于这种类似于有规律的数列的递归问题的解决方法。很像是一个人赶路的过程,每递归一次,这个人就向前走了一段路程,然后他举例目标位置也就又接近了一段路程。

一个人要赶路100公里,每天走10公里。第一天开始的时候,他从0公里的位置开始,距离终点100公里;第二天开始的时候,他就从10公里的位置开始,此时距离终点90公里…以此类推10天走完。
求斐波那契数列的第10项,第一次递归开始的时候,从第一项开始,这时距离第十项还有九个数;第二次递归开始的时候,从第二项开始,这时距离第十项只有八个数…以此类推第八次递归求出结果。

3、二分查找的递归实现:
例、用二分查找在一个从小到大排列好的数组中查找是否含有我们需要的数字。

二分查找递归实现的三要素:
(1)、终止条件:中间只有一个数的时候(也就是不能再进行二分查找的时候),对那个数进行判断
(2)、终止条件的解决方法:对中间数判断是否等于查找目标。
(3)、提取重复的逻辑:如果中间数大于查找目标,那么起始部分不变,中间数变为下一次二分查找的终止部分;如果中间数小于查找目标,那么终止部分不变,中间数变为下一次二分查找的起始部分。

另外,二分查找要保证两端不能调换位置!起始端不能大于终止端。

public static int binarySearchByRecur(int[] array, int low, int high, int target){
    if(array == null || array.length == 0)  return -1;
    //参数合法性
    if(low <= high){
    //注意:二分查找要保证两端不能调换位置!
        int mid = (low + high) >> 1;
        //给出终止条件
        if(array[mid] == target){
            return mid;//满足终止条件的解决办法
        }else if(array[mid] > target){
            //提取重复的逻辑
        return binarySearchByRecur(array, low, mid-1, target);
//如果中间数大于查找目标,那么起始部分不变,中间数变为下一次二分查找的终止部分
        }else{
            return binarySearchByRecur(array, mid+1, high, target);
// 如果中间数小于查找目标,那么终止部分不变,中间数变为下一次二分查找的起始部分   
        }
    }
        return -1;
}

4、回文字符串判断的递归实现:
回文字符串:从左到右读和从右到左读完全一样的字符串。
判断思路:对要判断的字符串的两边不断截取逐个判断。字符串中正数第n个和倒数第n个要相等。

递归判断回文字符串的三要素:
(1)、终止条件:开始和终止点相同或者开始小于终止点。
(2)、终止条件的解决方法:如果到达终止条件之前没有返回false的话,那么返回true。
(3)、提取重复的逻辑:判断开始或者终止点,若符合要求,那么用字符串的substring方法将字符串的两点截掉,若不符合要求直接返回false。

注:substring方法:
语法:public String substring(int beginIndex)或public String substring(int beginIndex, int endIndex)
两种语法分别表达从字符串的第beginIndex项开始的部分截取出一个新的字符串或从beginIndex到endIndex部分截取出一个新的字符串。
substring方法中的从beginIndex项开始是含该项的,而到endIndex项截止是不含第endIndex项的。

public static boolean isPalindromeStrsByRecur(String strs){
    int start = 0;
    int end = strs.length()-1;
        //给出终止条件

    if(start < end){
         if(strs.charAt(start) != strs.charAt(end)){
         // 满足终止条件的解决办法
         return false;
         }
         //提取重复逻辑
         return isPalindromeStrsByRecur(strs.substring(1, end));
//从原字符串第二项开始截取直到原字符串倒数第二项截取为新的字符串再次判断
     }
       return true;
}

5、递归求杨辉三角指定行指定列的值:
杨辉三角
Title: 杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数。
它的一个重要性质是:三角形中的每个数字等于它两肩上的数字相加。
要求:递归获取杨辉三角指定行、列(从0开始)的值,注意:与是否创建杨辉三角无关。x 表示指定行,y表示指定列。
分析:例:下面给出了杨辉三角形的前4行:在这里我们用(x,y)来表示第x行第y列的元素。
1
1 1 (1,0)=(1,1)=1
1 2 1 (2,0)=(2,2)=1 ,(2,1)=(1,0)+(1,1)
1 3 3 1 (3,0)=(3,3)=1,(3,1)=(2,0)+(2,1),(3,2)=(2,1)+(2,2)
1 4 6 4 1 (4,0)=(4,4)=1,(4,1)=(3,0)+(3,1),(4,2)=(3,1)+(3,2),(4,3)=(3,2)+(3,3)
通过上面我们发现的规律我们可以发现,当杨辉三角的x和y相同或者y=0的时候,杨辉三角的值是1;当这两种情况之外的杨辉三角值的规律是:(x,y)=(x-1,y-1)+(x-1,y)

杨辉三角递归的三要素:
(1)、终止条件:x=0或者x=y。
(2)、终止条件的解决方法:返回1。
(3)、提取重复的逻辑:
递去:(x,y)=(x-1,y-1)+(x-1,y);(x-1,y-1)=(x-2,y-2)+(x-2,y-1),(x-1,y)=(x-2,y-1)+(x-2,y)…以此类推,直到我们最后得到(2,1)=(1,0)+(1,1)算出(2,1)的结果
归来:根据之前计算得到的(2,1)再去沿着之前递去的拆解路径原路返回去计算所求结果。

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;
    } 
}

注:杨辉三角的递归实现类似于斐波那契数列的递归实现并且也和斐波那契数列一样存在着重复调用方法的弊端,故在实际递归实现时也可以用斐波那契优化类似的思路来对其进行优化。

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

汉诺塔枚举分析:
例:
A座上有1个盘子时,直接挪到C座上即可。
A座上有2个盘子时,1号盘挪到B座,2号盘挪到C座,然后再把B座上的1号盘挪到C座。
A座上有3个盘子时,1号盘挪到C座,2号盘挪到B座(此时1号盘在C座,2号盘在B座,3号盘在A座),然后把1号盘从C座挪到B座,把3号盘从A座挪到C座(此时1、2号盘都在B座,3号盘在C座),然后把1号盘从B座挪回A座,然后把2号盘从B座挪回C座,最后把1号盘从A座挪回C座。

通过对二阶的汉诺塔的思路分析我们可以得出结论:
二阶汉诺塔的移动:实际上就是将1号盘从起始柱挪到过渡柱,再将2号盘从起始柱挪到目标柱,然后再将1号盘从过渡柱移到目标柱。
注意:这实际上就是将1个盘子的汉诺塔移动重复了三次!

通过对三阶的汉诺塔的思路分析我们可以发现:
三阶的汉诺塔实质上可以简化为二阶的汉诺塔,把1号和2号盘都当做一个整体的盘子。先把这个整体的盘子从起始柱(A座)挪到过渡柱(B座)上,然后再把3号盘从起始柱(A座)挪到目标柱(C座)上,然后再把1号和2号从过渡柱(B座)挪到目标柱(C座)上。

通过对二阶和三阶的汉诺塔的移动分析我们可以得到汉诺塔的规律:
即:n阶汉诺塔:分为三步:先将1到n-1的盘子看成一个整体盘子,将这个整体盘子起始柱挪到过渡柱,然后再将第n个盘子从起始柱挪到目标柱,然后再将1到n-1的整体盘子再从过渡柱挪到目标柱。

综上所述:我们可以发现,所有的高于一阶的汉诺塔,都可以理解为一阶的汉诺塔进行了三次重复的结果。

汉诺塔递归的三要素:
(1)、终止条件:盘子数为1
(2)、终止条件的解决方法:直接返回。
(3)、提取重复的逻辑:设盘子数为n个(n远大于2)。

递去:先将1到n-1的盘子A座挪到B座,然后再将第n个盘子从从A座挪到C座,然后再将1到n-1的盘子再从B座挪到C座(A是起始柱,B是过渡柱,C是目标柱)。
而从A座挪到B座挪动n-1个盘子(注意:这次移动中A是起始柱,C是过渡柱,B是目标柱),实际上就是先将1到n-2这n-2个盘子A座挪到C座,然后再将第n-1个盘子从从A座挪到B座,然后再将1到n-2的盘子再从C座挪到B座。
而将1到n-2这n-2个盘子A座挪到C座也可以拆解为1到n-3的盘子的移动和第n-2个盘子的移动这样的三部分过程。
以此类推,我们可以一直对盘子的移动进行分解处理。直到分解到二阶汉诺塔,然后二阶汉诺塔分解到一阶汉诺塔,此时满足递归终止条件,停止递去。

归来:沿之前拆解汉诺塔的路径逆推返回即可。得到1阶汉诺塔的移动过程之后,我们也就可以回推得出2阶汉诺塔的移动过程,得到2阶也就可以回推得出3阶汉诺塔…以此类推知道回推到n阶汉诺塔。

public static void moveDish(int dishes , char from, char middle, char to){
    //递归终止条件
    if(dishes == 1){
        System.out.println("from " + from + " " + dishes + "号 " + "to " + to);
        return;//终止条件的解决办法
     }
        //重复的逻辑
     moveDish(dishes-1, from, to, middle);
     //n阶汉诺塔拆解为n-1阶汉诺塔,将这n-1个盘子从起始移动至过渡位置。
     //当拆解到1阶汉诺塔时,直接终止递归。
     System.out.println("from " + from + " " + dishes + "号 " + "to " + to);
     //第n个盘子的移动。
     moveDish(dishes-1, middle, from, to);
     //将拆解出的n-1个盘子,从过渡位置移动到终点位置。
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值