60. 第k个排列
题目描述
给出集合[1,2,3,…,n]
,其所有元素共有n!
种排列。
按大小顺序列出所有排列情况,并一一标记,当n = 3
时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定n
和 k
,返回第k
个排列。
说明:
给定n
的范围是 [1, 9]
。
给定 k
的范围是[1, n!]
。
示例 1:
输入: n = 3, k = 3
输出: "213"
示例 2:
输入: n = 4, k = 9
输出: "2314"
全排列暴力解法
n
个元素的元组,枚举遍历每一个位置的情况,返回满足条件的元组,这里元组的个数为n
,是固定的。
实现1
这里是在原地保存每一个位置能放置的元素的信息
class Solution {
public:
string getPermutation(int n, int k) {
string ret;
//原地保存元素容器
string s;
for(int i= 1;i <= n;i++)
s.push_back('0'+i);
getPermutationCore(s,0,ret,k);
return ret;
}
//注意这里的参数s的类型只能是传值参数,而不能是引用参数
bool getPermutationCore(string s,int index,string &ret,int &k)
{
if(index == s.size())
{
k--;
if(k == 0)
{
ret = s;
return true;
}
}
sort(s.begin()+index,s.end());//参数s的类型为传值参数主要是因为这里
for(int i = index;i < s.size();i++)//的排序操作
{
swap(s[index],s[i]);
if(getPermutationCore(s,index+1,ret,k);)
return true;
swap(s[index],s[i]);
}
return false;
}
};
实现2
还是全排列爆搜,但是没有用到排序,这里是单独使用了一个额外的数据结构保存每一个位置能够放置的元素的信息,没有在原地保存这个信息
class Solution {
public:
string getPermutation(int n, int k) {
string ret(n,'0');
//使用一个额外的容器存储结果排列每个位置可以放置的各个元素
vector<bool> visited(n,false);
dfs(visited,0,ret,k);
return ret;
}
//对index位置的情况进行枚举
bool dfs(vector<bool> &visited,int index,string &ret,int &k)
{
if(index == visited.size())
{
k--;
if(k == 0)//说明现在已经抵消掉K个排列了
return true;
}
//枚举index这个位置可以放置的所有可能的元素(按照从小到大的顺序)
for(int i = 0;i < visited.size();i++)
{
if(visited[i] == false)//说明i这个位置的元素还没有用到
{
visited[i] = true;
ret[index] = ('0'+i+1);
if(dfs(visited,index+1,ret,k))
return true;
visited[i] = false;
}
}
return false;
}
};
逆康托尔展开法
康托展开(Cantor expansion)是一个全排列到一个自然数的双射, 康托展开的实质是,计算当前排列在所有由小到大全排列中在它前面的全排列有多少个,因此是可逆的。
康托展开运算:
X = a_n*(n-1)!+a_(n-1)*(n-2)!+....+a1*0!
其中,a_i
为整数,并且0<=a_i<i,1<=i<=n
a_i
表示原数的第i
位(从右边数起[左边已经计算过个数了])在当前未出现的元素中(总共有i
个,所以a_i<i
,a_i
至多等于i-1
)排在它前面的数有几个,也即a_i
为下标值(以0
为起始下标),如果以1
为起始下标,则原数的第i
位元素是当前未出现的元素中的第(a_i + 1)
个元素,根据a_i
的含义a_1
天然的等于0
.这个计算出来的康托展开值X的含义是排在当前这个排列之前的排列有多少个,即如果以0
为坐标起点,当前的这个排列的下标值.
z
康托展开的逆运算
既然康托展开是一个双射(对应关系是唯一的),那么一定可以通过康托展开值求出原排列,即可以求出n
的全排列中第x
大排列。
实现1
class Solution {
public:
string getPermutation(int n, int k) {
vector<int> numFactorial(n+1,1);
for(int i = 1;i<=n;i++)
numFactorial[i] = i*numFactorial[i-1];
vector<bool> visited(n + 1, false);
string ret = "";
k -= 1;//需要减去自身,得到的才是这个排列所对应的康托尔展开值
//从高位到低位进行寻找
for(int i = n - 1; i >= 0; --i)
{
//这个除法取商的有效性可以通过将康托尔等式左右两边除以
//(n-1)!后观察结果可以得出结论
int index = k / numFactorial[i];
k = k % numFactorial[i];
int cnt = 0;
//在还未使用的数中,找第index+1大的数(从1开始)
for(int j = 1; j <= n; ++j)
{
if(visited[j] == false)//流式处理判断
{
++cnt;
if(cnt == index+1)
{ //java中对应的函数为.toString()
ret +=to_string(j);
visited[j] = true;
break;
}
}
}
}
return ret;
}
};
实现2
class Solution {
public:
string getPermutation(int n, int k) {
vector<int> numFactorial(n+1,1);
for(int i = 1;i<=n;i++)
numFactorial[i] = i*numFactorial[i-1];
vector<int> digit;
for(int i = 1;i<=n;i++)
digit.push_back(i);
string ret = "";
k -= 1;//需要减去自身,得到的才是这个排列所对应的康托尔展开值
for(int i = n - 1; i >= 0; --i)
{//从高位到低位进行寻找
int index = k / numFactorial[i];
k = k % numFactorial[i];
ret +=to_string(digit[index]);
auto iter = digit.begin()+index;
digit.erase(iter);//把用过的元素都删除掉,剩下的都是没有用过的
}
return ret;
}
};