全排列

题目

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

示例:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

题目是比较容易理解的,不同于数学题有多少个排列组合的问答,在算法题上解题,用最直接的穷举也不是能全部列出来,解题时没有想到有效的解决之道。

学习

回溯算法

将穷举的过程实现出来,假设有 n 个空,每个位置都把所有数字试一遍,按照从左往右的顺序,填过的数字不能有重复。

  • 定义递归函数 backtrack(first, output)
    first 表示从左往右填的位置,当前排列为 output。
  • 结果可分为两种情况
  1. first == n,结束
    下标从 0 开始,已经填了 n 个位置,找到了一个可行的解,我们将output 复制到返回数组中,递归结束。
  2. first < n,递归过程
    需要确定在该位置填的数字。由于不能重复填已经填过的数字,这里可以定义一个标记数组,用来标记已经填过的数,作用是,当遍历给定的 n 个数字,如果某个数字没有被标记过,就尝试填入,并将其标记,继续尝试填下一个位置,即调用函数 backtrack(first + 1, output)。
    搜索回溯的时候要撤销这一个位置填的数以及标记,并继续尝试其他没被标记过的数。1

理解
(图转自https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

  • 为降低空间复杂度,标记数组可以取代,通过动态维护数组,即左边表示已填过的数字,右边表示未填过的数字
    • 假设目前是在first位置,nums[0,first−1] 则代表是已填数字集合,nums[first,n−1] 是代填数字集合;

    • 假设待填数字下标为 i ,填完后将该位置的数字与在first位置上的数字交换;

    • 当填下一个位置数字时 nums[0,first]表示的是已填过数字,nums[first+1,n−1] 表示待填的数,回溯的时候需要交换回来完成撤销操作。

    • 例如,假设我们有 nums[1,2,3,4] ,在填第三个位置时,已经填了 [3,4] 两个数,此时nums状态为 [3,4,1,2] 。假设这个位置填 2 ,为了维护数组,我们将 2 和 1 交换,变为nums[3,4,2,1]。

提交代码

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def backtrack(first = 0):
            # 所有数都填完了
            if first == len(nums):  
                s.append(nums[:])
            for i in range(first, len(nums)):
                # 动态维护数组,原地调整数组,降低空间复杂度
                nums[first], nums[i] = nums[i], nums[first]
                # 继续递归填下一个数
                backtrack(first + 1)
                # 撤销操作
                nums[first], nums[i] = nums[i], nums[first]
        
        s = []
        backtrack()
        return s
  • 时间复杂度 O(n * n!),其中 n 为序列的长度。

    • backtrack 的调用次数为 n 的 k - 排列,或者部分排列,由公式可知 backtrack 的调用次数的时间复杂度是 O(n!) ;
    • backtrack 调用的每个叶结点共 n! 个,需要将当前答案使用 O(n)的时间复制到返回数组中,相乘得时间复杂度为 O(n * n!)。
  • 空间复杂度 O(n),其中 n 为序列的长度。
    除去返回数组,递归过程每一层递归函数都需要分配栈空间,所以这里需要额外空间且取决于递归的深度,递归调用深度为 O(n)。

学习总结

  1. 递归函数的练习;
  2. 树结构的分析;
  3. 回溯算法的学习应用。

  1. 个人理解是方便搜索全部解,不影响后续的标记过程 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值