46.全排列
46. 全排列 - 力扣(LeetCode)https://leetcode.cn/problems/permutations/description/
题目描述:
给定一个不含重复数字的数组 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]
思路分析:
今天的全排列题目就不在和之前的求组合的问题一样了,之前从一个集合中取子集,需要使用startIndex来控制每次搜索开始的位置。但是全排列则不能用startIndex来控制搜索的开始位置,因为一旦用了startIndex,则startIndex前面的元素就遍历不到了。所以,全排列的思路就是每次遍历都是将nums数组从头到尾遍历一遍(注意是每一次遍历,就是常写的for循环那里),已经在path中的数字不能再用。
那么如何使得用过的数字不再用呢?我这里的做法是使用一个used数组来做记录。对应的树形结构如下:
下面开始回溯三部曲:
第一步:确认回溯函数的参数和返回值。返回值老规矩void,参数除了题目的nums原数组,还需要used数组。通过used数组每次传进来上一层递归所使用元素的情况。used数组和nums数组长度一样。used某个位置的元素为0,则表明nums对应位置的元素没被用过;为1则表示被用过。
vector<int> path;
vector<vector<int>> result;
void backTracking(vector<int> & nums, vector<int>& used){}
第二步,确定回溯中之条件。当path搜集到的元素个数和nums一样时,说明遍历完了,可以用result记录了。
void backTracking(vector<int> & nums, vector<int>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
}
第三部:单层搜索逻辑。这个时候就要用到前面所说的 每次遍历都是将nums数组从头到尾遍历一遍,
void backTracking(vector<int> & nums, vector<int>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
//每次都是从头开始遍历nums中的元素
for(int i = 0; i<nums.size(); i++){
if(used[i] == 1){ //如果这个元素对应的标志位为1,表明已经用过,则continue进入下一轮循环,即开始检索下一个元素
continue;
}
//走到这一步说明当前元素还没被用过,马上要用
used[i] = 1; //标志为置0
path.push_back(nums[i]); //path记录
backTracking(nums, used); //递归
//两步回溯
used[i] = 0;
path.pop_back();
}
}
整体代码如下:
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backTracking(vector<int> & nums, vector<int>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
for(int i = 0; i<nums.size(); i++){
if(used[i] == 1){
continue;
}
used[i] = 1;
path.push_back(nums[i]);
backTracking(nums, used);
used[i] = 0;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
int N = nums.size();
vector<int> used(N,0);
backTracking(nums, used);
return result;
}
};
由于used数组不是局部变量,它需要在每次递归时都保留上一层元素是否使用的具体情况,所以我们将其初始化在permute函数中,并使用引用类型的形参来保持数据的更新。如果想节省空间,vector<int> used可以换成bool类型,即vector<bool> used。
总结
大家此时可以感受出排列问题的不同:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
47.全排列 II
47. 全排列 II - 力扣(LeetCode)https://leetcode.cn/problems/permutations-ii/description/
题目描述:
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,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]]
思路分析:
本题与46题不同的是,46题所有元素都不重复,所以不用担心因为元素重复所带来的排列重复的问题,所以46题可以好不担心的每次都从0位置开始遍历nums数组。
但是本题存在了重复元素,需要当心得到重复的排列,所以可以先画出对应的树形结构,看看重复出现在什么地方:
在40组合问题II、和90子集II中我们分别详细讲解了组合问题和子集问题如何去重。去重的前提是对nums原数组进行排序。我们尝试一下排序看行不行。发现当对数组进行排序后进行去重依然试用这道排列题。因为之前的i>0 && nums[i-1] == nums[i]可以帮助我们判断当前元素与其前一个元素是否相等。如果相等,那么去重问题的关键就成了怎么限定我访问的是第二个重复的元素呢?
首先,我们提前对数组进行了排序,所以能保证元素的增长顺序,其次,当i>0 && nums[i-1] == nums[i]这个条件满足时,说明有两个相邻元素时相等的,那么此时used数组就可以派上用场了。访问两个相同元素的前一个时,used置1,然后递归回溯,回溯后used对应位置会置0。走到这里表明两个相同元素中的前一个已经访问完毕,然后for循环i++,会访问到两个相同元素中的后一个,也就是 i>0 && nums[i-1] == nums[i]满足了,但是还不够,因为此时该元素不能被记录,所以还需要加的限制条件就是used[i-1] = 0, i>0 && nums[i-1] == nums[i]&&used[i-1] = 0 表明这是两个相同元素中的后一个,且used[i-1] = 0表明前面的元素已经遍历过了,该记录的也记录了,所以着第二个元素就直接continue跳过。
整体代码如下:
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int> &nums, vector<bool>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
//由于时排列问题,不用startIndex,每次都从头遍历nums
for(int i = 0; i< nums.size(); i++){
//path已经记录过的,标志为1,直接continue到下一个元素
if(used[i] == true){
continue;
}
//如果当前元素没有用过,那么就该判断是不是相同元素中的后一个,如果是,直接continue
if(i>0 && nums[i-1] == nums[i] && used[i-1]==false){
continue;
}
//正常元素的记录、递归、回溯
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
used[i] = false;
path.pop_back();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(), false);
backtracking(nums,used);
return result;
}
};