全排列的定义
对于包含 n 个不同元素的集合,全排列是将这 n 个元素按顺序排列成序列的所有可能方式,且每个元素在序列中只出现一次。
一、全排列的实现方法
1. 回溯法(递归实现)
通过 “选择 - 递归 - 回溯” 的逻辑生成所有排列,是最常用的方法。
- 核心思路: 每次从未选元素中选一个放入当前排列,递归处理剩余元素,最后回溯撤销选择。
-
class Solution { public: vector<vector<int>> permuteUnique(vector<int>& nums) { ranges::sort(nums); // 对数组排序,使相同元素相邻 int n = nums.size(); vector<vector<int>> ans; // 存储最终结果 vector<int> path(n); // 当前排列路径 vector<int> on_path(n); // 标记数组,记录哪些元素已被选中 // 递归函数,i 表示当前要填充排列的第几个位置 auto dfs = [&](this auto&& dfs, int i) -> void { if (i == n) { // 所有位置都已填充完毕 ans.push_back(path); // 将当前排列加入结果集 return; } // 遍历所有可能的元素 for (int j = 0; j < n; j++) { // 剪枝条件: // 1. 如果元素 j 已被选中 // 2. 或者元素 j 与前一个元素相同且前一个元素未被选中 if (on_path[j] || j > 0 && nums[j] == nums[j - 1] && !on_path[j - 1]) { continue; } path[i] = nums[j]; // 选择元素 j 填充当前位置 on_path[j] = true; // 标记元素 j 已被选中 dfs(i + 1); // 递归填充下一个位置 on_path[j] = false; // 回溯:撤销选择,允许后续选择其他元素 // path 无需恢复,因为后续递归会直接覆盖当前位置的值 } }; dfs(0); // 从位置 0 开始填充 return ans; } };
2. 交换法(回溯变种)
- 核心步骤:
- 固定位置,递归剩余元素:
从第一个位置开始,每次选择一个未被使用的元素放到当前位置,然后递归处理剩余元素。 - 去重策略:
- 对数组排序,确保相同元素相邻。
- 使用哈希集合
seen
记录当前位置已经使用过的元素值,避免重复选择相同值的元素。
- 关键操作:
通过交换元素来固定当前位置,递归后无需回溯(因为传入的是数组副本)。
- 固定位置,递归剩余元素:
-
class Solution { public: vector<vector<int>> permuteUnique(vector<int>& nums) { vector<vector<int>> result; sort(nums.begin(), nums.end()); // 排序以方便跳过重复元素 backtrack(nums, 0, result); return result; } private: void backtrack(vector<int> nums, int start, vector<vector<int>>& result) { if (start == nums.size()) { result.push_back(nums); return; } unordered_set<int> seen; // 记录已经作为当前位置首元素的数值 for (int i = start; i < nums.size(); ++i) { // 跳过已经在当前位置使用过的相同数值 if (seen.find(nums[i]) != seen.end()) continue; seen.insert(nums[i]); swap(nums[start], nums[i]); // 将选择的元素交换到当前位置 backtrack(nums, start + 1, result); // 递归处理剩余元素 // 不需要回溯交换,因为我们每次递归都传入了 nums 的副本 } } };
3. 字典序法(迭代实现)
通过生成下一个字典序排列的规则迭代生成所有排列,无需递归。
- 核心步骤:
- 找到最长的递增后缀(从后往前);
- 找到后缀前一个元素的下一个更大元素;
- 交换这两个元素,并反转后缀。
-
bool nextPermutation(vector<int>& nums) { int i = nums.size() - 2; while (i >= 0 && nums[i] >= nums[i + 1]) i--; // 找到递减后缀前的元素 if (i < 0) return false; // 已是最大排列 int j = nums.size() - 1; while (j > i && nums[j] <= nums[i]) j--; // 找到下一个更大元素 swap(nums[i], nums[j]); reverse(nums.begin() + i + 1, nums.end()); // 反转后缀 return true; }
二、应用场景
- 组合优化问题:如旅行商问题(TSP)中枚举所有路径。
- 密码学与随机数生成:生成所有可能的密钥组合。
- 数据排列与排序算法:如快速排序的随机化版本。
- 游戏与博弈论:如八数码问题中枚举所有状态。
- 算法测试:用于生成测试用例,验证算法正确性