递归算法及其应用

在上一篇讲二分查找的文章中我们提到了实现二分查找的一种方式,那就是使用递归算法。

    //递归
    public static int recursionBinarySearch(int[] arr,int key,int low,int high){
        
        if(key < arr[low] || key > arr[high] || low > high){
            return -1;              
        }
        
        int middle = (low + high) / 2;          //初始中间位置
        if(arr[middle] > key){
            //比关键字大则关键字在左区域
            return recursionBinarySearch(arr, key,low,middle-1);
        }else if(arr[middle] < key){
            //比关键字小则关键字在右区域
            return recursionBinarySearch(arr, key,middle+1,high);
        }else {
            return middle;
        }
    }

根据二分查找的原理,当我们取得中间值arr[middle]大于想要查找的目标值key的时候,我们需要排除比key大的那一半数据,也就是arr[middle]之后的数据。在代码中我们可以看到,我们通过将high参数设置为middle-1来排除middle之后的数据。同时我们将middle-1当做新的high传入到函数中继续调用。也就是这个函数自己又继续调用了自己本身,只是改变了参数,将问题分解为原来的一半。然后在此函数中继续上面的步骤,改变参数low或者high,将low或者high的新值作为参数继续传给自己并调用自己,这就是递归。

递归算法设计的基本思想是:对于一个复杂的问题,把原问题分解为若干个相对简单类同的子问题,继续下去直到子问题简单到能够直接求解,也就是说到了递归的出口,这样原问题就有递归得解。
例如上面二分查找(一个复杂的问题),我们不断将查找范围减半(也就是把原问题分解为相对简单的子问题),直到查找范围就剩一个数(子问题简单到能够直接求解),我们就找到了递归的出口(也就是上述函数中的return middle)。

举两个个生活中的例子来说明什么是递归

  1. 递归就是洋葱,一个洋葱是带着一层洋葱皮的洋葱,拨开一层洋葱皮,里面还是洋葱,再剥开一层洋葱皮,里面···还是洋葱。(如果你愿意一层一层一层的剥开TA的心。。。)
  2. 递归就是包子馅的包子,也就是你剥开包子皮,发现里面是一个新的包子。对这个包子取一个极限,你就会发现,这个包子就是一个馒头。。。

上面说到,使用递归算法求解,需要找到递归的出口,很容易理解,若是没有出口,那么函数总是自己调用自己,永远也不会停下来,直到报出栈溢出(StackOverflowError)的异常。例如如下代码

public class Test {

    public static void main(String[] args) {
        test();        
    }

    public static void test(){
        test();     //自己调用自己   
    }
}

输出结果为:

Exception in thread "main" java.lang.StackOverflowError
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)
        at Test.test(Test.java:11)

拿上面包子馅的包子来举例,我们一层层的剥开包子皮,目的就是找到最里面的包子,但如果我们在递归算法中不设置出口条件,那么就会出现不管剥开多少次包子皮,都找不到最里面的包子的情况。然后你就会发现,这包子其实就是一个馒头。。

所以在我们使用递归算法的时候,必须有一个明确的递归结束条件,称为递归出口,有的书上也称为基线条件
每个递归函数都需要包含两部分,基线条件和递归条件。递归条件指的是函数调用自己,而基线条件指的是函数不再调用自己,从而避免无线循环。
例如上述二分查找中的基线条件与递归条件,

        if(arr[middle] > key){           //递归条件
            return recursionBinarySearch(arr, key,low,middle-1);
        }else if(arr[middle] < key){    //递归条件
            return recursionBinarySearch(arr, key,middle+1,high);
        }else {                         //基线条件(递归出口)
            return middle;
        }
 

递归的应用有哪些呢?最经典的应用就是求斐波那契数列了。

public int f(int n){  
    if(n == 1 || n == 2){     // 基线条件(递归出口)
        return 1;  
    }  
    return f(n-1) + f(n-2);    //递归条件
}

当然斐波那契数列也可以用循环的方式来解决。

public int f(int n) {  
    int f0 = 1;  
    int f1 = 1;  
    int f2 = 0;  
  
    for(int i = 2; i < n; i++){  
        f2 = f0 + f1;  
        f0 = f1;  
        f1 = f2;  
    }  
    return f2;  
}

这两种方法的作用相同,结过相同,但在程序运行的性能上有很大差距。
使用递归的方式来求解斐波那契数列的时间复杂度为O(2n),而使用循环的方式的时间复杂度为O(n)。可见递归只是让解决方案变得更清晰,并没有性能上的优势。实际上,在一些情况下,使用循环的性能更好。

当然,递归的应用并不只是斐波那契数列求和,二叉树的遍历,排序算法中的排序,都可以用递归来实现。

递归是一种非常优雅的问题解决方法(在不考虑程序性能的前提下),递归将人分为截然不同的阵营:恨它的、爱它的以及恨了几年后又爱上它的。

最后分享一句话,Leigh Caldwell在Stack Overflow上说:“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值