目录
前文:
博主是一名算法小白,深感算法之困难,但是算法有没有模板呢?毫无疑问,是有的,通过学习一些算法模板,并记牢这些模板,或许在考场中,你会见到一些题目蕴含这些模板的影子,通过这些模板,可以帮助你找到一些解题思路,本文仅以博主自己的实际经验,撰写文章,如有不当之处,敬请批评指正。
模板一,子集:
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/subsets
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
这道题能帮助我们什么呢?其实或许我们做的很多递归类题目都包含这道题的影子。
题解:
对于每个数而言,只有两个选择,选或者不选,因此,对于这个示例来说,我们可以画一个二叉树来表示选或者不选两种状态。
啊,忽略我难看的字体。。。。画出的二叉树如图所示,最后我们merge所有结果,就能拿到ans,所以我们得到了逻辑如下:
res = [] // 定义存放结果的list
if 选:
res.add(number) // 将当前选的数加进去
if 不选:
nothing //什么也不做,继续下一次调用
所以,我们怎么用递归解决呢?
代码:
class Solution {
// 主程序代码
public List<List<Integer>> subsets(int[] nums) {
n = nums.length; // 求出传递的数组长度,赋值给n
recur(nums, 0); // 调用recur函数
return ans; // 返回ans 结果
}
int n; // 传递数组长度
List<List<Integer>> ans = new ArrayList<>(); // 新建一个用于存放结果的list
List<Integer> a = new ArrayList<>(); // 存放中间结果的list
private void recur(int[] nums, int i){
// 递归边界
if (i == n) {
// 因为不能直接将a加入到最终结果,需要一个中间列表存储a的所有内容
ArrayList<Integer> tmp = new ArrayList<>();
for (int j = 0; j < a.size(); j++) {
tmp.add(null);
}
Collections.copy(tmp, a);
// 将其中一个分支的结果加入到最终结果
ans.add(tmp);
return;
}
// 这里代表如果选的分支
recur(nums, i+1);
// 将当前的数值加入到中间列表中
a.add(nums[i]);
// 这里代表如果不选的分支
recur(nums, i+1);
// 这里注意还原现场,如果不还原现场得到的结果是错的
a.remove(a.size() - 1);
}
}
模板二,组合:
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/combinations
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
题解:
同样的,对于每个状态,我们都有选或者不选两种状态,但是这里我们要注意到边界条件的变化,我们这里保存的中间结果数组的大小是不能超过k的,也就是说,如果中间结果数组大小是k我们就应该结束此次递归
代码:
class Solution {
public List<List<Integer>> combine(int n, int k) {
this.n = n;
this.k = k;
// 开始调用recur函数
recur(1);
return ans;
}
private void recur(int i) {
// 进行剪枝,如果我们保存中间结果的数组大小已经大于k了,后面的就不用看了,直接结束递归
// 或者是如果当前保存中间结果的数组大小再加上后面还未访问到的结果的数量还小于k的话,同样
// 结束递归
if (a.size() > k || a.size() + (n - i + 1) < k) {
return;
}
// 递归边界
if (i == n+1) {
// 这句写法就等同于 Collections.copy() 也就是将a列表浅拷贝到tmp列表
ArrayList<Integer> tmp = new ArrayList<>(a);
ans.add(tmp);
return;
}
// 如果选中当前数值的递归
recur(i+1);
a.add(i);
// 如果不选中当前数值的递归
recur(i+1);
// 注意还原现场
a.remove(a.size() - 1);
}
int n, k;
List<List<Integer>> ans = new ArrayList<>();
List<Integer> a = new ArrayList<>();
}
模板三,全排列:
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/permutations
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
题解:
这里思路略微与前两道题思路不同,全排列是需要将所有没选中过的数字,依次加入到中间数组,所以这里我们需要维护一个bool数组,用于记录当前数字是否选中。
代码:
class Solution {
public List<List<Integer>> permute(int[] nums) {
n = nums.length;
// 初始化bool数组,将所有设置成未访问,也就是false
for (int i = 0; i < nums.length; i++) {
used.add(false);
}
// 调用recur函数
recur(nums, 0);
return ans;
}
int n;
// 定义记录访问的数组
List<Boolean> used = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
List<Integer> a = new ArrayList<>();
private void recur(int[] nums, int i) {
// 递归边界
if (i == n) {
ArrayList<Integer> tmp = new ArrayList<>(a);
ans.add(tmp);
return;
}
// 对于nums的所有数字而言
for (int j = 0; j < nums.length; j++) {
// 如果未被访问,才新增
if (!used.get(j)) {
// 将当前数字新增到中间数组中
a.add(nums[j]);
// 记录当前数字已访问,下次就不能新增这个数字
used.set(j, true);
// 递归调用
recur(nums, i+1);
// 还原现场
a.remove(a.size() - 1);
// 再将当前数字设置成未访问
used.set(j, false);
}
}
}
}