跟着左神学算法——数据结构与算法学习日记(十)

40 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

最近开始接触与学习数据结构与算法,一方面为了解决课内数据结构课解题思路不清晰的问题,另一方面也是听说左神的大名,故开始跟着左神一起学习数据结构与算法。同时以写博客的形式作为输出,也算是为了对所学的知识能掌握的更深吧

今天学习的是——暴力递归

概述

暴力递归就是尝试

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

经典题目

汉诺塔问题

image.png 思路:假设在from上从上到下圆盘编号依次是1,2,3...i,可以将步骤大致分成三步。第一步:将1~i-1的圆盘放到按照摆放顺序放在other上;第二步:把from上的i放到to上;第三步:把other上的1~i-1放按照摆放顺序放到to上。 也可以这样想:如果现在我要把i个圆盘从from放到to上并且按照顺序的话,那么我就需要先把i-1个圆盘放到other上,再将第i个圆盘放到to上,再把other上的圆盘放回到to上即可。而这也是每一步递归的思路。只要保证每一次递归都是按照这个思路走,那么最后整体的结果也会符合这个思路

代码实现:

//返回汉诺塔问题结果的函数
public static void hanoi(int n){//n是汉诺塔问题的规模
	if(n>0){
		func(n,"左","右","中");
	}
}

//start:起始位置 end:目标位置 other:剩下的圆柱
public static void func(int i,String start,String end,String other){
	if(i == 1){//用于结束递归的条件——basecase
		System.out.println("Move 1 from " + start +" to " +end); 
	}
	//1.把i-1个圆盘放到other上
	func(i-1,start,other,end);
	//2.把第i个圆盘放到end上
	System.out.println("Move "+ i +" from " + start +" to " +end);
	//把i-1个圆盘从other放到to上
	func(i-1,other,end,start);
}

打印一个字符串的所有子序列,包括空字符串

例如:对于字符串"abc",它的所有子序列分别是:"a"、"b"、"c"、"ab"、"ac"、"bc"、"abc"、""八种

思路:从字符串的第一个字符开始,对于每个字符而言,可以选择要或者不要该字符,最终在结构上形成一个树形结构,从根节点开始一直到叶子节点的每一条路都代表着该字符串的一个子序列。

image.png

代码实现:

public static void function(String str){
	char[] chs = str.toCharArray();
	process(chs,0,new ArrayList<Character>());
}

public static void process(char[] chs,i,List<Character> res){
	//设置跳出递归的条件basecase
	if(i == chs.length){
		print(res);//对结果进行处理(操作或者打印等操作)
		return;
	}

	//要该字符
	List<Character> resKeep = copyList(res);
	resKeep.add(chs[i]);
	process(chs,i+1,resKeep);
	//不要该字符
	List<Character> resNoKeep = copyList(res);
	process(chs,i+1,resNoKeep);
}

优化版的代码实现(在空间复杂度上实现了优化):

public static void printAllsubsquence(String str){
	char[] chs = str.toCharArray();
	process(chs,0);
}
//递归函数
public static void process(char[] chs,i){
	//设置好basecase
	if(i == chs.length){
		System.out.println(String.valueof(str));
	}

	//要当前字符,不对str做修改,保持即可
	process(str,i+1);//对下一个字符进行选择
	//不要当前字符
	char tmp = str[i];
	str[i] = 0;//相当于把当前str[i]从字符串中删去
	process(str,i+1);
	str[i] = tmp;//恢复字符串
}

打印一个字符串的全排列,且不出现重复的排列

递归思路:比如字符串"abc",先计算a开头时的全排列,然后是b开头时的全排列,然后是c开头的全排列;对于a开头时的全排列,计算第二个是b时的全排列,第二个是c时的全排列...

代码实现:

public static ArrayList<String> Permutation(String str){
	//1.创建一个用于返回结果的ArrayList
	ArrayList res = new ArrayList<String>();
	if(str == null || str.length==0){
		return null;
	}
	//2.将字符串转换成字符数组
	Char[] chs = str.toCharArray();
	//3.进行递归函数的调用
	process(chs,0,res);
	//4.返回结果
	return res;
}
//递归函数process
public static void process(char[] str,int i,ArrayList<String> res){
	//设置basecase
	if(i == str.length){
		res.add(String.valueOf(str));
	}
	//创建一个用于判断是否重复的boolean数组
	boolean[] visit = new boolean[26];
	//进行迭代和递归
	for(int j = i;j<str.length;j++){
		if(visit[chs[j]-'a']){
			visit[chs[j]-'a'] = true;
			swap(str,i,j);
			process(str,i+1;res);
			swap(str,i,j);
		}
	}
}

拿纸牌问题

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

对于玩家而言,自己所需要做的就是确保在拿走左或者右的纸牌之后,留给对手的牌是最小的,才是对自己最有利的情况 因此, 思路:根据题意,A为先手,构建一个f函数,返回A在i到j这个区间上通过拿牌得到的最大分数,B为后手,B若是不想让A赢,就必须保证B拿完后牌后,留给A的牌是最小的。所以构建一个s函数,返回从i到j上A通过拿牌获得分数的最小值

代码实现:

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