题面
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3],以下这些都可以视作arr的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1]。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]的下一个排列是[1,3,2]。 - 类似地,
arr = [2,3,1]的下一个排列是[3,1,2]。 - 而
arr = [3,2,1]的下一个排列是[1,2,3],因为[3,2,1]不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须原地 修改,只允许使用额外常数空间。
示例 1:
输入: nums = [1,2,3] 输出:[1,3,2]
示例 2:
输入: nums = [3,2,1] 输出:[1,2,3]
示例 3:
输入: nums = [1,1,5] 输出:[1,5,1]
提示:
1 <= nums.length <= 1000 <= nums[i] <= 100
分析
-
如果一个数组是降序的,说明其不存在下一个排列,需要返回升序排序的数组
-
如果一个数组是升序的,说明其下一个排列就是最后两个数交换得到的数组
-
将这个过程一般化,得到
下一个排列 下一个整数 相同点 从右向左,找第一个小于右侧相邻数字的数 从右向左,找第一个小于 的数 都是找第一个小于右侧相邻数字的数 找 右边最小的大于 的数 ,交换 和 把 加一,得到 增大这个数 反转 右边的数,把右边的数变成最小的排列 把 右边的数都变成 把右边的数变到最小 -
代码实现
class Solution { public: void nextPermutation(vector<int>& nums) { int n = nums.size(); // 第一步:从右向左找到第一个小于右侧相邻数字的数 nums[i] int i = n - 2; while (i >= 0 && nums[i] >= nums[i + 1]) { i--; } // 如果找到了,进入第二步;否则跳过第二步,反转整个数组 if (i >= 0) { // 第二步:从右向左找到 nums[i] 右边最小的大于 nums[i] 的数 nums[j] int j = n - 1; while (nums[j] <= nums[i]) { j--; } // 交换 nums[i] 和 nums[j] swap(nums[i], nums[j]); } // 第三步:反转 [i+1, n-1](如果上面跳过第二步,此时 i = -1) reverse(nums.begin() + i + 1, nums.end()); } };
查找第 K 小的排列
-
显然,先对 nums 排列,当在回溯过程中添加到第 k 个结果时,就正好是第 k 小的排列
-
实际上,不要拘泥于回溯的思路,直接生成所有全排列再取第
k个的方法(时间复杂度 O(n!))效率极低,尤其是当n较大时(比如n=10,全排列有 3628800 个) -
更优的思路是数学推导 + 逐步确定每一位数字:
- 阶乘分组:对于第 1 位数字,每选择一个数字,后续的
n-1位有(n-1)!种排列(比如n=3时,第 1 位选 1,后续有2! = 2种排列:[1,2,3]、[1,3,2]); - 确定每一位:通过
k除以阶乘值,确定当前位选哪个数字,再更新k为余数,继续确定下一位; - 动态维护可选数字列表:每确定一位数字后,从可选列表中移除该数字,避免重复选择
- 阶乘分组:对于第 1 位数字,每选择一个数字,后续的
-
代码实现
#include <iostream> #include <vector> #include <string> using namespace std; class Solution { public: string getPermutation(int n, int k) { // 1. 预计算阶乘:fact[i] = i!,用于快速计算分组数 vector<int> fact(n); fact[0] = 1; // 0! = 1 for (int i = 1; i < n; ++i) { fact[i] = fact[i-1] * i; } // 2. 初始化可选数字列表(1~n) vector<int> nums; for (int i = 1; i <= n; ++i) { nums.push_back(i); } // 3. 转换k为0索引(方便数组操作) k--; string result; // 4. 逐步确定每一位数字 for (int i = n; i >= 1; --i) { // 计算当前位的分组索引:k / (i-1)! int index = k / fact[i-1]; // 将选中的数字加入结果 result += to_string(nums[index]); // 从可选列表中移除该数字 nums.erase(nums.begin() + index); // 更新k为余数:k % (i-1)! k = k % fact[i-1]; } return result; } }; // 测试示例 int main() { Solution s; // 测试用例1:n=3, k=3 → 预期输出 "213" cout << "n=3, k=3 的第k个排列:" << s.getPermutation(3, 3) << endl; // 测试用例2:n=4, k=9 → 预期输出 "2314" cout << "n=4, k=9 的第k个排列:" << s.getPermutation(4, 9) << endl; // 测试用例3:n=1, k=1 → 预期输出 "1" cout << "n=1, k=1 的第k个排列:" << s.getPermutation(1, 1) << endl; return 0; }
求 nums 是字典序第几小的排列
-
这个问题是 “求第 k 小排列” 的逆问题,核心思路依然是阶乘分组计算,但方向相反:逐位计算当前位数字比它小的可选数字数量,累加这些数字对应的排列数,最终得到该排列的字典序位次
-
阶乘预计算:先计算
0!到(n-1)!,用于快速计算 “剩余位数的排列数”; -
逐位统计:从第一位到最后一位,对每一位数字:
- 找出当前可选数字中比当前位数字小的数字个数
cnt;
- 找出当前可选数字中比当前位数字小的数字个数
- 累加
cnt * (剩余位数-1)!(每个更小的数字作为当前位时,后续有(剩余位数-1)!种排列,都比当前排列小);- 从可选数字列表中移除当前位数字,继续处理下一位;
- 结果修正:最终累加的结果加 1(因为位次从 1 开始计数)
-
-
代码实现
#include <iostream> #include <vector> #include <algorithm> // 用于find using namespace std; class Solution { public: int getPermutationRank(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; // 1. 预计算阶乘:fact[i] = i!,fact[0]=0!, fact[1]=1!, ..., fact[n-1]=(n-1)! vector<long long> fact(n); // 用long long避免溢出(n>=20时阶乘会超int) fact[0] = 1; for (int i = 1; i < n; ++i) { fact[i] = fact[i-1] * i; } // 2. 初始化可选数字列表(1~n) vector<int> candidates; for (int i = 1; i <= n; ++i) { candidates.push_back(i); } long long rank = 0; // 累计比当前排列小的排列数(从0开始) // 3. 逐位计算 for (int i = 0; i < n; ++i) { int current = nums[i]; // 找到当前数字在可选列表中的索引 auto it = find(candidates.begin(), candidates.end(), current); int idx = it - candidates.begin(); // 累加:比current小的数字个数 * 剩余位数的阶乘 rank += idx * fact[n - 1 - i]; // 从可选列表中移除当前数字 candidates.erase(it); } // 4. 位次从1开始,所以最终结果+1 return rank + 1; } }; // 测试示例 int main() { Solution s; // 测试用例1:nums = [2,1,3] → 预期输出 3 vector<int> nums1 = {2,1,3}; cout << "排列 [2,1,3] 的字典序位次:" << s.getPermutationRank(nums1) << endl; // 测试用例2:nums = [2,3,1,4] → 预期输出 9 vector<int> nums2 = {2,3,1,4}; cout << "排列 [2,3,1,4] 的字典序位次:" << s.getPermutationRank(nums2) << endl; // 测试用例3:nums = [1] → 预期输出 1 vector<int> nums3 = {1}; cout << "排列 [1] 的字典序位次:" << s.getPermutationRank(nums3) << endl; // 测试用例4:nums = [3,2,1] → 预期输出 6(n=3时全排列共6个,这是最后一个) vector<int> nums4 = {3,2,1}; cout << "排列 [3,2,1] 的字典序位次:" << s.getPermutationRank(nums4) << endl; return 0; }
给定 nums,如何生成下下个排列?如何生成下 k 个排列
-
理论上,可以调用 k 次生成下一个排列的算法,但是效率确实不高 —— 因为每次生成下一个排列都要从头遍历数组,存在大量重复操作
-
核心改进逻辑是:
- 先计算当前排列的字典序位次 rank(从 1 开始);
- 计算目标位次
target_rank = rank + k; - 直接根据
target_rank生成对应位次的排列(而非逐次生成); - 若
target_rank > n!(超出全排列总数),则返回空
-
时间复杂度是 ,但是与 k 无关,即使 k 很大,效率也不会下降
-
代码实现
#include <iostream> #include <vector> #include <algorithm> // 仅用于find(可手动实现,见备注) using namespace std; // --------------- 工具函数:阶乘计算 + 防溢出 --------------- long long factorial(int n) { long long res = 1; for (int i = 2; i <= n; ++i) { res *= i; // 防溢出:n>=20时阶乘远超long long,实际场景n<=12即可 if (res > 1e18) break; } return res; } // --------------- 步骤1:计算当前排列的字典序位次 --------------- long long getPermutationRank(const vector<int>& nums) { int n = nums.size(); long long rank = 0; vector<int> candidates; for (int i = 1; i <= n; ++i) candidates.push_back(i); for (int i = 0; i < n; ++i) { int curr = nums[i]; // 找curr在candidates中的索引(可手动实现find,见备注) auto it = find(candidates.begin(), candidates.end(), curr); int idx = it - candidates.begin(); // 累加:比curr小的数字数 * 剩余位数的阶乘 rank += idx * factorial(n - 1 - i); // 移除已选数字 candidates.erase(it); } return rank + 1; // 位次从1开始 } // --------------- 步骤2:根据目标位次生成排列 --------------- vector<int> getPermutationByRank(int n, long long target_rank) { long long total = factorial(n); if (target_rank < 1 || target_rank > total) return {}; // 超出范围 target_rank--; // 转0索引 vector<int> candidates; for (int i = 1; i <= n; ++i) candidates.push_back(i); vector<int> res; for (int i = n; i >= 1; --i) { long long f = factorial(i - 1); int idx = target_rank / f; // 计算当前位的索引 res.push_back(candidates[idx]); candidates.erase(candidates.begin() + idx); // 移除已选数字 target_rank = target_rank % f; // 更新剩余位次 } return res; } // --------------- 核心函数:生成下k个排列 --------------- vector<int> getNextKPermutation(const vector<int>& nums, int k) { int n = nums.size(); long long total = factorial(n); long long curr_rank = getPermutationRank(nums); long long target_rank = curr_rank + k; // 检查是否超出全排列总数 if (target_rank > total) return {}; // 直接生成目标位次的排列 return getPermutationByRank(n, target_rank); } // 辅助:打印排列 void printPermutation(const vector<int>& perm) { if (perm.empty()) { cout << "不存在对应的后续排列" << endl; return; } cout << "["; for (int i = 0; i < perm.size(); ++i) { cout << perm[i]; if (i != perm.size() - 1) cout << ", "; } cout << "]" << endl; }