Leetcode 46.全排列


题目描述:

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

1
2
输入:nums = [1]
输出:[[1]]

提示:

  • $1 <= nums.length <= 6$
  • $-10 <= nums[i] <= 10$
  • nums 中的所有整数 互不相同

链接:

https://leetcode-cn.com/problems/permutations


题目分析

  全排列也即题目中所给数字填在不同的位置构成的所有情况,我们可以按顺序从左到右填上数字,利用回溯的方法即可穷举所有的情况。我们需要填入的数字也即 nums 数组,如何标记已经填入的数字呢?一个方法是维护一个布尔数组用于记录,将已填入的数字标记为 true。每次填入时按顺序遍历,寻找未填入的数字进行填入,并更新这个标记。回溯之后记得将标记回溯为 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
void backtrack(vector<vector<int>>& result, vector<int>& now, vector<int>& nums, vector<bool>& flag, int cnt){
if(cnt == nums.size()){
result.push_back(now);
return;
}
for(int i = 0; i < nums.size(); i++){
if(!flag[i]){
now.push_back(nums[i]);
flag[i] = true;
backtrack(result, now, nums, flag, cnt+1);
now.pop_back();
flag[i] = false;
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
vector<int> now;
vector<bool> flag(nums.size(), false);

backtrack(result, now, nums, flag, 0);
return result;
}
};

  上面的代码我们可以发现,每一次填数时,都需要遍历 flag 数组寻找未填入的数字,这样需要的时间更长,也需要额外的空间,有没有什么办法可以不需要这个标记数组呢?答案是肯定的。我们可以将 nums 数组分割为两个部分,一部分表示已经填入的数,一部分表示未填入的数,每次填入一个数,就将其与分割线的位置交换,例如 {2, 3, | 4, 5, 6} 这样的数组,下一个填入 6 时就将 46 交换,并且移动分割线,变成 {2, 3, 6, | 5, 4},这样每次只需要从分割线的位置开始遍历就可以了,并且不需要额外的空间进行标记。值得注意的是,这样的交换会打乱数组的顺序,所以最后输出的结果并不是按照字典序来排列的。题目中说明可以按照任意顺序返回答案,因此可以这样做,如果有要求按照字典序,则只能按照上面的做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
void backtrack(vector<vector<int>>& result, vector<int>& now, vector<int>& nums, int begin){
if(begin == nums.size()){
result.push_back(now);
return;
}
for(int i = begin; i < nums.size(); i++){
now.push_back(nums[i]);
swap(nums[begin], nums[i]);
backtrack(result, now, nums, begin+1);
now.pop_back();
swap(nums[begin], nums[i]);
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
vector<int> now;

backtrack(result, now, nums, 0);
return result;
}
};

  时间复杂度:$O(n\times n!)$,其中 $n$ 是数组的大小。因为我们遍历了所有的排列,全排列的个数为 $n!$。 而对于每一个解,我们都需要 $O(n)$ 的时间将其复制到答案数组中,因此总的时间复杂度为 $O(n\times n!)$。
  空间复杂度:$O(n)$,其中 $n$ 是数组的大小。因为作为答案返回的数组不计入所需空间中,而我们需要的额外空间即递归调用的深度,最大深度为 $n$。

官方题解

  看完官方题解才发现,我们在动态维护的 nums 数组,左边表示的已填入的数字就是按照填入的顺序进行排列的,因此可以不用额外的一个 now 数组对结果进行存储,在填完所有的数字后 nums 即为这种排列本身,将其复制到答案中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
void backtrack(vector<vector<int>>& res, vector<int>& output, int first, int len){
// 所有数都填完了
if (first == len) {
res.emplace_back(output);
return;
}
for (int i = first; i < len; ++i) {
// 动态维护数组
swap(output[i], output[first]);
// 继续递归填下一个数
backtrack(res, output, first + 1, len);
// 撤销操作
swap(output[i], output[first]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int> > res;
backtrack(res, nums, 0, (int)nums.size());
return res;
}
};