关于数组排列想说的话
最近应聘了不少公司,在某客网上网上笔试时遇到了一个排列的问题,没做出来,最后也没收到offer。于是就决定解决一下自己的知识漏洞。
三种类型
排列问题,面试中最常见的有3种类型:
- 全排列;
- 下一个排列 ;
- 第N个排列 ;
1. 全排列问题
例如:leetcode第46题
给定一个没有重复数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
题目链接: https://leetcode-cn.com/problems/permutations/.
对于此类问题,排列的顺序不重要,可以使用“交换”的思想回溯写出全排列。生成 N!个全排列需要时间O(N×N!)。该算法可以解决第一类问题。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
if(nums.empty()) return {};
vector<vector<int>> res;
backTrade(res, nums, 0);
return res;
}
void backTrade( vector<vector<int>>& res, vector<int>& nums, int start) {
if(start == nums.size() - 1){
res.push_back(nums);
return;
}
for(int i=start; i<nums.size() ; i++){
swap(nums[start], nums[i]);
backTrade(res, nums, start+1);
swap(nums[start], nums[i]);
}
}
};
但到这里还没完,Leetcode第47题立马就给出了变化
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:[ [1,1,2], [1,2,1], [2,1,1] ]
题目链接: https://leetcode-cn.com/problems/permutations-ii/submissions/.
这样的话再使用46题的解法会产生重复项,只需要在恰当的地方去除重复项即可,代码如下:
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& a) {
vector<vector<int>> res;
bt(a, res, 0);
return res;
}
void bt(vector<int>& a, vector<vector<int>>& res, int level) {
if (level == a.size()) {
res.push_back(a);
return;
}
//枚举的时候去重,只用没有排过的
unordered_set<int> uniq;
for (int i = level; i < a.size(); ++i) {
if (uniq.count(a[i])) {
continue; //already used
}
swap(a[i], a[level]);
bt(a, res, level+1);
swap(a[i], a[level]);
uniq.insert(a[i]);
}
}
};
作者:Emilio
链接:https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-mei-ju-shi-jian-dan-qu-zhong-ji-ke-by-emili/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.下一个排列
例如:leetcode第31题
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1
题目链接: https://leetcode-cn.com/problems/next-permutation.
对这一类题目,可以使用D.E. Knuth 算法按照字典顺序生成全排列,在O(N) 时间内完成。
一遍扫描
首先,我们观察到对于任何给定序列的降序,没有可能的下一个更大的排列。
例如,以下数组不可能有下一个排列: [9, 5, 4, 3, 1]
我们需要从右边找到第一对两个连续的数字 a[i]a[i] 和 a[i-1]a[i−1],它们满足 a[i]>a[i-1]a[i]>a[i−1]。现在,没有对 a[i-1]a[i−1] 右侧的重新排列可以创建更大的排列,因为该子数组由数字按降序组成。因此,我们需要重新排列 a[i-1]a[i−1] 右边的数字,包括它自己。
现在,什么样的重新排列将产生下一个更大的数字?我们想要创建比当前更大的排列。因此,我们需要将数字 a[i-1]a[i−1] 替换为位于其右侧区域的数字中比它更大的数字,例如 a[j]a[j]。
我们交换数字 a[i-1]a[i−1] 和 a[j]a[j]。我们现在在索引 i-1i−1 处有正确的数字。 但目前的排列仍然不是我们正在寻找的排列。我们需要通过仅使用 a[i-1]a[i−1]右边的数字来形成最小的排列。 因此,我们需要放置那些按升序排列的数字,以获得最小的排列。
但是,请记住,在从右侧扫描数字时,我们只是继续递减索引直到我们找到 a[i]a[i] 和 a[i-1]a[i−1] 这对数。其中,a[i] > a[i-1]a[i]>a[i−1]。因此,a[i-1]a[i−1] 右边的所有数字都已按降序排序。此外,交换 a[i-1]a[i−1] 和 a[j]a[j] 并未改变该顺序。因此,我们只需要反转 a[i-1]a[i−1] 之后的数字,以获得下一个最小的字典排列。
代码如下:
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int flag = 0;
int i = nums.size()-1;
for( ; i>0 ;i--){
if(nums[i-1] < nums[i]){
flag = i-1;
break;
}
}
if(i==0){
sort(nums.begin(), nums.end());
}
else{
sort(nums.begin() + i, nums.end());
int j = i;
for(; j<nums.size();j++){
if(nums[j] > nums[flag])
break;
}
int t = nums[j];
nums[j] = nums[flag];
nums[flag] = t;
}
}
};
3.第N个排列
虽然前两个算法很好使,但是前两个算法不能解决第三类问题,原因就在于:
1.良好的时间复杂度,即无回溯。
2.先前排列未知,即不能使用 D.E. Knuth 算法。
首先解决这个问题,取巧的方法确实有,就是C++的 next_permutation() 函数,具体用法可自行网上搜索。
用这个方法确实是因为这个方法太方便了。
class Solution {
public:
string getPermutation(int n, int k) {
vector<char> nums(n);
for(int i=0; i<n; i++)
nums[i]=(char)(i+1+'0');
while(k-->1)
next_permutation(nums.begin(), nums.end());
string ans="";
for(int i=0; i<n; i++)
ans+=nums[i];
return ans;
}
};
还有康拓展开
class Solution {
public:
string getPermutation(int n, int k) {
vector<char> chs={'1','2','3','4','5','6','7','8','9'};
const int factor[]={1,1,2,6,24,120,720,5040,40320,362880};
string str;
for(--k; n--; k%=factor[n]){
const int i=k/factor[n];
str.push_back(chs[i]);
chs.erase(chs.begin()+i);
}
return str;
}
};
当然,递归理论上能解决此类问题,但是一般网站都不会过,因为要很好的剪枝才能优化的很好。先去睡了,有空再跟。