leetcode46: Permutations 全排列解析及时间复杂度分析

本文详细解析了使用递归解决全排列问题的方法,通过分解问题规模,将大问题转化为小问题来逐步求解,并提供了具体的Java代码实现及时间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

思路

递归解决问题分3步:

  1. 大问题==》小问题

    如果我们确定了最后一位数据,那就变成了求解剩下 k−1个数据的排列问题。
    而最后一位数据可以是 k 个数据中的任意一个,因此它的取值就有 k 种情况。
    所以,“k 个数据的排列”问题,就可以分解成 k 个“n−1个数据的排列”的子问题。

  2. 递推公式
    比如[1,2,3]
    perm(1,2,3) = perm(2,3) 1 + perm(1,3) 2 + 3 perm(1,2) 3

  3. 终止条件
    终止条件 当子数组长度为1的时候 输出当前数组的值

这个递归代码的一个关键的思维节点是:不管递归到哪一层,都是在执行递归函数这同一份代码,不同的层只有一些状态数据不同而已,在本问题中,其状态数据是这个nums数组,这个nums数组是在内存中只有一个,但是在递归过程中,不同的层其状态不同。

代码实现

public class Solution {

    public List<List<Integer>> permute(int[] nums) {

        List<List<Integer>> res = new ArrayList<>();
        doPermute(nums, nums.length, nums.length, res);
        return res;
    }
    
    /**
     *
     * @param data数组
     * @param n data.length
     * @param k k个 k-1个数据的排列问题 缩小范围
     * @param res
     */
    private void doPermute(int[] data, int n, int k, List<List<Integer>> res) {

        // 终止条件 当k=1的时候 输出当前数组的值
        if (k == 1) {
            List<Integer> list = new ArrayList<>();
            // loop invariant:
            for (int i = 0; i < n; i++) {
                list.add(data[i]);
            }
            res.add(list);
            return;
        }

        // loop invariant: 每次变换数组最后的值 构成递推公式
        for (int i = 0; i < k; i++) {
            // 将子组数中的所有的数分别与第一个数交换,这样就总是在处理后n-1个数的全排列。

            // 比如[1,2,3]
            // perm(1,2,3) = perm(2,3) 1 + perm(1,3) 2 + 3 perm(1,2) 3



            // i=0 就是值1 k=3, k-1=2  将0和2交换 之后执行doPermute(k-1)

            // 内层循环
            // i=0 就是值3 k=2, k-1=1  将0和1交换 之后执行doPermute(k-1) 整体相当于执行 perm(2) 3 1
            // 之后再把0和1交换回来 3 2 1

            // i=1 就是值2 k=2, k-1=1  将1和1交换 之后执行doPermute(k-1) 整体相当于执行 perm(3) 2 1
            // 之后再把0和1交换回来 3 2 1

            // 回到外层循环 之后再把0和2交换回来 [1,2,3]  整体相当于执行 perm(3,2) 1





            // i=1 就是值2 k=3, k-1=2  将1和2交换 之后执行doPermute(k-1)
            //
            // 回到外层循环 之后再把1和2交换回来 [1,2,3]  整体相当于执行 perm(1,3) 2




            // i=2 就是值3 k=3, k-1=2  将2和2交换 之后执行doPermute(k-1)
            //
            // 回到外层循环 之后再把2和2交换回来 [1,2,3]  整体相当于执行 perm(1,2) 3

			// 将子组数中的所有的数分别与第一个数交换,这样就总是在处理后k-1个数的全排列
            swap(data, i, k - 1);
            // 这里直接认为下一层返回正确
            doPermute(data, n, k - 1, res);
            // 需要将nums数组复原
            swap(data, i, k-1);
        }

    }

    private void swap(int[] data, int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

}

时间复杂度分析

采用递归树的方式
在这里插入图片描述
第一层分解有 n 次交换操作,

第二层有 n 个节点,每个节点分解需要 n−1 次交换,所以第二层总的交换次数是 n∗(n−1)。

第三层有 n∗(n−1) 个节点,每个节点分解需要 n−2 次交换,所以第三层总的交换次数是 n∗(n−1)∗(n−2)。

以此类推,第 k 层总的交换次数就是 n∗(n−1)∗(n−2)∗…∗(n−k+1)。

最后一层的交换次数就是 n∗(n−1)∗(n−2)∗…∗2∗1。每一层的交换次数之和就是总的交换次数。

这个公式的求和比较复杂,我们看最后一个数,n∗(n−1)∗(n−2)∗…∗2∗1 等于 n!,而前面的 n−1 个数都小于最后一个数,所以,总和肯定小于 n∗n!,也就是说,全排列的递归算法的时间复杂度大于 O(n!),小于 O(n∗n!),虽然我们没法知道非常精确的时间复杂度,但是这样一个范围已经让我们知道,全排列的时间复杂度是非常高的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值