DFS:带重复项的全排列,程序运行全流程解析

1. 问题描述

给出一组可能包含重复项的数字,返回该组数字的所有排列,结果以字典序 升序 排列。

示例:

输入:[1,1,2]

返回值:[[1,1,2],[1,2,1],[2,1,1]]

2. 核心逻辑

在处理 [1, 1, 2] 时,由于含有两个 1,如果套用最基础的 DFS 模板,就会产生大量冗余的重复排列。

例如:

  • 当我们以第一个 1 为开头,可以得到 [1, 1, 2][1, 2, 1]
  • 当我们以第二个 1 为开头,又会得到 [1, 1, 2][1, 2, 1]

这样得到的结果就含有重复项。

要想去除掉重复项,需要按照下面的逻辑进行处理:

  • 首先将数组 升序 排列,让相同的数字放在一起。
  • 然后,在递归树的同一层,如果 当前的数字和上一个数字相同且上一个数字已经尝试过了,那么当前这个数字就不应该再作为开头去尝试。

3. 完整C++代码实现

cpp 复制代码
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param num int整型vector 
     * @return int整型vector<vector<>>
     */
    vector<vector<int>> res;
    bool used[8] = {false};
    vector<int> path;

    void dfs(vector<int> &num)
    {
        if(path.size() == num.size())
        {
            res.push_back(path);
            return;
        }

        for(int i=0; i<num.size(); i++)
        {
            if(used[i]) continue;

            //注意这里i>0很关键,我们肯定不会在第一次循环就进行剪枝
            if(i>0 && num[i] == num[i-1] && !used[i-1]) continue;

            used[i] = true;
            path.push_back(num[i]);

            dfs(num);

            path.pop_back();
            used[i] = false;
        }
    }

    vector<vector<int> > permuteUnique(vector<int>& num) {
        //排序是剪枝的前提
        sort(num.begin(),num.end());
        dfs(num);
        return res;
    }
};

4. 程序运行全过程

假设输入数组已排序为 nums = [1, 1, 2],我们来分析一下 DFS 的完整流程,大家可以对照上面的代码看下面的流程分析:

从根节点出发:

  1. 选择 nums[0] ,此时 used = [T, F, F]push_back 之后 path = [1]
  2. 进入第二层递归:
    • 由于 used[0]true,所以会直接跳过第一轮 for 循环,选择第二轮 for 循环的 nums[1] ,此时 used = [T, T, F]push_back 之后 path = [1, 1]
    • 然后进入第三层递归:
      • 前两个 used 都为 true ,选择 nums[2],此时 used = [T, T, T]push_back 之后 path = [1, 1, 2]这是第一个结果
      • 在尝试进行第四次递归时,此时 已经满足path.size() == num.size(),于是进行 回溯
      • 回到第三层递归,执行 pop_back 并把 used[2] 置位 false,当前 used = [T, T, F]path = [1, 1]
      • 此时,for 循环达到边界,跳出循环,本次 dfs 调用结束,进行回溯。
    • 回溯到第二层,执行 pop_back 并把 used[1] 置位 false,当前 used = [T, F, F]path = [1]
    • 然后进行第二层的下一轮 for 循环,此时选择 nums[2]path = [1, 2]
      • 然后再进入第三次递归,选择 nums[1],此时,path = [1, 2, 1]
      • 得到了第二个结果 [1, 2, 1],然后进行回溯。
      • 此时,回溯到第二层之后,第二层递归的 for 循环也结束了,继续回溯。

程序又回到了根节点:

  • 撤销了 num[0],此时 used = [F, F, F]path = [],进入根节点的第二轮 for 循环。
  • 准备选择第二个 1 时,触发判断条件 nums[1] == nums[0]used[0] == false,这意味着 nums[0] 刚刚作为开头已经完整试过了所有可能性,并退回了。
  • 这时执行剪枝 ,用 continue 直接跳过第二个 1 。

收尾:

  • 循环进入 i = 2,选择 2 作为开头,此时 path = [2]
  • 后续逻辑同上,最终得到 [2, 1, 1]
相关推荐
01二进制代码漫游日记3 分钟前
C++之类和对象(上)02
c++
wzdark5 分钟前
数据压缩算法的时间复杂度与压缩率权衡4
算法
小陈phd23 分钟前
深入理解agent学习笔记(一)——现代agent介绍
人工智能·算法
渡我白衣24 分钟前
HttpRequest与HttpResponse的实现
服务器·数据结构·c++·人工智能·tcp/ip·机器学习·caffe
刃神太酷啦25 分钟前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html
蒸蒸yyyyzwd28 分钟前
八股学习笔记 day42
c++·八股
凤年徐38 分钟前
C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现
linux·c++
晴天的雨.9923 小时前
【C++算法】和为s的两个数
开发语言·数据结构·c++·算法
aramae10 小时前
MySQL复合查询(8)
java·c语言·开发语言·后端·算法
荆棘鸟智能11 小时前
城市感知设备怎么统一接入?从多协议网关到设备模型的中间件架构设计
人工智能·算法·边缘计算