我对空间复杂性的分析是否正确?

时间:2015-03-24 04:01:20

标签: java algorithm recursion time-complexity space-complexity

这是Cracking the Coding Interview 5 th edition

的问题9.5

问题:编写一种方法来计算字符串的所有排列

这是我的解决方案,用Java编码(测试它,它可以工作:))

public static void generatePerm(String s) {
    Queue<Character> poss = new LinkedList<Character>();
    int len = s.length();
    for(int count=0;count<len;count++)
        poss.add(s.charAt(count));
    generateRecurse(poss, len, "");
}
private static void generateRecurse(Queue<Character> possibles, int n, String word) {
    if(n==0)
        System.out.println(word);
    else {
        for(int count=0;count<n;count++) {
            char first = possibles.remove();
            generateRecurse(possibles, n-1, word+first);
            possibles.add(first);
        }
    }
}

我同意作者的观点,我的解决方案在 O(n!)时间复杂度中运行,因为要解决这个问题,你必须考虑阶乘,比如像#34; top&#这样的词34;,第一个字母有三种可能性,第二个字母有2种,等等......

然而,她没有提到空间复杂性。我知道面试官喜欢问你解决方案的时间和空间复杂性。这个解决方案的空间复杂度是多少?

我的初始猜测是O(n 2 ),因为在每个级别n都有n个递归调用。所以你要加n + n - 1 + n - 2 + n - 3 ..... + 1来得到 n(n + 1) / 2 这是在O(n 2 )。我推断有n个递归调用,因为你必须在每个级别回溯n次,并且空间复杂性是你的算法所做的递归调用的次数。例如,当考虑&#34; TOP&#34;的所有排列时,在3级递归调用,gR([O,P],2,&#34; T&#34;),gR([P,制作T],2,&#34; O&#34;),gR([T,O],2,&#34; P&#34;)。我对空间复杂性的分析是否正确?

1 个答案:

答案 0 :(得分:8)

我认为你找到了正确答案,但出于错误的原因。递归调用的数量与它没有任何关系。当你进行递归调用时,它会向堆栈添加一定的空间;但是当该调用退出时,堆栈空间被释放。所以假设你有这样的东西:

void method(int n) {
    if (n == 1) {
        for (int i = 0; i < 10000; i++) {
            method(0);
        }
    }
}

method(1);

虽然method自称10000次,但任何时候堆栈上的method调用次数仍不超过2次。因此空间复杂度为O(1)[常数]。

您的算法具有空间复杂度O(n 2 )的原因是word字符串。当n降为0时,len的调用将占用generateRecurse个堆栈条目。最多会有len个堆栈条目,因此堆栈的空间使用量只有O(n);但是每个堆栈条目都有自己的word,它们将同时存在于堆上;这些word参数的长度为1,2,...,len,当然加起来为(len * (len+1)) / 2,这意味着空间用法为O(n 2 )。

关于堆叠框架的更多信息:看来堆栈框架基础知识的解释会有所帮助......

A&#34;堆叠框架&#34;只是一个记忆区域,是&#34;堆栈的一部分&#34;。通常,堆栈是预定义的存储区域;但是,堆栈帧的位置和大小不是预定义的。当一个程序第一次执行时,堆栈中就不会有任何东西(实际上,那里可能会有一些初始数据,但是让我们说没有什么,只是为了让事情变得简单) 。所以内存的堆栈区域如下所示:

bottom of stack                                       top of stack
------------------------------------------------------------------
|                      nothing                                   |
------------------------------------------------------------------
^
+--- stack pointer

(这假设堆栈从较低地址向较高地址增长。许多机器都有向下增长的堆栈。为了简化,我将继续假设这是堆栈向上增长的机器。)

当调用方法(函数,过程,子例程等)时,分配堆栈的某个区域。该区域足以容纳方法的局部变量(或对它们的引用),参数(或对它们的引用),一些数据,以便程序在return时知道返回的位置,以及可能还有其他信息 - 其他信息高度依赖于机器,编程语言和编译器。在Java中,第一种方法是main

bottom of stack                                       top of stack
------------------------------------------------------------------
| main's frame |                  nothing                        |
------------------------------------------------------------------
                ^
                +--- stack pointer

请注意,堆栈指针已向上移动。现在main调用method1。由于method1将返回main,因此当main恢复执行时,必须保留main的局部变量和参数。在堆栈上分配一些大小的新帧:

bottom of stack                                       top of stack
------------------------------------------------------------------
| main's frame | method1's frame |      nothing                  |
------------------------------------------------------------------
                                  ^
                                  +--- stack pointer

然后method1调用method2

bottom of stack                                       top of stack
------------------------------------------------------------------
| main's frame | method1's frame | method2's frame |   nothing   |
------------------------------------------------------------------
                                                    ^
                                                    +--- stack pointer

现在method2返回。 method2返回后,将无法再访问其参数和局部变量。因此,可以抛出整个框架。这是通过将堆栈指针移回到之前的位置来完成的。 (&#34;先前的堆栈指针&#34;是某些帧中保存的东西之一。)堆栈返回到如下所示:

bottom of stack                                       top of stack
------------------------------------------------------------------
| main's frame | method1's frame |        nothing                |
------------------------------------------------------------------
                                  ^
                                  +--- stack pointer

这意味着,此时,机器将看到以堆栈指针开头的堆栈部分为&#34;未使用&#34;。谈到method2的框架被重用是不正确的。你无法真正使用已经不存在的东西,并且method2的框架不再存在。从概念上讲,所有存在的堆栈都是一个很大的空白区域。如果method1调用其他方法,无论是method2method1递归,System.out.println还是其他方法,都会在其中创建一个新框架堆栈指针现在指向。该帧可以比以前的method2帧更小,相等或更大。它将占用method2帧所在的部分或全部内存。如果它是对method2的另一次调用,那么使用相同或不同的参数调用它并不重要。这无关紧要,因为该程序不记得上次使用的参数。它只知道从堆栈指针开始的内存区域是空的并且可以使用。该计划不知道最近在那里生活的框架。那个框架消失了,走了,走了。

如果您可以遵循这一点,您可以看到在计算空间复杂性时以及仅查看堆栈使用的空间量时,唯一重要的是,任何一个堆栈上可以存在多少帧时间点?无论调用方法的参数是什么,过去可能存在但不再存在的帧与计算无关。

(P.S。如果有人计划指出我在这个或那个细节上的技术错误 - 我已经知道这是一个粗略的过度简化。)