数据结构:递归算法

记得小时候经常讲的一个故事:从前有座山,山上有座庙,庙里有一个老和尚和一个小和尚,一天,老和尚给小和尚讲了一个故事,故事内容是“从前有座山,山上有座庙,庙里有一个老和尚和一个小和尚,一天,老和尚给小和尚讲了一个故事,故事内容......”

  什么是递归,上面的小故事就是一个明显的递归。以编程的角度来看,程序调用自身的编程技巧称为递归( recursion)。

  百度百科中的解释是这样的:递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。

1、递归的定义

  递归,就是在运行的过程中调用自己。

  递归必须要有三个要素:

  ①、边界条件

  ②、递归前进段

  ③、递归返回段

  当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

2、求一个数的阶乘:n!

1

n! = n*(n-1)*(n-2)*......1

  规定:

  ①、0!=1

  ②、1!=1

  ③、负数没有阶乘

  上面的表达式我们先用for循环改写:

/**
 * 0!=1  1!=1
 * 负数没有阶乘,如果输入负数返回-1
 * @param n
 * @return
 */
public static int getFactorialFor(int n){
    int temp = 1;
    if(n >=0){
        for(int i = 1 ; i <= n ; i++){
            temp = temp*i;
        }
    }else{
        return -1;
    }
    return temp;
}

如果求阶乘的表达式是这样的呢?

n! = n*(n-1)!

我们用递归来改写:

/**
 * 0!=1  1!=1
 * 负数没有阶乘,如果输入负数返回-1
 * @param n
 * @return
 */
public static int getFactorial(int n){
    if(n >= 0){
        if(n==0){
            System.out.println(n+"!=1");
            return 1;
        }else{
            System.out.println(n);
            int temp = n*getFactorial(n-1);
            System.out.println(n+"!="+temp);
            return temp;
        }
    }
    return -1;
}

 

我们调用该方法getFactorial(4);即求4!打印如下:

  

  这段递归程序的边界条件就是n==0时,返回1,具体调用过程如下:

  

3:fibonacci数列

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

规律后一个数是前两个数的和 n>=3
源代码

/** 
     * fibonacci数列 
     * @param n 
     * @return 
     */  
    public static long fibonacci(int n) {  
        if((0 == n) || (1 == n)) {  
            return n;  
        }else {  
            return fibonacci(n-1) + fibonacci(n-2);  
        }  
    }  

4:1加到n累加

用递归实现从1加到n,即1+2+3+4+…+n。

规律是后一个数等于第n个数n+它的前一个数的和,当n > 1时候
源代码

/** 
     * 累加,从1加到n,即1+2+3+4+...+n 
     * @param n 要累加到的数值 
     * @return 累加的结果 
     */  
    public static long total(int n) {  
        if(1 == n) {  
            return n;  
        }else {  
            return total(n-1) + n;  
        }  
    }  

5:数组求和 比如 arr[] = [2,6,8];


private int sum(int[] arr,int l){
    if(l == arr.length)
        return 0;
    return arr[l] + sum(arr,l+1);
}

6:删除链表中等于给定值 val 的所有节点。

示例:

输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
1. 递归方式实现

public ListNode removeElements3(ListNode head, int val) {
    if(head == null)
        return null;
    //1->2->3->4
    head.next = removeElements3(head.next, val);
    return head.val == val ? head.next : head;
}

 

7、递归的二分查找

  注意:二分查找的数组一定是有序的!!!

  在有序数组array[]中,不断将数组的中间值(mid)和被查找的值比较,如果被查找的值等于array[mid],就返回下标mid; 否则,就将查找范围缩小一半。如果被查找的值小于array[mid], 就继续在左半边查找;如果被查找的值大于array[mid],  就继续在右半边查找。 直到查找到该值或者查找范围为空时, 查找结束。

 不用递归的二分查找如下:

/**
 * 找到目标值返回数组下标,找不到返回-1
 * @param array
 * @param key
 * @return
 */
public static int findTwoPoint(int[] array,int key){
    int start = 0;
    int last = array.length-1;
    while(start <= last){
        int mid = (last-start)/2+start;//防止直接相加造成int范围溢出
        if(key == array[mid]){//查找值等于当前值,返回数组下标
            return mid;
        }
        if(key > array[mid]){//查找值比当前值大
            start = mid+1;
        }
        if(key < array[mid]){//查找值比当前值小
            last = mid-1;
        }
    }
    return -1;
}

二分查找用递归来改写,相信也很简单。边界条件是找到当前值,或者查找范围为空。否则每一次查找都将范围缩小一半。

public static int search(int[] array,int key,int low,int high){
    int mid = (high-low)/2+low;
    if(key == array[mid]){//查找值等于当前值,返回数组下标
        return mid;
    }else if(low > high){//找不到查找值,返回-1
        return -1;
    }else{
        if(key < array[mid]){//查找值比当前值小
            return search(array,key,low,mid-1);
        }
        if(key > array[mid]){//查找值比当前值大
            return search(array,key,mid+1,high);
        }
    }
    return -1;
}

递归的二分查找和非递归的二分查找效率都为O(logN),递归的二分查找更加简洁,便于理解,但是速度会比非递归的慢。

8、汉诺塔问题

  汉诺塔问题是由很多放置在三个塔座上的盘子组成的一个古老的难题。如下图所示,所有盘子的直径是不同的,并且盘子中央都有一个洞使得它们刚好可以放在塔座上。所有的盘子刚开始都放置在A 塔座上。这个难题的目标是将所有的盘子都从塔座A移动到塔座C上,每次只可以移动一个盘子,并且任何一个盘子都不可以放置在比自己小的盘子之上。

试想一下,如果只有两个盘子,盘子从小到大我们以数字命名(也可以想象为直径),两个盘子上面就是盘子1,下面是盘子2,那么我们只需要将盘子1先移动到B塔座上,然后将盘子2移动到C塔座,最后将盘子1移动到C塔座上。即完成2个盘子从A到C的移动。

  如果有三个盘子,那么我们将盘子1放到C塔座,盘子2放到B塔座,在将C塔座的盘子1放到B塔座上,然后将A塔座的盘子3放到C塔座上,然后将B塔座的盘子1放到A塔座,将B塔座的盘子2放到C塔座,最后将A塔座的盘子1放到C塔座上。

  如果有四个,五个,N个盘子,那么我们应该怎么去做?这时候递归的思想就很好解决这样的问题了,当只有两个盘子的时候,我们只需要将B塔座作为中介,将盘子1先放到中介塔座B上,然后将盘子2放到目标塔座C上,最后将中介塔座B上的盘子放到目标塔座C上即可。

  所以无论有多少个盘子,我们都将其看做只有两个盘子。假设有 N 个盘子在塔座A上,我们将其看为两个盘子,其中(N-1)~1个盘子看成是一个盘子,最下面第N个盘子看成是一个盘子,那么解决办法为:

  ①、先将A塔座的第(N-1)~1个盘子看成是一个盘子,放到中介塔座B上,然后将第N个盘子放到目标塔座C上。

  ②、然后A塔座为空,看成是中介塔座,B塔座这时候有N-1个盘子,将(N-2)~1个盘子看成是一个盘子,放到中介塔座A上,然后将B塔座的第(N-1)号盘子放到目标塔座C上。

  ③、这时候A塔座上有(N-2)个盘子,B塔座为空,又将B塔座视为中介塔座,重复①,②步骤,直到所有盘子都放到目标塔座C上结束。

  简单来说,跟把大象放进冰箱的步骤一样,递归算法为:

  ①、从初始塔座A上移动包含n-1个盘子到中介塔座B上。

  ②、将初始塔座A上剩余的一个盘子(最大的一个盘子)放到目标塔座C上。

  ③、将中介塔座B上n-1个盘子移动到目标塔座C上。

public class HanNuoTa {

    public static void main(String[] args) {
        move(4,"A","B","C");
    }

    public static void move(int dish,String from,String temp,String to){
        //第一个盘子 从a-b
        if(dish == 1){
            System.out.println("将盘子"+dish+"从塔座"+from+"移动到目标塔座"+to);
        }else{
            //看成2个盘子 N-1~1个盘子 开始移动 将a柱子上的从上到下n-1个盘移到b柱子上
            move(dish-1,from,to,temp);
            
            //将a柱子上的N最后一个盘子移动到c
            System.out.println("将盘子"+dish+"从塔座"+from+"移动到目标塔座"+to);

            //将b柱子上的n-1个盘子移到c柱子上
            move(dish-1,temp,from,to);
        }
    }
}

测试:

move(4,"A","B","C");

将盘子1从塔座A移动到目标塔座B
将盘子2从塔座A移动到目标塔座C
将盘子1从塔座B移动到目标塔座C
将盘子3从塔座A移动到目标塔座B
将盘子1从塔座C移动到目标塔座A
将盘子2从塔座C移动到目标塔座B
将盘子1从塔座A移动到目标塔座B
将盘子4从塔座A移动到目标塔座C
将盘子1从塔座B移动到目标塔座C
将盘子2从塔座B移动到目标塔座A
将盘子1从塔座C移动到目标塔座A
将盘子3从塔座B移动到目标塔座C
将盘子1从塔座A移动到目标塔座B
将盘子2从塔座A移动到目标塔座C
将盘子1从塔座B移动到目标塔座C 

解释:

//移动盘子12在c柱
将盘子1从塔座AB
将盘子2从塔座AC
将盘子1从塔座BC

//移动盘子123在b柱
将盘子3从塔座AB
将盘子1从塔座CA
将盘子2从塔座CB
将盘子1从塔座AB

//移动盘子1234在c柱
将盘子4从塔座AC
将盘子1从塔座BC
将盘子2从塔座BA
将盘子1从塔座CA
将盘子3从塔座BC
将盘子1从塔座AB
将盘子2从塔座AC
将盘子1从塔座BC

 

汉诺塔:汉诺塔(Tower of Hanoi)源于印度传说中,大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

这里我们先把上方的63个盘子看成整体,这下就等于只有两个盘子,自然很容易了,我们只要完成两个盘子的转移就行了,好了现在我们先不管第64个盘子,假设a柱只有63个盘子,与之前一样的解决方式,前62个盘子先完成移动目标。

    嗯,就这样一步步向前找到可以直接移动的盘子,62,61,60,......,2,1,最终,最上方的盘子是可以直接移动到c柱的,那就好办了,我们的2号盘也能完成向c柱的转移,这时c柱上时已经转移成功的2个盘,于是3号盘也可以了,一直到第64号盘。

代码很简洁,可能对于递归不是很理解的同学觉得有些吃力,下面我来具体解释下递归的流程。

       当n=64时,前63个要想办法成功移动到b柱上,64号是Boss,他不管上面的63个小弟用什么办法,我可以先等着,前面63个小弟可以利用我的c柱,于是64号在等着上面63号完成移到b柱,现在63是临时老大,他也想去c柱,于是他命令前62号移到b柱,他等着,62号也采取之前两个的做法,于是这个命令一直往前传,没办法,上面被压着自己也没法动啊。

        终于到了1号,他是现在唯一能动的,于是1号移动到了b柱,好了,2号可以到c柱,2第一个到目的地,心里十分激动,我都到c柱,舒服。不过当他看到a柱上的3号时,猛然一惊,我还有个上司,好吧得完成任务啊,于是让1号移到c柱,3号可以到b柱了,之后1号和2号在想办法到b柱,于是1,2,3号在b柱,4号一看很满意,但我得到b柱啊,嗯,1,2,3号你们按照刚才的办法到c柱,空出b柱给我。唉,接着折腾,后面的5号一直到63号都是这么折腾的,终于前63号移动到b柱,64号直接跑到了c柱,他觉得这些小弟办事效率真不行,不过他还是招呼小弟到c柱。

     于是剩下在b柱的63个小弟还要再干一遍他们在a柱上干的事,这里来看,1号操作的频率是最高的,而64号只要移动一次就行了,要是在现实中让人这么去干,估计早就被逼疯了。

      如果真要解释代码的每一步执行过程,那会很乱,两层递归,最后自己也会被绕的晕头转向,这个时候只要理解递归最终的解决的问题是什么就行了,中间的事交给程序,递归可以很绕也可以很直接,我们按照最直接的理解就行了。

  最后关于递归算法,这中解决问题的方法也只有计算机才喜欢,我们虽然看着代码很简单,但真要深入理解也是很费脑细胞的,不过递归确实有中数学上的简洁美和逻辑美。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值