给定一个不含重复数字的数组 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、2、3。三个放牌的位置,我们需要从左往右寻找所有的排列组合。
如果我们按照每一个位置按1,2,3的顺序来排放,
第一个位置 1 第二个位置 2 第三个位置 3 ,然后将牌回收直到可以排下一个组合
第一个位置 1 第二个位置 3 第三个位置 2 ,在回收了牌之后,不能放原来的牌
第一个位置 2 第二个位置 1 第三个位置 3
第一个位置 2 第二个位置 3 第三个位置 1 。。。。。
这样我们可以得到所有序列,我们来模拟这个过程,发现比较困难。
我们缩小问题规模,数组[1,2],位置[1,2]
第一个位置 1 第二个位置 2
第一个位置 2 第二个位置 1
再缩小数组[1],位置[1]
第一个位置 1
寻找规律,可以得到,第1个位置上重新排列,其排序结果相当于2~n的子序列的一次重新排列。进而可以理解
全排列公式:n!=n*(n-1)*(n-2)*....1;
这样排列的规律是:每一个位置按从小到大的顺序来排放,前一个位置的排列重置后面子序列的排列。
根据这个规律可以写出流程:
第一个位置 放1,第二个位置 放2 第三个位置 放3,没有下一个位置了。产生一个序列。
收回牌,收回3位置的牌,3,由于每一个位置递增排列,没有比三大的数,回退。
收回第二个位置的牌,2,第二个位置可以放3。排第三个位置。
第三个位置重排,可以现有牌的最小牌,2,没有下一个位置了。产生一个序列。
。。。。。
如何实现这个流程,解决问题:
1、如何放牌?
1)将手里的牌从小到大放牌,如果没有可选择的牌就结束放牌。如果有,就放完该位置,保存该位置的状态,准备放下一个位置。
我们来标注一下保存该位置的状态,到时候注意这个操作会产生的效果
将手里的牌从小到大放牌,这个产生的效果可以是这样
因为我们规定按照每一个位置按1,2,3的顺序来排放----->这个可以由循环实现
但是我们如何将这个状态转化为下一个状态呢?
保存每一个位置的状态,由于我们只放牌,我们只需要知道,我们把哪一张牌放到了哪一个位置,递归能够很好的解决保存状态的问题。
产生的效果:当保存状态之后,将手里的牌从小到大放牌,当回退重排时,就会基于上一个回收的牌,变成从x+1到更大放牌,至此我们实现了第二种排列
我们在通过放牌的思路,梳理一下其他的状态,都可以完成,至此我们放牌的策略算是构建成功。
但是这个实现是基于收牌的基础上的,我们来看一下如何收牌?
2、如何收牌? 没有下一个位置时或者回收位置没有可排序序列时收牌,表示递归结束。至此我们的问题变成了如何实现递归算法。
递归
问题1:如何递推?每一个位置,通过循环从小到大递增寻找没有使用的元素,为每一个位置赋值,并标记为已经使用,记录该位置的信息,并为下一个位置赋值。
//代码实现
//book[]数组表示下标为i的元素的状态,如果没有放为true,放了为false
//每一个存放数据的位置,通过循环从小到大递增寻找没有使用的元素
boolean[] book=new boolean[4]{true};//0不用,我们使用的牌从1开始
//boolean[]类型不能这样初始化
for(int i=1;i<=3;i++){//循环从小到大,同时也记录了当前所放的牌是哪一张。
//这张牌回收之后用的是下一张
if(book[i]==true){//如果牌这张牌没有放,可以将这张牌放下去,并标记这张牌已经被放了。
a[x]=i;
book[i]=false;
dfs(x+1);
}
}
问题2: 如何回溯?递归实现给下一个位置赋值,回退,标记当前元素为未被使用的状态,也就是回收当前位置上的值。
boolean[] book=new boolean[4]{true};//0不用,我们使用的牌从1开始
public void dfs(int x)//表示存储点,从0开始
{
if(x==size+1)//表示存储点已经没有了
return ;
for(int i=1;i<=3;i++){//循环从小到大,同时也记录了当前所放的牌是哪一张。
//这张牌回收之后用的是下一张
if(book[i]==true){//如果牌这张牌没有放,可以将这张牌放下去,并标记这张牌已经被放了。
a[x]=i;
book[i]=false;
dfs(x+1);
book[i]=true;
}
//如果这张牌已经用过了,就看下一张牌有没有用过
}
}
代码实现
class Solution {
List<List<Integer>> arr=new ArrayList();
int[] book;//用于标记下标为i的数据是否已经被使用,0为未被使用
int len;//数据的个数==存储位置的个数
List<Integer> list=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
book=new int[nums.length];
len=nums.length;
f(nums,1);//位置从1开始
return arr;
}
public void f(int[] nums,int x)//x表示当前位置
{
if(x==len+1)//存储位置的下一位
{
arr.add(new ArrayList<Integer>(list));
return ;
}
for(int i=0;i<len;i++)
{
if(book[i]==0){
list.add(nums[i]);//往末尾添加元素
book[i]=1;
f(nums,x+1);
list.remove(list.size()-1);//删除末尾元素
book[i]=0;
}
}
}
}
如果数组中存在重复的数字?
如1 2 2 如何处理?相关题解,请看我大哥的题解.
set实现去重的原理是:在前一个位置未重排前,防止重复的数填入同一个位置。
比如 1 2 2
回收2 2,当前的set中存储了一个2,就不能重新,set记录的是当前执行函数中的该位置的存储记录。生命周期是当前递归函数。
去重三行搞定,放入合适的位置。
Set<Integer> set=new HashSet<>();
if(set.contains(nums[i])) continue;
set.add(nums[i]);
关键代码
Set<Integer> set=new HashSet<>();
for(int i=0;i<len;i++)
{
if(book[i]==0){
if(set.contains(nums[i])) continue;
set.add(nums[i]);
list.add(nums[i]);//往末尾添加元素
book[i]=1;
f(nums,x+1);
list.remove(list.size()-1);//删除末尾元素
book[i]=0;
}
}
代码
class Solution {
List<List<Integer>> arr=new ArrayList();
int[] book;//用于标记下标为i的数据是否已经被使用,0为未被使用
int len;//数据的个数==存储位置的个数
List<Integer> list=new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
book=new int[nums.length];
len=nums.length;
f(nums,1);//位置从1开始
return arr;
}
public void f(int[] nums,int x)//x表示当前位置
{
if(x==len+1)//存储位置的下一位
{
arr.add(new ArrayList<Integer>(list));
return ;
}
Set<Integer> set=new HashSet<>();
for(int i=0;i<len;i++)
{
if(book[i]==0){
if(set.contains(nums[i])) continue;
set.add(nums[i]);
list.add(nums[i]);//往末尾添加元素
book[i]=1;
f(nums,x+1);
list.remove(list.size()-1);//删除末尾元素
book[i]=0;
}
}
}
}