题目介绍
力扣46题:https://leetcode-cn.com/problems/permutations/
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
分析
很明显,n个不同的数的全排列,应该有n!种情形。
这个问题需要暴力穷举。从前到后依次遍历每一个“位置”,每次填入一个数;而之后的一个位置,能够填入的可能性就会少一个。这样,直接用n重循环,考察每个位置就可以得到结果。
不过针对本题,数组的长度是不固定的,直接用多重循环的方式显然不妥。我们可以考虑使用递归。每个位置选取某个值,然后递归地对后面位置取值;由于本位置应该还可以取别的多个值,所以应该在一次递归完毕后,还可以“回到”递归调用的地方,继续取下一个值。所以,这就是回溯遍历的方法。
回溯实现
我们可以定义一个递归函数 backtrack(int i) ,表示当前要对第i个位置进行填充。递归函数分为两个情况:
- 如果 i >= n,说明我们已经填完了 n 个位置,找到了一个可行的解,我们将solution放入result数组中,递归结束。
- 如果 i < n,我们可以选择一个尚未使用的数填入当前位置。然后递归调用,对下一个位置进行判断处理。
为了快速判断某个数“尚未使用”,可以用一个set来保存已经填充过的数,使用某个数时直接判断一下是否存在就可以了。
代码如下:
// 回溯实现
public List<List<Integer>> permute1(int[] nums){
// 定义保存结果的List
ArrayList<List<Integer>> result = new ArrayList<>();
// 用一个ArrayList保存一组解
ArrayList<Integer> solution = new ArrayList<>();
// 从0位置开始填充数
backtrack1(nums, result, solution, 0);
return result;
}
// 定义一个辅助集合,保存已经用过的数
HashSet<Integer> filledNums = new HashSet<>();
// 实现一个回溯方法,方便递归调用
public void backtrack1(int[] nums, List<List<Integer>> result, List<Integer> solution, int i){
int n = nums.length;
// 首先判断退出递归调用的场景
if (i >= n){
result.add(new ArrayList<>(solution));
} else {
// 需要对i位置选数填入,遍历数组中所有数,取没有用过的数进行填充
for (int j = 0; j < n; j++){
if (!filledNums.contains(nums[j])){
// 没用过直接填入
solution.add(nums[j]);
filledNums.add(nums[j]);
// 递归调用,处理下一个位置
backtrack1(nums, result, solution, i + 1);
// 回溯,回退状态
solution.remove(i);
filledNums.remove(nums[j]);
}
}
}
}
复杂度分析
- 时间复杂度:O(nn!),其中n为nums长度。递归调用的总次数,就是n个数的全排列数,而每一次调用,都需要复制solution到结果中,这需要耗费O(n)的时间。所以总体的时间复杂度为O(n!) O(n) = O(n*n!)。
- 空间复杂度:O(n),其中n为nums长度。除结果数组外,用到了辅助的HashSet,额外的内存占用为O(n);另外,递归调用的深度为O(n),所以需要O(n)栈空间。总共的空间复杂度就是O(n)。