46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
排列问题的不同:
1.每层都是从0开始搜索而不是startIndex
2.需要used数组记录path里都放了哪些元素了
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
backtracing(nums,used);
return res;
}
public void backtracing(int[] nums,boolean[] used){
if(path.size() == nums.length){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i] == true) continue;
path.add(nums[i]);
used[i] = true;
backtracing(nums,used);
used[i] = false;
path.removeLast();
}
}
}
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
思路
涉及到去重
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
思路
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
used = new boolean[nums.length];
backtracing(nums);
return res;
}
public void backtracing(int[] nums){
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++){
//去重
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == true)
continue;
if(used[i] == true)
continue;
path.add(nums[i]);
used[i] = true;
backtracing(nums);
used[i] = false;
path.removeLast();
}
}
}
面试题 08.07. 无重复字符串的排列组合
无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。
代码
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
boolean[] used;
public String[] permutation(String S) {
char[] arr = S.toCharArray();
used = new boolean[arr.length];
backtracing(arr);
return res.toArray(new String[res.size()]);
}
//确定回溯参数
public void backtracing(char[] arr){
//终止条件
if(path.length() == arr.length){
res.add(path.toString());
return;
}
for(int i = 0; i < arr.length; i++){
if(used[i] == true) continue;
//标记为已访问过
used[i] = true;
path.append(arr[i]);
backtracing(arr);
used[i] = false;
path.deleteCharAt(path.length() - 1);
}
}
}
面试题 08.08. 有重复字符串的排列组合
有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。
代码
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
//排列问题一定要有标记数组
boolean[] used;
public String[] permutation(String S) {
//去重一定要对数组进行排序
char[] arr = S.toCharArray();
Arrays.sort(arr);
used = new boolean[arr.length];
backtracing(arr);
return res.toArray(new String[res.size()]);
}
public void backtracing(char[] arr){
if(path.length() == arr.length){
res.add(path.toString());
return;
}
for(int i = 0; i < arr.length; i++){
//去重操作
if(i > 0 && arr[i] == arr[i-1] && used[i-1] == false){
continue;
}
if(used[i] == true){
continue;
}
used[i] = true;
path.append(arr[i]);
backtracing(arr);
used[i] = false;
path.deleteCharAt(path.length() - 1);
}
}
}
剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
同上一题
代码
class Solution {
StringBuilder path = new StringBuilder();
List<String> res = new ArrayList<>();
boolean[] used;
public String[] permutation(String s) {
//全排列 + 回溯
//要考虑有重复元素的情况---去重
used = new boolean[s.length()];
char[] arr = s.toCharArray();
Arrays.sort(arr);
backtracking(arr);
return res.toArray(new String[res.size()]);
}
public void backtracking(char[] arr){
if(path.length() == arr.length){
res.add(path.toString());
return;
}
for(int i = 0; i < arr.length; i++){
if(i > 0 && arr[i] == arr[i-1] && used[i - 1] == false){
continue;
}
if(used[i] == false){
used[i] = true;
path.append(arr[i]);
backtracking(arr);
path.deleteCharAt(path.length()-1);
used[i] = false;
}
}
}
}
77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
思路
函数里一定有两个参数,既然是集合n里面取k的数,那么n和k是两个int型的参数。然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,…,n] )。
为什么要有这个startIndex呢?
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex。
void backtracking(int n, int k, int startIndex)
剪枝优化
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程:
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracing(n,k,1);
return res;
}
public void backtracing(int n,int k,int startIndex){
if(k == path.size()){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
backtracing(n,k,i+1);
path.removeLast();
}
}
}
216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9;
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
思路
剪枝操作:
1.已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。
if (sum > targetSum) { // 剪枝操作
return;
}
2.for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracing(k,n,1,0);
return res;
}
public void backtracing(int k,int n,int startIndex,int sum){
if(sum > n){
return;
}
//终止条件
if(path.size() == k){
if(sum == n){
res.add(new ArrayList<>(path));
}
return;
}
for(int i = startIndex; i <= 9-(k-path.size())+1; i++){
path.add(i);
sum += i;
backtracing(k,n,i+1,sum);
sum -= i;
path.removeLast();
}
}
}
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
代码
class Solution {
//本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而77. 组合 和216.组合总和III都是是求同一个集合中的组合!
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0)
return res;
String[] dig = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backtracing(digits,dig,0);
return res;
}
//这个num是记录遍历第几个数字了,就是用来遍历digits的,同时num也表示树的深度。
public void backtracing(String digits,String[] dig,int num){
//输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果num 等于 输入的数字个数(digits.size)了(本来num就是用来遍历digits的)
if(num == digits.length()){
res.add(path.toString());
return;
}
//首先要取num指向的数字,并找到对应的字符集(手机键盘的字符集)。
String str = dig[digits.charAt(num)-'0'];
for(int i = 0; i < str.length(); i++){
path.append(str.charAt(i));
// 递归,注意sum+1,一下层要处理下一个数字了
backtracing(digits,dig,num+1);
path.deleteCharAt(path.length()-1);
}
}
}
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个
思路
1.递归函数参数
2.递归终止条件 sum大于target和sum等于target。
3.单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合。
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startIndex
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracing(candidates,target,0);
return res;
}
public void backtracing(int[] candidates,int target,int startIndex){
if(sum > target){
return;
}
if(sum == target){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = startIndex; i < candidates.length; i++){
sum += candidates[i];
path.add(candidates[i]);
backtracing(candidates,target,i);
sum -= candidates[i];
path.removeLast();
}
}
}
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
思路
1.本题candidates 中的每个数字在每个组合中只能使用一次。
2.本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合。所以要在搜索的过程中就去掉重复组合。
元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
树层去重的话,需要对数组排序!
1.递归函数参数
2.递归终止条件
3.单层搜索的逻辑
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracing(candidates,target,0);
return res;
}
public void backtracing(int[] candidates, int target,int startIndex){
if(sum > target){
return;
}
if(sum == target){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i = startIndex; i < candidates.length; i++){
//去重操作
if(i > startIndex && candidates[i] == candidates[i-1]){
continue;
}
sum += candidates[i];
path.add(candidates[i]);
backtracing(candidates,target,i+1);
sum -= candidates[i];
path.removeLast();
}
}
}
78. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路
组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
- 递归函数参数
- 递归终止条件
剩余集合为空的时候,就是叶子节点。
那么什么时候剩余集合为空呢?
就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了 - 单层搜索逻辑
- 求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树。
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtracing(nums,0);
return res;
}
public void backtracing(int[] nums,int startIndex){
res.add(new ArrayList<Integer>(path));
if(startIndex >= nums.length){
return;
}
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backtracing(nums,i+1);
path.removeLast();
}
}
}
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
!!! 去重
代码
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtracing(nums,0);
return res;
}
public void backtracing(int[] nums,int startIndex){
res.add(new ArrayList<Integer>(path));
for(int i = startIndex; i < nums.length; i++){
if(i > startIndex && nums[i] == nums[i-1]){
continue;
}
path.add(nums[i]);
backtracing(nums,i+1);
path.removeLast();
}
}
}
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
if(root == null) return res;
dfs(root,target);
return res;
}
public void dfs(TreeNode root,int target){
if(root == null) return;
path.add(root.val);
target -= root.val;
//1.各节点值的和等于目标值target时 2.根节点到叶子节点形成的路径,将此路径加入列表结果
if(target == 0 && root.left == null && root.right == null)
//new LinkedList(path)相当于复制了一份path到res中,因为后面path会发生变化,所以不能写成res.add(path).
res.add(new LinkedList(path));
//先序遍历 根左右
dfs(root.left,target);
dfs(root.right,target);
//回溯
path.removeLast();
}
}