逐步生成式递归

1. 走楼梯

问题描述:

有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶。
请实现一个方法,计算小孩有多少种上楼的方式。
为了防止溢出,请将结果Mod 1000000007

给定一个正整数int n,请返回一个数,代表上楼的方式数。
保证n小于等于100000。

思路解析:

递归方程:f(x) = f(x-1)+f(x-2)+f(x-3)

迭代方程:dp[i]=dp[i-1]+dp[i-2]+dp[i-3]

代码:

package 逐步生成式递归;

import java.util.Scanner;

/**
 * @author: DreamCode
 * @file: 走楼梯.java
 * @time: 2022年3月24日-上午9:01:26
 */
public class 走楼梯 {

	static long[] rec;
	static long MODE = 1000000007;

	public static void main(String[] args) {
		int n = 1000;
		rec = new long[n + 1];
		rec[1] = 1;
		rec[2] = 2;
		rec[3] = 4;
		long ans = recursion(n); // 递归
		long ans2 = iteration(n); // 迭代
		System.out.println(ans);
		System.out.println(ans2);

	}

	private static long iteration(int n) {
		// TODO 迭代形式
		long[] dp = new long[n + 1];
		dp[1] = 1;
		dp[2] = 2;
		dp[3] = 4;
		for (int i = 4; i <= n; i++) {
			dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
			dp[i] %= MODE;
		}
		return dp[n];
	}

	private static long recursion(int n) {
		// TODO 递归方程;f(x) = f(x-1)+f(x-2)+f(x-3)
		if (rec[n] != 0) {
			return rec[n];
		}
		long res;
		res = (recursion(n - 1) + recursion(n - 2) + recursion(n - 3)) % MODE;
		rec[n] = res;
		return res;
	}

}

2. 机器人走格子

问题描述:

有一个X*Y的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。
请设计一个算法,计算机器人有多少种走法。
给定两个正整数int x,int y,请返回机器人的走法数目。保证x+y小于等于12。

思路解析:

递归方程:f(x,y)=f(x-1,y)+f(x,y-1)

迭代方程:dp(i,j) = dp(i-1,j) + dp(i,j-1)

代码:

package 逐步生成式递归;

/**
 * @author: DreamCode
 * @file: 机器人走格子.java
 * @time: 2022年3月24日-上午9:32:57
 */
public class 机器人走格子 {

	static long[][] rec;

	public static void main(String[] args) {
		int x = 1000;
		int y = 1000;
		rec = new long[x + 1][y + 1];
		long ans1 = recursion(x, y); // 递归
		long ans2 = iteration(x, y); // 迭代
		System.out.println(ans1);
		System.out.println(ans2);

	}

	private static long iteration(int x, int y) {
		// TODO 迭代形式(动态规划)
		long[][] dp = new long[x + 1][y + 1];
		for (int i = 1; i <= x; i++) { // 所有列为1的都只有一种走法
			dp[i][1] = 1;
		}
		for (int i = 1; i <= y; i++) { // 所有行为1的都只有一种走法
			dp[1][i] = 1;
		}
		for (int i = 2; i <= x; i++) {
			for (int j = 2; j <= y; j++) {
				dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
			}
		}
		return dp[x][y];
	}

	private static long recursion(int x, int y) {
		// TODO 递归方式:f(x,y)=f(x-1,y)+f(x,y-1)
		if (rec[x][y] != 0) {
			return rec[x][y];
		}
		long res;
		if (x == 1) { // 只有一列
			res = 1;
			rec[x][y] = res;
			return res;
		}
		if (y == 1) { // 只有一行
			res = 1;
			rec[x][y] = res;
			return res;
		}
		res = recursion(x - 1, y) + recursion(x, y - 1);
		rec[x][y] = res;
		return res;
	}

}

3. 非空子集(递归|迭代)

问题描述:

请编写一个方法,返回某集合的所有非空子集。

给定一个int数组A和数组的大小int n,请返回A的所有非空子集。
保证A的元素个数小于等于20,且元素互异。

各子集内部从大到小排序,子集之间字典逆序排序

思路解析:

常规子集生成(递归增量构造法)算法;f(n)是在f(n-1)的基础上逐步生成的

代码:

package 逐步生成式递归;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * @author: DreamCode
 * @file: 子集生成.java
 * @time: 2022年3月25日-下午2:14:16
 */
public class 非空子集生成 {

	public static void main(String[] args) {
		int n = 3;
		int[] arr = new int[n];
		for (int i = 0; i < n; i++) {
			arr[i] = i + 1;
		}
		Set<Set<Integer>> ans_recursion = getSubSet_recursion(arr, n - 1); // 递归
		System.out.println(ans_recursion);
		Set<Set<Integer>> ans_iteration = getSubSet_iteration(arr); // 迭代
		System.out.println(ans_iteration);
	}

	private static Set<Set<Integer>> getSubSet_iteration(int[] arr) {
		// TODO 逐步迭代
		Set<Set<Integer>> old_rec = new HashSet<>();
		old_rec.add(new HashSet<>()); // 初始化有一个空集
		for (int i = 0; i < arr.length; i++) {
			Set<Set<Integer>> new_rec = new HashSet<>(); // 新的集合
			for (Set<Integer> set : old_rec) { // 遍历旧集合的每一个元素
				new_rec.add(set); // 不选择arr[i]
				HashSet<Integer> temp = (HashSet<Integer>) ((HashSet<Integer>) set).clone();
				temp.add(arr[i]); // 选择当前元素加入集合
				new_rec.add(temp); // 将新的集合加入到new_rec
			}
			old_rec = new_rec;
		}
		return old_rec;
	}

	private static Set<Set<Integer>> getSubSet_recursion(int[] arr, int n) {
		// TODO 递归增量递增法
		Set<Set<Integer>> new_rec = new HashSet<>();
		if (n == -1) { // 出口
			new_rec.add(new HashSet<Integer>());
			return new_rec;
		}
		Set<Set<Integer>> old_rec = getSubSet_recursion(arr, n - 1);
		for (Set<Integer> set : old_rec) { // 遍历每一个f(n-1)
			new_rec.add(set); // 不选当前的arr[n]
			HashSet<Integer> temp = (HashSet<Integer>) ((HashSet<Integer>) set).clone(); // set为引用类型,修改时需要备份
			temp.add(arr[n]);
			new_rec.add(temp); // 选择当前元素
		}
		return new_rec;
	}

}

4. 非空子集(二进制)

问题描述:

请编写一个方法,返回某集合的所有非空子集。

给定一个int数组A和数组的大小int n,请返回A的所有非空子集。
保证A的元素个数小于等于20,且元素互异。

各子集内部从大到小排序,子集之间字典逆序排序

思路解析:

二进制法构造非空子集;二进制的每一位数字代表当前位置选与不选

代码:

package 逐步生成式递归;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * @author: DreamCode
 * @file: 非空子集生成_二进制.java
 * @time: 2022年3月25日-下午2:48:32
 */
public class 非空子集生成_二进制 {

	public static void main(String[] args) {
		int[] arr = { 1, 3, 2 }; 
		ArrayList<ArrayList<Integer>> ans = getSubSet(arr);
		System.out.println(ans);
	}

	private static ArrayList<ArrayList<Integer>> getSubSet(int[] arr) {
		// TODO 二进制法构造非空子集;二进制的每一位数字代表当前位置选与不选
		Arrays.sort(arr); // 对arr进行升序排序
		int n = arr.length; // arr数组的长度(元素的个数)
		ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
		for (int num = (int) (Math.pow(2, n) - 1); num > 0; num--) { // 为方便字典序,选择从大到小的遍历,不含有空集则不能等于0
			ArrayList<Integer> list = new ArrayList<>(); // 当前数的选择集合
			for (int i = n - 1; i >= 0; i--) { // 向右移动n-1~0位,再与1做&,得到1代表当前位选则,否则代表不选
				if (((num >> i) & 1) == 1) { // 当前位置选择
					list.add(arr[i]);
				}
			}
			ans.add(list);
		}
		return ans;
	}

}

5. 全排列(迭代)

思路解析:

逐个迭代的方法,逐个元素字符选取,插入到字符串的两两字符之间;避免产生重复子集元素,故选取set存储

代码:

package 逐步生成式递归;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author: DreamCode
 * @file: 全排列.java
 * @time: 2022年3月25日-下午9:30:33
 */
public class 全排列_迭代法 {

	public static void main(String[] args) {
		String A = "ABC";
		int n = 3;
		Set<String> ans = permutation(A, n);
		// 将HashSet排序,将HashSet转换为TreeSet,descendingSet()实现降序排序
		System.out.println(new TreeSet<>(ans).descendingSet());
	}

	private static Set<String> permutation(String string, int n) {
		// TODO 逐步生成大法-迭代法
		Set<String> rec_old = new HashSet<>();
		rec_old.add("");
		for (int i = 0; i < n; i++) {// 遍历每一个字符
			Set<String> rec_new = new HashSet<>(); // 新的存储容器
			char c = string.charAt(i);
			for (String s : rec_old) { // 访问上一趟集合中的每个字符串
				rec_new.add(c + s);  // s之前插入字符
				for (int j = 1; j < s.length(); j++) {  // s的每一个空格插入字符
					String subString = s.substring(0, j) + c + s.substring(j);  // substring(i,j):取左不取右
					rec_new.add(subString);
				}	
				rec_new.add(s + c);  // s之后插入字符
			}
			rec_old = rec_new; // 更新旧的容器
		}
		return rec_old;
	}

}

6. 全排列(递归回溯法)

思路解析:

逐步递归回溯。逐个交换,递归回溯;这次选用ArrayList存储;

简单、但是不能保证字典序。

如果原数组含有大量重复元素,会造成重复排列

代码:

package 逐步生成式递归;

import java.util.ArrayList;
import java.util.TreeSet;

/**
 * @author: DreamCode
 * @file: 全排类_交换法_递归回溯.java
 * @time: 2022年3月25日-下午10:15:33
 */
public class 全排类_交换法_递归回溯 {

	static ArrayList<String> container = new ArrayList<>();

	public static void main(String[] args) {
		String string = "ABC";
		int n = 3;
		permutation(string.toCharArray(), 0);
		System.out.println(new TreeSet<>(container).descendingSet());
	}

	private static void permutation(char[] string, int k) {
		// TODO 将第i位置字符逐个交换到第k位,递归回溯
		if (k == string.length) {// 当前已经构成一个完整的字符串
			container.add(new String(string));
			return;
		}
		for (int i = k; i < string.length; i++) {
			swap(string, i, k); // 将第i位与第k位进行交换
			permutation(string, k + 1); // 递归进入下一层
			swap(string, i, k); // 回溯
		}
	}

	private static void swap(char[] string, int i, int k) {
		// TODO 交换string字符串的i位与k位
		char t = string[i];
		string[i] = string[k];
		string[k] = t;
	}

}

7. 全排列(前缀法)

思路解析:

全排列_前缀法_递归回溯;每次根据字符数组从头扫描,只要该字符可用,就将其加到前缀的后面。

字符可用的前提:该字符在前缀中出现的次数小于在数组中出现的次数;

递归出口:前缀长度=字符数组长度

复杂、但是能保证字典序,对于求第K个是最佳选择;

代码:

package 逐步生成式递归;

import java.util.ArrayList;
import java.util.List;

/**
 * @author: DreamCode
 * @file: 全排列_前缀法_递归回溯.java
 * @time: 2022年3月25日-下午10:33:05
 */
public class 全排列_前缀法_递归回溯 {

	static List<String> container = new ArrayList<>();
	static long k = 0; // 记录当前是第几个排列

	public static void main(String[] args) {
		String string = "ABCDEFGHIJKLMNOPQRS";
		permutation("", string.toCharArray());
		System.out.println(container);
	}

	private static void permutation(String prefix, char[] charArray) {
		// TODO 前缀法
		if (charArray.length == prefix.length()) {
			container.add(prefix);
			k++;
			if(k==100) {
				System.out.println(prefix);
				System.exit(0);
			}
			return;
		}
		//每次都从头扫描,只要该字符可用,我们就附加到前缀后面,前缀变长了
		for (int i = 0; i < charArray.length; i++) {
			char c = charArray[i];
			if(count(prefix,c)<count(new String(charArray),c)) {
				permutation(prefix+c, charArray);
			}
		}
	}

	private static int count(String string, char c) {
		// TODO 计算在string字符串中字符c出现的次数
		int res=0;
		for(int i=0;i<string.length();i++) {
			if(string.charAt(i)==c) {
				res++;
			}
		}
		return res;
	}

}

8. 全排列(抽取法)

思路解析:

整体和递归回溯法相似,但是在选择元素的时候有所不同。当前元素与前一个元素相同,必须保证前一个元素已经选择才能选择当前元素,不然会有重复

代码:

 package7;

/**
 * @author: DreamCode
 * @file: G_剪邮票.java
 * @time: 2022年3月13日-下午5:42:19
 */
public class G_剪邮票 {

	static int[] record = { 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 }; // 全排列的数组,为1的位置为选择的卡片
	static boolean[] vis = new boolean[12]; // 记录当前的卡片是否选中的标记数组
	static int ans = 0; // 种类数答案

	public static void main(String[] args) {
		permutation(0, new int[12]); // 全排列
		System.out.println(ans);
	}

	private static void permutation(int k, int[] arr) {
		// TODO 全排列算法:抽取式
		if (k == 12) { // 排完一轮
			ans++;
			return;
		}
		for (int i = 0; i < 12; i++) {
             // 当前元素与前一个元素相同,必须保证前一个元素已经选择才能选择当前元素,不然会有重复
			if (i > 0 && record[i - 1] == record[i] && vis[i - 1] == false) { 
				continue;
			}
			if (vis[i] == false) {  // 当前元素未抽取
				vis[i] = true;
				arr[k] = record[i];
				permutation(k + 1, arr);
				vis[i] = false;  // 回溯
			}
		}
	}
    
}

9. 合法括号

思路解析:

输出合法的括号组合,输入括号对数,输出所有合法组合

输入:3

输出:()()(),((())),(()()),()(()),(())(),

判断一个字符串是否合法

思路解析:

f(n)=对f(n-1)中的每一个元素左边加括号,右边加括号,外层加括号

代码:

package 逐步生成式递归;

import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

/**
 * @author: DreamCode
 * @file: 合法括号.java
 * @time: 2022年3月25日-上午9:43:12
 */
public class 合法括号 {

	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		int n=scanner.nextInt();
		Set<String> ans= recursion(n);
		for(String string:ans) {
			System.out.print(string+"、");
		}
	}

	private static Set<String> recursion(int n) {
		Set<String> ans = new HashSet<>();
		if(n==1) {
			ans.add("()");
			return ans;
		}
		Set<String> ans_n_1 = recursion(n-1);
		for(String string:ans_n_1) {
			ans.add("()"+string);  //加左括号
			ans.add(string+"()");  //加右括号
			ans.add("("+string+")");  //外层加括号
		}
		return ans;
	}

}

10. 硬币表示

问题描述:

假设我们有8种不同面值的硬币{1,2,5,10,20,50,100,200},用这些硬币组合构成一个给定的数值n。

例如n=200,那么一种可能的组合方式为 200 = 3 * 1 + 1*2 + 1*5 + 2*20 + 1 * 50 + 1 * 100.
问总共有多少种可能的组合方式? (这道题目来自著名编程网站ProjectEuler) 类似的题目还有:

[华为面试题] 1分2分5分的硬币三种,组合成1角,共有多少种组合 1x + 2y + 5*z=10 [创新工厂笔试题]
有1分,2分,5分,10分四种硬币,每种硬币数量无限,给定n分钱,有多少组合可以组成n分钱
1 5 10 25 分 n,多少种组合方法.

思路解析:

递归方式:f(n,a)=f(n-k*coins[a],a-1)+k

迭代方式:动态规划

代码:

package 逐步生成式递归;

/**
 * @author: DreamCode
 * @file: 硬币表示.java
 * @time: 2022年3月24日-上午9:51:15
 * @思路:
 */
public class 硬币表示 {

	/**
	 * 假设我们有8种不同面值的硬币{1,2,5,10,20,50,100,200},用这些硬币组合构成一个给定的数值n。
	 * 例如n=200,那么一种可能的组合方式为 200 = 3 * 1 + 1*2 + 1*5 + 2*20 + 1 * 50 + 1 * 100.
	 * 问总共有多少种可能的组合方式? (这道题目来自著名编程网站ProjectEuler) 类似的题目还有:
	 * 
	 * [华为面试题] 1分2分5分的硬币三种,组合成1角,共有多少种组合 1*x + 2*y + 5*z=10 [创新工厂笔试题]
	 * 有1分,2分,5分,10分四种硬币,每种硬币数量无限,给定n分钱,有多少组合可以组成n分钱
	 * 
	 * 1 5 10 25 分 n,多少种组合方法.
	 */
	static long[][] rec;
	static int[] coins = { 1, 2, 5, 10, 20, 50, 100, 200 };

	public static void main(String[] args) {
		int n = 1000;
		rec = new long[n + 1][coins.length];
		long ans1 = recursion(n, coins.length - 1); // 递归
		long ans2 = iteration(n); // 递推(动态规划)二维
		long ans3 = iteration2(n); // 递推(动态规划)一维
		System.out.println(ans1);
		System.out.println(ans2);
		System.out.println(ans3);

	}

	private static long iteration2(int n) {
		// TODO 迭代方式(动态规划)一维
		int[] dp = new int[n + 1]; // 记录每种面值可以兑换的方法数
		dp[0] = 1; // 面值为0只有一种兑换方法
		for (int i = 0; i < coins.length; i++) {
			for (int j = coins[i]; j <= n; j++) {
				dp[j] = dp[j] + dp[j - coins[i]];
			}
		}
		return dp[n];
	}

	private static long iteration(int n) {
		// TODO 迭代方式(动态规划)二维
		int[][] dp = new int[coins.length][n + 1];
		for (int i = 0; i <= n; i++) { // 只有“1”这种硬币的兑换方式都只有1种
			dp[0][i] = 1;
		}
		for (int i = 0; i < coins.length; i++) { // 不管有哪些硬币,兑换“0”都只有一种方法
			dp[i][0] = 1;
		}
		for (int i = 1; i < coins.length; i++) {
			for (int j = 1; j <= n; j++) { // 用前i种硬币兑换j的面值
				for (int k = 0; k <= j / coins[i]; k++) { // 当前兑换i种硬币的个数
					dp[i][j] += dp[i - 1][j - k * coins[i]];
				}
			}
		}
		return dp[coins.length - 1][n];
	}

	private static long recursion(int n, int available) {
		// TODO 递归方式:f(n,a)=f(n-k*coins[a],a-1)+k
		if (rec[n][available] != 0) {
			return rec[n][available];
		}
		if (available == 0 || n == 0) { // 当前只有“1”这一种硬币,必然只有一种兑换方法
			return 1;
		}
		long res = 0;
		for (int k = 0; k * coins[available] <= n; k++) {
			res += recursion(n - k * coins[available], available - 1);
		}
		rec[n][available] = res;
		return res;
	}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java Stream API 提供了一种基于函数式编程风格的数据处理方式,可以用来简化并行操作、数据转换等任务。虽然 Java Streams 并不是直接用于生成树结构的工具,但它们能够非常方便地配合其他 Java 类库(如 Apache Commons Lang 的 RecursiveTreeBuilder 或自定义算法)来构建或操作树形结构。 在讨论如何使用 Java Stream API 来递归生成树之前,我们需要了解基本的树形结构及其关键组件: - **节点**:树的基本单位,通常包含数据和指向其子节点的引用。 - **根节点**:树的最顶级节点,无父节点。 - **子节点**:每个非叶子节点都有一个或多个子节点。 ### 使用 Java Stream 实现树的构建 虽然 Java Stream 主要用于处理集合,并非直接用于递归生成树,但在某种程度上,通过将递归过程转换为迭代形式,我们可以在一定程度上利用 Stream 来构建树状结构。下面是一个简单的示例,展示如何使用 Stream 构建树形结构。假设我们有一个代表树节点的简单类 `TreeNode`: ```java class TreeNode { String value; List<TreeNode> children; public TreeNode(String value) { this.value = value; this.children = new ArrayList<>(); } // 添加子节点 public void addChild(TreeNode child) { children.add(child); } } ``` #### 示例:使用递归来创建树结构 下面的例子展示了如何使用递归Java Streams 的组合来创建树结构。在这个例子中,我们将从一个列表开始,其中包含了父节点与子节点的关系,然后逐步构建树结构。 ```java public class TreeBuilder { public static TreeNode buildTree(List<NodePair> nodePairs) { return nodePairs.stream() .reduce(new TreeNode(null), (parent, next) -> { if (next.getParent() == null || parent.getValue().equals(next.getParent())) { parent.addChild(new TreeNode(next.getValue())); return parent; } else { return null; // 这里只是举例,实际情况下需要更复杂的逻辑来调整路径 } }, (left, right) -> left); // 使用空值作为初始节点,最终返回构建完成的根节点 // NodePair 类表示一个节点及其父节点名称 class NodePair { String value; String parent; public NodePair(String value, String parent) { this.value = value; this.parent = parent; } public String getParent() { return parent; } public String getValue() { return value; } } } } // 测试数据 List<NodePair> nodePairs = Arrays.asList( new NodePair("nodeB", "nodeA"), new NodePair("nodeD", "nodeC"), new NodePair("nodeE", "nodeC") ); TreeNode root = TreeBuilder.buildTree(nodePairs); root.getChildren().forEach(System.out::println); // 打印树的所有节点以验证结果 ``` 请注意,上述代码仅展示了如何将数据映射到树结构的基础概念。实际上,在构建树的过程中可能涉及到更复杂的问题,比如循环依赖检查、优化路径查找或添加额外的属性到节点上等等。在真正的应用中,你可能需要设计更多的辅助类和逻辑来处理各种情况。此外,这个示例并没有使用实际的 Stream 减少机制,而是简单地使用了 reduce 操作来进行构建。对于大型树结构的构建,使用传统的 for 循环或递归可能会更高效。 ### 相关问题: 1. **如何在构建树过程中处理循环依赖问题?** - 当遇到循环依赖时,应采取策略来避免无限递归,例如,可以限制递归深度或使用图论中的拓扑排序来检测和处理循环。 2. **Stream 是否适用于大规模树结构的构建?** - 尽管 Stream API 提供了一个优雅的方式来处理集合和序列化操作,但对于构建大型或极其复杂的树结构,由于内存管理和并行计算的限制,可能需要考虑使用传统循环或其他非函数式编程技术。 3. **在实际项目中应该选择哪种方式构建树结构?** - 这取决于具体的业务需求和系统的性能约束。如果系统允许并发处理并且对代码的简洁性和易读性有较高要求,那么使用 Stream 可能是更好的选择;而对于性能敏感的应用场景,则可能需要权衡代码复杂度和执行效率,考虑使用迭代或其他数据处理机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦码城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值