递归算法的总结与应用

一、步骤

  1. 把问题转化为规模缩小了的同类问题的子问题
  2. 有明确的不需要继续进行递归的条件(base case)
  3. 有当得到了子问题的结果之后的决策过程
  4. 不记录每一个子问题的解

二、实例

2.1 汉诺塔问题

打印n层汉诺塔从左边移动到最右边的全部过程

2.1.1 思路

不考虑左中右的情况。只考虑需要从源位置from移动到目标位置to,中间暂存的位置是help。那么问题就可以抽象为:

  • 1~i-1:from - > help
  • i :from - > to
  • 1~i-1:help - > to

2.1.2 代码解析

public class Hanoi {
​
    public static void hanoi(int n) {
        if (n <= 1) return;
        // n层汉诺塔,起始位置圆盘全放在left,目标是全部移动到right
        func(n,"left", "mid", "right");
    }
​
    public static void func(int i, String from, String help, String to) {
        // 当i等于1,说明达到递归终止条件,只剩一个最后圆盘,将它从from移动到to即可
        if (i == 1){
            System.out.println("Move 1 from " + from + " to " + to);
        } else {
            // 递归过程,和上述思路完全一致,中间i移动的过程打印
            func(i-1, from, help, to);
            System.out.println("Move " + i + " from " + from + " to " + to);
            func(i-1, help, to, from);
        }
    }
​
    public static void main(String[] args) {
        // 3层汉诺塔的打印示例
        int n = 3;
        hanoi(n);
    }
​
}

2.2 打印字符串的全部子序列

打印一个字符串的全部子序列,包括空字符串

2.2.1 思路

对于字符串中的每个字符,都有两个选择:要or不要,最后就能得到所有情况的子序列

2.2.2 代码解析

public class PrintAllSubsquences {
​
    public static void printAllSubsquence(String str) {
        char[] chs = str.toCharArray();
        process(chs, 0);
    }
​
    public static void process(char[] chs, int i) {
        if(i == chs.length){
            System.out.println(String.valueOf(chs));
            return;
        }
        // 要当前字符接着走的路
        process(chs, i+1);
        char tmp = chs[i];
        chs[i] = 0;
        // 不要当前字符接着走的路
        process(chs, i+1);
        chs[i] = tmp;
    }
    
    public static void main(String[] args) {
        String test = "abc";
        printAllSubsquence(test);
    }
}

2.3 全排列

打印一个字符串的全排列

2.3.1 思路

从字符串的起点字符开始,将其后续的任一字符与其交换位置则构成一次排列,然后将其恢复继续处理下一位置。(可以利用visited数组优化递归过程,相当于剪枝,避免重复访问造成重复的全排列)

2.3.2 代码解析

public class PrintAllPermutations {
​
    public static ArrayList<String> Permutation(String str) {
        // 结果数组
       ArrayList<String> res = new ArrayList<>();
       if (str == null || str.length() == 0) return res;
       char[] chs = str.toCharArray();
       process(chs, 0, res);
       return res;
​
    }
​
    public static void process(char[] chs, int i, ArrayList<String> res) {
        // 如果i等于chs的长度,说明完成了一次全排列,将其加入res中
        if(i == chs.length){
            res.add(String.valueOf(chs));
        }
        // visited存储当前位置是否被访问过,防止重复访问造成重复全排列
        boolean[] visited = new boolean[26];
        // i之前的位置是已经选择的位置,i之后的是可以进行选择的位置
        for (int j = i; j < chs.length; j++) {
            // 对于i之后的任意位置如果没有被访问过,则可以进行排列
            if(!visited[chs[j] - 'a']){
                visited[chs[j] - 'a'] = true;
                // 排列方式是交换这两个位置完成一次不同排列
                swap(chs, i, j);
                // 然后继续处理下一位置
                process(chs, i+1, res);
                // 再将之前交换的位置恢复
                swap(chs, i, j);
            }
​
        }
    }
​
    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }
​
}

2.4 玩纸牌

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸 牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A 和玩家B都绝顶聪明。请返回最后获胜者的分数。

【举例】

arr=[1,2,100,4]

开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来 玩家 B可以拿走2或4,然后继续轮到玩家A...

如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继 续轮到玩家A...

玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1, 让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜, 分数为101。所以返回101。

arr=[1,100,2]

开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜, 分数为100。所以返回100。

2.4.1 思路

主函数返回先手拿牌和后手拿牌中分数高的那个值

  • 作为先手拿牌,他有两种选择:
    • (选择左边+下一次作为后手拿牌)或者(选择右边+下一次作为后手拿牌)中的更大值
    • 当拿到最后一张牌,即L等于R的时候,应该先手拿牌,返回arr[L]作为终止递归的条件

  • 作为后手拿牌,也有两种选择:
    • 如果别人拿走L,那作为先手在L+1和R上进行选择。如果别人拿走R,则作为先手在L和R-1上进行选择,取这两者中的更小值(对方会决定对我最不利的)
    • 当拿到最后一张牌,即L等于R的时候,后手不拿牌,返回0作为终止递归的条件。

2.4.2 代码解析

public class CardsInLine {
​
    // 暴力递归
    public static int win1(int[] arr) {
        if(arr == null || arr.length == 0){
            return 0;
        }
        return Math.max(f(arr, 0, arr.length-1), s(arr, 0, arr.length-1));
    }
​
    public static int f(int[] arr, int i, int j) {
        if(i == j){
            return arr[i];
        }
        return Math.max(arr[i] + s(arr, i+1, j), arr[j] + s(arr, i, j-1));
    }
​
    public static int s(int[] arr, int i, int j) {
        if(i == j) {
            return 0;
        }
        return Math.min(f(arr, i+1, j), f(arr, i, j-1));
    }
​
    // 动态优化版本
    public static int win2(int[] arr) {
        if(arr == null || arr.length == 0) {
            return 0;
        }
        int[][] f = new int[arr.length][arr.length];
        int[][] s = new int[arr.length][arr.length];
        for (int j = 0; j < arr.length; j++) {
            f[j][j] = arr[j];
            for (int i = j-1; i >= 0; i--) {
                f[i][j] = Math.max(arr[i] + s[i+1][j], arr[j] + s[i][j-1]);
                s[i][j] = Math.min(f[i+1][j], f[i][j-1]);
            }
        }
        return Math.max(f(arr, 0, arr.length-1), s(arr, 0, arr.length-1));
    }
​
    public static void main(String[] args) {
        int[] arr = { 1, 2, 100, 4};
        System.out.println(win1(arr));
        System.out.println(win2(arr));
​
    }
}

2.5 逆序栈

给定一个栈,请逆序这个栈,不能申请额外的数据结构,只能使用递归函数。

2.5.1 思路

首先设计一个递归函数f,f函数可以实现将栈中最底部元素弹出,其余元素覆盖当前栈的功能。

然后设计逆序栈的函数,通过调用f函数进行递归,最后可以实现栈的逆序。

2.5.2 代码解析

public class ReverseStackUsingRecursive {
​
    public static void reverse(Stack<Integer> stack) {
        if(stack.isEmpty()) return;
        else{
            int i = getAndRemoveLastElement(stack);
            reverse(stack);
            stack.push(i);
        }
    }
​
    public static int getAndRemoveLastElement(Stack<Integer> stack) {
        int result = stack.pop();
        if(stack.isEmpty()){
            return result;
        } else{
            int last = getAndRemoveLastElement(stack);
            stack.push(result);
            return last;
        }
    }
​
    public static void main(String[] args) {
        Stack<Integer> test = new Stack<Integer>();
        test.push(1);
        test.push(2);
        test.push(3);
        test.push(4);
        test.push(5);
        reverse(test);
        while (!test.isEmpty()) {
            System.out.println(test.pop());
        }
​
    }
​
}

2.6 数字字符串转化为字母字符串

规定1和A对应、2和B对应、3和C对应...

那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。

给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

2.6.1 思路

假设0到i-1的位置上已经确定,从i位置开始做转化

  • 如果i位置上为‘0‘,返回0,因为0没办法转化为字母
  • 如果i位置上为’3‘ ~ ’9‘,因为字母只有26个,只能将其转化为对应字母,接着尝试下一个位置。
  • 如果i位置上为’1‘ ,有两种选择:
    • i自己作为单独部分,接着尝试i+1~最后
    • i和i+1作为单独部分,接着尝试i+2~最后

  • 如果i位置上为’2‘ ,有两种选择:
    • i自己作为单独部分,接着尝试i+1~最后
    • 如果i+1的值为’0‘ ~ ’6’,可以将i和i+1作为单独部分,接着尝试i+2~最后

2.6.2 代码解析

public class Code06_ConvertToLetterString {
​
   public static int number(String str) {
      if (str == null || str.length() == 0) {
         return 0;
      }
      return process(str.toCharArray(), 0);
   }
​
   public static int process(char[] chs, int i) {
      if (i == chs.length) return 1;
      if (chs[i] == '0') return 0;
      if (chs[i] == '1'){
         int res = process(chs, i+1);
         if (i + 1 < chs.length){
            res += process(chs, i+2);
         }
         return res;
      }
      if (chs[i] == '2'){
         int res = process(chs, i+1);
         if (i + 1 < chs.length && chs[i+1] >= '0' && chs[i+1] <= '6'){
            res += process(chs, i+2);
         }
         return res;
      } else{
         return process(chs, i+1);
      }
   }
​
   public static void main(String[] args) {
      System.out.println(number("11111"));
   }
​
}

2.7 装物品

给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物 品不能超过这个重量。返回你能装下最多的价值是多少?

2.7.1 思路

从weights数组中依次拿出一个物品,看bag剩余空间是否能放这个物品,如果能放则有放与不放两种选择。选择结束之后选过的位置不再变化,继续对下一个物品进行选择。

2.7.2 代码解析

public class Code07_Knapsack {
​
   public static int maxValue1(int[] weights, int[] values, int bag) {
      return process1(weights, values, 0, bag);
   }
​
   /**
    * 暴力递归
    * @param weights 重量数组
    * @param values 价值数组
    * @param i:i之前的是已经确定的,在i之后进行选择
    * @param bag:剩余空间
    * @return
    */
   public static int process1(int[] weights, int[] values, int i, int bag) {
      // 如果i来到了最后,则没有选择的了,返回0
      if (i == weights.length){
         return 0;
      }
      // 如果剩余空间大于等于放下的物品,有放与不放两种选择,返回其中更大的那个
      if (bag >= weights[i]){
         return Math.max(process1(weights, values, i+1, bag),
               values[i] + process1(weights, values, i+1, bag-weights[i]));
      }
      return 0;
   }
​
   // 动态规划版本
   public static int maxValue2(int[] c, int[] p, int bag) {
      int[][] dp = new int[c.length+1][bag+1];
      for(int i = c.length-1; i >= 0 ; i--){
         for(int j = bag; j >= 0; j--){
            dp[i][j] = dp[i+1][j];
            if(j + c[i] <= bag){
               dp[i][j] = Math.max(dp[i][j], p[i] + dp[i+1][j+c[i]]);
            }
         }
      }
      return dp[0][0];
   }
​
   public static void main(String[] args) {
      int[] weights = { 3, 2, 4, 7 };
      int[] values = { 5, 6, 3, 19 };
      int bag = 11;
      System.out.println(maxValue1(weights, values, bag));
      System.out.println(maxValue2(weights, values, bag));
   }
​
}

有帮助到你的点赞、收藏和关注一下吧

                                                                           需要更多教程,微信扫码即可

                                                                                 

                                                                                         👆👆👆

                                                        别忘了扫码领资料哦【高清Java学习路线图】

                                                                     和【全套学习视频及配套资料】
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值