【问题标题】:Runtime and space complexity of recursive and tail-recursive code递归和尾递归代码的运行时和空间复杂度
【发布时间】:2017-08-26 16:13:51
【问题描述】:

以下代码查找一组数字的所有子集的复杂度是多少?

public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> subsets = new ArrayList<>();
        subsets.add(new ArrayList<Integer>());
        return find(subsets, nums, 0);
    }

    private List<List<Integer>> find(List<List<Integer>> subsets, int[] nums, int index) {
        if (index == nums.length) {
            return subsets;
        }
        List<List<Integer>> newSubsets = new ArrayList<>();
        for (List<Integer> subset: subsets) {
            List<Integer> newSubset = new ArrayList<>();
            newSubset.addAll(subset);
            newSubset.add(nums[index]);
            newSubsets.add(newSubset);
        }
        subsets.addAll(newSubsets);
        return find(subsets, nums, index + 1);
    }

O(2^nums.length),因为这是子集的数量,您必须将每个子集添加到返回的列表中吗?另外,我是否认为下面的版本仍然是渐近的O(2^set.size()),但一般递归贡献的空间复杂度是O(set.size()),而在上面的尾递归代码中,它是O(1)

public static ArrayList<ArrayList<Integer>> getSubsets(ArrayList<Integer> set) {
    ArrayList<ArrayList<Integer>> subsets = new ArrayList<>();
    getSubsets(set, subsets, 0);
    return subsets;
}

private static void getSubsets(ArrayList<Integer> set, ArrayList<ArrayList<Integer>> subsets, int index) {
    if (index == set.size()) {
        subsets.add(new ArrayList<Integer>());
        return;
    }
    getSubsets(set, subsets, index + 1);
    int item = set.get(index);
    ArrayList<ArrayList<Integer>> moreSubsets = new ArrayList<>();
    for (ArrayList<Integer> subset: subsets) {
        ArrayList<Integer> newSubset = new ArrayList<>();
        newSubset.addAll(subset);
        newSubset.add(item);
        moreSubsets.add(newSubset);
    }
    subsets.addAll(moreSubsets);
}

【问题讨论】:

  • Java 不进行尾调用消除。
  • ... 但是现在您只需一步将递归转换为迭代。它会运行得稍微快一些,但是由于缺少方法调用,您不会节省太多内存。经过几次迭代后,子集将分配比堆栈帧多得多的内存。

标签: algorithm recursion big-o


【解决方案1】:

在我看来,这里的主要问题是数据空间的复杂性。如果原始数组的长度超过几十个,您的代码将抛出 OutOfMemoryError。再多一些元素以及地球上所有的内存芯片和硬盘驱动器都不足以存储这些子集。

通过将其优化为尾递归来节省几个字节并没有什么不同。

出于实际目的,以下代码非常有效。虽然总体上速度较慢,但​​它的数据空间复杂度为 O(N),并且它会即时计算子集,因此如果您只想处理某些子集,它甚至可以为您提供更好的结果。

private static List<Integer> getSubset(int[] nums, BigInteger n) {
    List<Integer> subset = new ArrayList<>();
    for (int i = 0; i < nums.length; i++) {
        if (n.testBit(i)) {
            subset.add(nums[i]);
        }
    }
    return subset;
}

您可以像这样访问子集:

public static void main(String... args) {
    int[] nums = { 1, 2, 3, 4 };
    for (BigInteger n = new BigInteger("2").pow(nums.length).subtract(BigInteger.ONE); n
            .compareTo(BigInteger.ZERO) >= 0; n = n.subtract(BigInteger.ONE)) {
        // process nth subset
        // each subset is identified by n where 0<=n<2^nums.lenght
        System.out.println(getSubset(nums, n));
    }
}

【讨论】:

    猜你喜欢
    • 2021-06-27
    • 1970-01-01
    • 2023-03-04
    • 1970-01-01
    • 2023-03-13
    • 2019-04-21
    • 2023-03-24
    • 1970-01-01
    • 2015-12-14
    相关资源
    最近更新 更多