暴力递归详解

目录

暴力递归介绍

暴力递归的应用

     汉诺塔问题

     打印字符串子序列问题

     打印一个字符串的全部排列 

     拿牌问题

     栈的逆序 

     数字转化问题

     装货物问题

     N皇后问题 


暴力递归介绍

       暴力递归就是尝试,按照下面的步骤:1,把问题转化为规模缩小了的同类问题的子问题 ;2,有明确的不需要继续进行递归的条件;3,有当得到了子问题的结果之后的决策过程; 4,不记录每一个子问题的解。

       尝试的原则是找可变参数形式最简单,可变数量最少的方式。

暴力递归的应用

     汉诺塔问题

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

       这个问题的解决首先是假设三个杆分别是from,to,other,我们的目标是将所有的圆盘从from移动到to,我们按照这样的步骤进行,先将1~n-1个圆盘从from移动到other,接着将第n个圆盘从from移动到to,最后再将1~n-1个圆盘从other移动到to,即可完成这个完整的过程。

public class Code_Hanoi{
      public static void hanoi(int n){
            if(n>0){
               func(n,"左”,"右","中");
            }
      }
      //将1~n-1个圆盘从from移动到other,将n从from移动到to,最后将1~n-1个圆盘从other移动到to
      public static void func(int i,String start,String end,String other){
            if( i==1 ){//终止条件
                System.out.println("Move 1 from "+ start + "to" +end);
            }
            else{
                func(i-1,start,other,end);
                System.out.println("Move" + i + "from" +start+ "to" + end);
                func(i-1,other,to,start);
            }
      }
      public static void main(String[] args){
                int n = 3;
                hanoi(n);
      }
}

     打印字符串子序列问题

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

       先来到第一个字符的位置有要和不要两个选择,接着继续下去,直到最后一个字符就是一棵完全二叉树。

public class Code_PrintAllSubsquences{
       public static void printAllSubsquence(String str){
           char[] chs = str.toCharArray();
           process(chs,0);
       }
       //当前来到i位置,要和不要,走两条路
       //之前的选择所形成的结果是str
       //这个方式更省空间
       public static void process(char[] str,int i){
           if(i==str.length){//如果到达终止位置,直接打印
                System.out.println(String.valueOf(str));
                return;
           }
           process(str ,i+1);//要当前字符的路
           char tmp = str[i];//将i位置的路记录
           str[i]=0;//改为0
           process(str,i+1);//不要当前字符的路
           str[i]=tmp;//继续改回去
       }
       public static void function(String str){
           char[] chs = str.toCharArray();
           process(chs,0,new ArrayList());
       }
       //当前来到i位置,要和不要,走两条路
       //res记录着之前的选择所形成的列表
       // public static void process(char[] str,int i,List res){
       //     if(i==str.length){//如果到达终止位置,直接打印
       //           printList(res);
       //           return;
       //     }
       //     List resKeep = copyList(res);//将之前形成的记录打印一份
       //     resKeep.add(str[i]);//将新的字符加进去
       //     process(str,i+1,resKeep);//表示要当前的路
       //     List resNoInclude = copyList(res);//将之前形成的记录打印一份
       //     process(str,i+1,resNoInclude);//表示不要当前的路
       //}
       

     打印一个字符串的全部排列 

       要求不要出现重复的排列。

       一个字符串的全部排列的输出,第一个位置有n种选择,第二个位置有n-1种选择,依次按照这种方式选择下去,解决这个问题,将一个字符串的前面一部分当作已经做好的选择,然后在i位置上将后面的每一个节点分别放在i位置上然后排列它的所有的后面的选择,选择完以后换回去,接着重复这个操作,直到全部字符完成全排列。

       对于这个问题采用的是分支限界的方式,在排列选择的过程中,已经避免了重复的继续执行程序,相对于全部排列完,再删除重复的排列更快,而时间复杂度的量级并没有降低,在实际应用中,常数项可能有优化。

public class Code_PrintAllPermutations {

	public static ArrayList Permutation(String str) {
		ArrayList res = new ArrayList<>();
		if (str == null || str.length() == 0) {
			return res;
		}
		char[] chs = str.toCharArray();
		process(chs, 0, res);
		return res;
	}
    //str[i..]范围上,所有的字符,都可以在i位置上,后续都去尝试
    //str[0..i-1]范围上,是之前做的选择
    //把所有字符串形成的全排列,加入到res里去
	public static void process(char[] chs, int i, ArrayList res) {
		if (i == chs.length) {//如果全部排列好,直接添加到res中
			res.add(String.valueOf(chs));
		}
		boolean[] visit = new boolean[26];
		for (int j = i; j < chs.length; j++) {//每一个放到i位置进行选择
			if (!visit[chs[j] - 'a']) {//如果这个字符没有出现过,执行循环,避免出现重复的排列
				visit[chs[j] - 'a'] = true;
				swap(chs, i, j);//将第j位置的字符换到i位置
				process(chs, i + 1, res);//将i位置后面的字符分支全部情况选择好
				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;
	}

}

     拿牌问题

       给定一个整型数组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。

        对于这个问题分为先手和后手去分析,根据题意写出递归条件和终止条件。


	public static int win(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 void reverse(Stack stack) {//逆序栈
		if (stack.isEmpty()) {//如果栈为空,直接返回
			return;
		}
		int i = getAndRemoveLastElement(stack);//返回栈底元素,其它压下去
		reverse(stack);
		stack.push(i);//压入栈
	}

	public static int getAndRemoveLastElement(Stack stack) {//返回栈底元素,其它压下去
		int result = stack.pop();//弹出栈顶元素
		if (stack.isEmpty()) {//如果栈为空,直接返回
			return result;
		} else {
			int last = getAndRemoveLastElement(stack);//继续调用自身
			stack.push(result);//将result压入栈
			return last;//返回栈底元素
		}
	}

     数字转化问题

       规定1和A对应、2和B对应、3和C对应... 那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。 给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

   //从左往右递归尝试
    public static int number(String str) {
		if (str == null || str.length() == 0) {
			return 0;
		}
		return process(str.toCharArray(), 0);
	}
    //i之前的位置,如何转化已经做过决定了
    //i之后有多少种转化的结果
	public static int process(char[] chs, int i) {
		if (i == chs.length) {//如果字符已经到了最后位置,返回之前位置做的决定
			return 1;
		}
		if (chs[i] == '0') {//如果当前字符的0字符,之前做的决定存在问题,整体只有0种有效的决定
			return 0;
		}
		if (chs[i] == '1') {//如果当前字符为1
			int res = process(chs, i + 1);//i自己作为单独的部分,后续有多少种方法
			if (i + 1 < chs.length) {//如果当前字符下面还有字符
				res += process(chs, i + 2);//i和i+1作为单独的部分,后续还有多少种方法
			}
			return res;
		}
		if (chs[i] == '2') {//如果当前字符为2
			int res = process(chs, i + 1);//i自己作为单独的部分,后续有多少种方法
			if (i + 1 < chs.length && (chs[i + 1] >= '0' && chs[i + 1] <= '6')) {
				res += process(chs, i + 2);//如果当前字符后面还有字符,并且两者结合不会超过26,i和i+1作为单独的部分,后续还有多少种方法
			}
			return res;
		}
		return process(chs, i + 1);//当前字符是3~9范围上
	}

     装货物问题

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

    ///这个问题依然是从左往右递归尝试
    public static int maxValue(int[] weights, int[] values, int bag) {
		return process1(weights, values, 0, 0, bag);
	}
    //i后面的货物自由选择,形成的最大价值返回
    //alreadyweight之前做的决定,所达到的重量
	public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {
		if (alreadyweight > bag) {//如果超重了,不可以
			return 0;
		}
		if (i == weights.length) {//如果没货了,最大价值为0
			return 0;
		}
		return Math.max(

				process1(weights, values, i + 1, alreadyweight, bag),//如果不要i号货物

				values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));//要i号货物
	}

     N皇后问题 

       N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数n,返回n皇后的摆法有多少种。 n=1,返回1。 n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。n=8,返回92。

   public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		int[] record = new int[n];//record[i]第i行的皇后放在了第几列
		return process1(0, record, n);
	}

	public static int process1(int i, int[] record, int n) {
		if (i == n) {//如果已经来到第n行,之前的是正确的,返回一种情况
			return 1;
		}
		int res = 0;
		for (int j = 0; j < n; j++) {//来到第i行,从第一列到第n-1列开始尝试
			if (isValid(record, i, j)) {//如果和之前的i-1行的皇后既不共行共列,也不共斜线,那么认为当前位置有效,记录该位置
				record[i] = j;
				res += process1(i + 1, record, n);//继续往下尝试
			}
		}
		return res;
	}
    //record[0..i-1]位置需要看,record[i..]的位置不需要看
    //返回i行皇后,放在了j列,是否有效
	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) {//之前的某个k行的皇后
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {//如果共列,或者共斜线,返回false
				return false;
			}
		}
		return true;
	}
    ///加速算法,对上面的进行优化,思路一样
    //请不要超过32皇后问题
	public static int num2(int n) {
		if (n < 1 || n > 32) {
			return 0;
		}
		int upperLim = n == 32 ? -1 : (1 << n) - 1;//对于n皇后问腿,那么就是后面n个全是1,前面全是0
		return process2(upperLim, 0, 0, 0);
	}
    //利用位运算加速
	public static int process2(int upperLim, int colLim, int leftDiaLim,
			int rightDiaLim) {//通过改变限制,进行皇后的选择,这几个变量存在,之前的选择造成的右限制,左斜线和右斜线限制
		if (colLim == upperLim) {//全部放完,返回一种情况
			return 1;
		}
		int pos = 0;
		int mostRightOne = 0;
		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));//哪些位置可以放皇后
		int res = 0;
		while (pos != 0) {//将pos中的1依次都提取出来
			mostRightOne = pos & (~pos + 1);//提取出能选皇后位置的最右侧的1
			pos = pos - mostRightOne;
			res += process2(upperLim, colLim | mostRightOne,
					(leftDiaLim | mostRightOne) << 1,//左斜线限制的更新
					(rightDiaLim | mostRightOne) >>> 1);//右斜线限制的更新
		}
		return res;
	}

你可能感兴趣的:(数据结构与算法,数据结构,算法,java)