LeetCode 刷题 [C++] 第46题.全排列(回溯法+深度优先遍历DFS)

题目描述

给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

在对全排列题目进行解答之前,先回顾和学习下递归回溯算法深度优先遍历算法。

递归:递归的实质就是借用栈实现一些操作,利用递归能够实现的操作使用栈也能实现,并且直接利用栈的话可以很好的控制停止,效率更高(通常使用迭代法)。这主要是因为递归是一个有“来回”过程,回来的时候需要特殊判断(结束条件)。
为什么会使用递归:递归思路对程序员来说更加简洁巧妙,并且递归的思考方式更偏向于人的思考方式。在解决一些可以将父问题通过一定的关系转化为相同子问题的复杂问题时,使用递归思想能够简洁巧妙的解决。

回溯法
维基百科的解释:采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
找到一个可能存在的正确的答案;
在尝试了所有可能的分步方法后宣告该问题没有答案。

通俗来讲:回溯法的核心为试探和复原。整个过程利用递归去执行,在递归函数执行前取修改尝试,满足条件后下沉到下一层,试探完成后将数值复原。在整个试探和复原的过程中找到最终需要的一个或所有解。

深度优先遍历
维基百科的解释:(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深的搜索树的分支。当结点v的所在边都己被探寻过,搜索将回溯到发现结点v的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

从上面的解释能够看出,回溯算法其实是一种特殊的深度优先遍历算法。之所以叫回溯,主要是因为回溯利用一个不断变化的变量,通过尝试各种可能的过程来搜索需要的结果,强调了回退操作对于搜索的合理性。而深度优先遍历强调的是遍历的思想。

直接看维基百科的解释会感觉很抽象,因此,需要通过做题来理解回溯算法与深度优先遍历的联系。

解题思路
一、首先列出全排列问题的搜索结果过程,数组 [1, 2, 3] 的全排列:

  1. 先写以1开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列;
  2. 再写以2开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;
  3. 最后写以3开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。

过程总结:按顺序枚举每一位可能出现的情况,已经选择的数字在当前要选择的数字中不能出现。按照这种策略搜索就能够做到不重不漏。这样的思路,可以用一个树形结构表示。
二、解题过程
在此建议画出全排列问题的树形结构,然后根据树形结构设计状态变量:

  1. 首先这棵树除了根结点和叶子结点以外,中间层的每个结点做着同样的事情,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个递归结构;
  2. 递归的终止条件是:一个排列中的数字已经选够了;
  3. 布尔数组eleSta,初始化的时候都为true表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为false,这样在考虑下一个位置的时候,就能够以O(1)的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。
    这些变量称为“状态变量”,它们表示了在求解一个问题的时候所处的阶段。需要根据问题的场景设计合适的状态变量。

具体实现代码如下

class Solution {
private:
     vector<vector<int>> res;
public:
    void backtrack(vector<int> &nums,int len,vector<int> &current, vector<bool>&eleSta) {
	    if (current.size() == eleSta.size())
		    res.emplace_back(current);
	    else {
		    for (int i = 0; i < len; ++i) {
			    if (eleSta[i]) {
				    current.emplace_back(nums[i]);
				    eleSta[i] = false;
				    backtrack(nums, len, current, eleSta);
				    eleSta[i] = true;
				    current.pop_back();
			    }
		    }
	    }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        int len = nums.size();
        if(len > 1) {
            vector<bool> eleSta(len,true);
    		vector<int> current;
    		backtrack(nums, len, current, eleSta);
    		return res;
        } 
        return { nums };
    }
};

复杂度分析

  1. 时间复杂度:每个内部结点循环N次,故非叶子结点的时间复杂度为 O(N×N!);叶节点共N!个,在叶子结点处拷贝需要 O(N),叶子结点的时间复杂度也为 O(N×N!)。因此,时间复杂度为O(N×N!)。
  2. 空间复杂度:全排列个数 N!,每个全排列占空间 N。空间复杂度为O(N×N!)。

AC结果
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值