1 BM55 没有重复项数字的全排列
1 描述
给出一组数字,返回该组数字的所有排列
例如:
[1,2,3]的所有排列如下
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2], [3,2,1].
(以数字在数组中的位置靠前为优先级,按字典序排列输出。)
数据范围:数字个数 0 < <n≤6
要求:空间复杂度 O(n!) ,时间复杂度 O(n!)
2 牛客官解
终止条件: 要交换位置的下标到了数组末尾,没有可交换的了,那这就构成了一个排列情况,可以加入输出数组。
返回值: 每一级的子问题应该把什么东西传递给父问题呢,这个题中我们是交换数组元素位置,前面已经确定好位置的元素就是我们返还给父问题的结果,后续递归下去会逐渐把整个数组位置都确定,形成一种排列情况。
本级任务: 每一级需要做的就是遍历从它开始的后续元素,每一级就与它交换一次位置。
如果只是使用递归,我们会发现,上例中的1与3交换位置以后,如果2再与4交换位置的时候,我们只能得到3412这种排列,无法得到1432这种情况。这是因为遍历的时候1与3交换位置在2与4交换位置前面,递归过程中就默认将后者看成了前者的子问题,但是其实我们1与3没有交换位置的情况都没有结束,相当于上述图示中只进行了第一个分支。因此我们用到了回溯。处理完1与3交换位置的子问题以后,我们再将其交换回原来的情况,相当于上述图示回到了父节点,那后续完整的情况交换我们也能得到。
//遍历后续的元素
for(int i = index; i < num.size(); i++){
//交换二者
swap(num, i, index);
//继续往后找
recursion(res, num, index + 1);
//回溯
swap(num, i, index);
}
具体做法:
step 1:先将数组排序,获取字典序最小的排列情况。
step 2:递归的时候根据当前下标,遍历后续的元素,交换二者位置后,进入下一层递归。
step 3:处理完一分支的递归后,将交换的情况再换回来进行回溯,进入其他分支。
step 4:当前下标到达数组末尾就是一种排列情况。
图示:
void recursion(vector<vector<int>>& res, vector<int>& num, int index) {
//分枝进入结尾,找到一种排列
if (index == num.size() - 1)//当前下标到达数组末尾就构成了一个排列情况,可以加入输出数组。
res.push_back(num);
else {
//遍历后续的元素
for (int i = index; i < num.size(); i++) {
//交换二者
swap(num[i], num[index]);
//继续往后找
recursion(res, num, index + 1);
//回溯
swap(num[i], num[index]);
}
}
}
vector<vector<int> > permute(vector<int>& num) {
//先按字典序排序
sort(num.begin(), num.end());
vector<vector<int> > res;
//递归获取
recursion(res, num, 0);
return res;
}
可以看到输出结果,与实例不一致,并没有按照字典序列,,据百度及其他资料来看,子后一个组合应该是 321
时间复杂度:O(n∗n!),n个元素的数组进行全排列的递归,每次递归都要遍历数组
空间复杂度:O(n),递归栈的最大深度为数组长度n,res属于返回必要空间
3 力扣官解
注意:力扣题目 按任意顺序 返回答案,而牛客要求 字典顺序输出
举个简单的例子,假设我们有 [2, 5, 8, 9, 10][2,5,8,9,10] 这 55 个数要填入,已经填到第 33 个位置,已经填了 [8, 9][8,9] 两个数,那么这个数组目前为 [8, 9|2, 5, 10][8,9 ∣ 2,5,10] 这样的状态,分隔符区分了左右两个部分。假设这个位置我们要填 1010 这个数,为了维护数组,我们将 22 和 1010 交换,即能使得数组继续保持分隔符左边的数已经填过,右边的待填 [8, 9, 10|2, 5][8,9,10 ∣ 2,5] 。
当然善于思考的读者肯定已经发现这样生成的全排列并不是按字典序存储在答案数组中的,如果题目要求按字典序输出,那么请还是用标记数组或者其他方法。
下面的图展示了回溯的整个过程:
//力扣
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>> permute2(vector<int>& nums) {
vector<vector<int> > res;
backtrack(res, nums, 0, (int)nums.size());
return res;
}
4 测试输出
int main() {
vector<vector<int>> res,res2;
vector<int> array = {1 ,2, 3, 4};
//牛客
res = permute(array);
int n = res.size();
int m = res[0].size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m;j++) {
cout << res[i][j];
}
cout << endl;
}
//力扣
cout << endl;
res2 = permute2(array);
n = res2.size();
m = res2[0].size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cout << res2[i][j];
}
cout << endl;
}
return 0;
}
5 疑问? 什么是字典顺序,牛客和力扣答案一样(但要求字典序、任意序)
5.1 百度介绍
在数学中,字典或词典顺序(也称为词汇顺序,字典顺序,字母顺序或词典顺序)是基于字母顺序排列的单词按字母顺序排列的方法。 这种泛化主要在于定义有序完全有序集合(通常称为字母表)的元素的序列(通常称为计算机科学中的单词)的总顺序。
对于数字1、2、3…n的排列,不同排列的先后关系是从左到右逐个比较对应的数字的先后来决定的。例如对于5个数字的排列 12354和12345,排列12345在前,排列12354在后。按照这样的规定,5个数字的所有的排列中最前面的是12345,最后面的是 54321。
我们先看一个例子。
示例: 1 2 3的全排列如下:
1 2 3 , 1 3 2 , 2 1 3 , 2 3 1 , 3 1 2 , 3 2 1
我们这里是通过字典序法找出来的。
那么什么是字典序法呢?
从上面的全排列也可以看出来了,从左往右依次增大,对这就是字典序法。可是如何用算法来实现字典序法全排列呢?
我们再来看一段文字描述:(用字典序法找124653的下一个排列)
你主要看红色字体部分就行了,这就是步骤。
如果当前排列是124653,找它的下一个排列的方法是,从这个序列中从右至左找第一个左邻小于右邻的数,
如果找不到,则所有排列求解完成,如果找得到则说明排列未完成。
本例中将找到46,计4所在的位置为i,找到后不能直接将46位置互换,而又要从右到左到第一个比4大的数,
本例找到的数是5,其位置计为j,将i与j所在元素交换125643,
然后将i+1至最后一个元素从小到大排序得到125346,这就是124653的下一个排列。
下图是用字典序法找1 2 3的全排列(全过程):
总结得出字典排序算法四步法:
字典排序:
第一步:从右至左找第一个左邻小于右邻的数,记下位置i,值list[a]
第二部:从右边往左找第一个右边大于list[a]的第一个值,记下位置j,值list[b]
第三步:交换list[a]和list[b]的值
第四步:将i以后的元素重新按从小到大的顺序排列
举例:125643的下一个字典序列
第一步:右边值大于左边的3<4,4<6,6>5,则i=2,list[a]=5
第二步:从右往左找出第一个右边大于list[a]=5的值,找到6>5,j=3;list[b]=6;
第三步:交换list[a]和list[b]的值,序列125643->126543
第四步:将位置2以后的元素重新排序,126543->126345;
结束: 126345即125643的下一个序列
代码实现(C语言):
#include <stdio.h>
#define swap(a,b) {int temp=a;a=b;b=temp;} //交换a,b值
void sort(int arr[],int start,int end)//冒泡排序,从start到end的排序,使用时注意是数组的下标,如数组下标0-3排序,sort(arr,0,3)
{
int i,j;
for(i=0;i<=end-start;i++)
{
for(j=start;j<=end-i-1;j++)
{
if(arr[j]>arr[j+1])
swap(arr[j],arr[j+1]);
}
}
}
void permutation(int arr[],int n) //字典排序
{
int num=1,i=0,j=0,j1=0,k=0,a,b;
for(i=1;i<=n;i++)//算出需要执行的次数,即全排列的次数,共n!种排法
{
num=num*i;
}
sort(arr,0,n-1);//先对数组进行一次按从小到大排列排序
for(k=num;k>0;k--) //进行num次循环
{
for(i=0;i<n;i++) //输出排好的数组,第一次直接按最小的输出
{
printf("%d",arr[i]);
}
printf("\n");
for(j=n-1;j>0;j--)
{
if(arr[j-1]<arr[j]) //这是字典排序的第一步,自己定义的四步法,获取arr[a]值
{
a=j-1;
break;
}
}
for(j1=n-1;j1>=0;j1--)
{
if(arr[j1]>arr[a]) //这是字典排序第二步,获取arr[b]的值
{
b=j1;
break;
}
}
swap(arr[a],arr[b]); //这是第三步
sort(arr,a+1,n-1); //这是第四步
}
}
int main()
{
int arr[]={1,2,4,3};
permutation(arr,4);
return 0;
}
2 BM56 有重复项数字的全排列
给出一组可能包含重复项的数字,返回该组数字的所有排列。结果以字典序升序排列。
数据范围: 0 < n ≤8 ,数组中的值满足 -1 ≤val≤5
要求:空间复杂度 O(n!),时间复杂度 O(n!)
示例1
输入:[1,1,2]
返回值:[[1,1,2],[1,2,1],[2,1,1]]
示例2
输入:[0,1]
返回值:[[0,1],[1,0]]
此题,与上一题区别在于,去除重复排序
2.1 思路,set去重 在上一题基础上去重
二维维数组改为 set<vector< >>
void recursion(set<vector<int>>&res, vector<int>&num, int index){
修改,return res ;
return vector<vector<int>> {res.begin(),res.end()};
完整代码
vector<vector<int> > permuteUnique(vector<int> &num) {
set<vector<int>> res;//集合去重
sort(num.begin(),num.end());
recursion(res,num, 0);
return vector<vector<int>> {res.begin(),res.end()};
}
void recursion(set<vector<int>>&res, vector<int>&num, int index){
if(index == num.size()-1)
res.insert(num); //一种排列完成,就加入输出数组
else{
for(int i=0;i<num.size();i++){
//交换
swap(num[i],num[index]);
//继续向后
recursion(res,num,index+1);
//回溯到父问题,为下一次排列做准备
swap(num[i],num[index]);
}
}
}
2.2 利箭代码
class Solution {
public:
set<vector<int>> temp;
void dfs(vector<int>& num,int cur)
{
if(cur == num.size())
{
temp.insert(num);
return ;
}
for(int i = cur;i < num.size();i++)
{
swap(num[cur],num[i]);
dfs(num,cur+1);
swap(num[cur], num[i]);
}
}
vector<vector<int> > permuteUnique(vector<int> &num) {
//sort(num.begin(),num.end());
dfs(num,0);
vector<vector<int>> ans;
for(auto &i : temp)
ans.push_back(i);
return ans;
}
};