全排列算法解析与实现

全排列的定义

对于包含 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. 交换法(回溯变种) 
  • 核心步骤
    1. 固定位置,递归剩余元素
      从第一个位置开始,每次选择一个未被使用的元素放到当前位置,然后递归处理剩余元素。
    2. 去重策略
      • 对数组排序,确保相同元素相邻。
      • 使用哈希集合 seen 记录当前位置已经使用过的元素值,避免重复选择相同值的元素。
    3. 关键操作
      通过交换元素来固定当前位置,递归后无需回溯(因为传入的是数组副本)。
  • 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. 字典序法(迭代实现)

通过生成下一个字典序排列的规则迭代生成所有排列,无需递归。

  • 核心步骤
    1. 找到最长的递增后缀(从后往前);
    2. 找到后缀前一个元素的下一个更大元素;
    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;
    }

二、应用场景

  1. 组合优化问题:如旅行商问题(TSP)中枚举所有路径。
  2. 密码学与随机数生成:生成所有可能的密钥组合。
  3. 数据排列与排序算法:如快速排序的随机化版本。
  4. 游戏与博弈论:如八数码问题中枚举所有状态。
  5. 算法测试:用于生成测试用例,验证算法正确性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值