【问题标题】:Is my analysis of space complexity correct?我对空间复杂度的分析是否正确?
【发布时间】:2015-05-27 07:16:40
【问题描述】:

这是Cracking the Coding Interview 5th 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!) 时间复杂度运行,因为要解决这个问题,您必须考虑阶乘,例如“top”这样的词,有第一个字母有 3 种可能,第二个字母有 2 种,依此类推....

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

我最初的猜测是 O(n2),因为在每个级别 n 都有 n 个递归调用。所以你会添加 n + n - 1 + n - 2 + n - 3.....+ 1 得到 n(n+1)2 这是在 O(n2) 中。我推断有 n 个递归调用,因为您必须在每个级别回溯 n 次,并且空间复杂度是您的算法进行的递归调用的数量。例如,当考虑“TOP”的所有排列时,在级别,3个递归调用,gR([O,P],2,"T"), gR([P,T],2,"O"), gR([T,O],2,"P") 已生成。我对空间复杂度的分析是否正确?

【问题讨论】:

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


    【解决方案1】:

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

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

    虽然method 调用自身10000 次,但在任何时候堆栈上仍然不会超过2 次method 调用。所以空间复杂度将是 O(1) [常数]。

    您的算法具有空间复杂度 O(n2) 的原因是 word 字符串。当n 降至0 时,将有len 堆栈条目被generateRecurse 的调用占用。最多会有len个栈入口,所以栈的空间使用量只会是O(n);但是这些堆栈条目中的每一个都有自己的word,它们都将同时存在于堆上;而那些word参数的长度是1、2、...、len,当然do加起来就是(len * (len+1)) / 2,也就是说空间使用量是O(n 2)。

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

    “堆栈帧”只是作为“堆栈”一部分的内存区域。通常,堆栈是预定义的内存区域;但是,堆栈帧的位置和大小不是预定义的。当一个程序第一次执行时,堆栈上不会有任何东西(实际上,那里可能会有一些初始数据,但假设什么都没有,只是为了让事情变得简单)。所以内存的栈区是这样的:

    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 返回后,其参数和局部变量将不再可访问。因此,可以将整个框架扔掉。这是通过将堆栈指针移回原来的位置来完成的。 (“前一个堆栈指针”是保存在某个帧中的东西之一。)堆栈回到看起来像这样:

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

    这意味着,此时,机器会将堆栈中以堆栈指针开头的部分视为“未使用”。说method2 的框架被重用是不正确的。你不能真正使用已经不复存在的东西,method2 的框架不再存在。从概念上讲,堆栈中只有一个很大的空白区域。如果method1 调用另一个方法,无论是method2method1 递归、System.out.println 还是其他方法,都会在堆栈指针现在指向的位置创建一个新帧。此框架的大小可以小于、等于或大于过去的 method2 框架。它将占用method2 帧所在的部分或全部内存。如果是对method2 的另一个调用,则使用相同或不同的参数调用它都没有关系。没关系,因为程序不记得上次使用了什么参数。它只知道以堆栈指针开头的内存区域是空的并且可以使用。该程序不知道最近住在那里的框架。那个框架不见了,不见了,不见了。

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

    (P.S. 万一有人打算指出我在这个或那个细节上的技术错误——我已经知道这过于简单化了。)

    【讨论】:

    • 所以对方法(0)的 10,000 次调用将一遍又一遍地重复使用相同的堆栈帧?
    • 你为什么要把所有的长度加起来?以“TOP”示例为例,第一个函数调用将是 generateRecurse([O,P], 2, "T")。然后下一个将是 generateRecurse([P], 1, "TO")。最后,最后一个递归调用将是 generateRecurse([],0,"TOP")。我同意这里有 len 堆栈帧。但是我不明白你为什么添加每个单词的长度。每个单词只是一个局部变量。我应该纠正自己。空间复杂度与递归调用的深度有关。在这种情况下,递归调用的深度是 len,所以空间复杂度不应该是 O(n) 吗?
    • 回答你的两个cmets:(1)是的。 (2)我添加了单词的长度,因为它们都同时存在于内存中。他们必须。当最后一个递归调用退出时,它会返回到第二个调用,其中word"TO"。这意味着字符串"TO" 必须在最后一个递归调用运行时保持存在——程序在退出时无法凭空重建它。同样,字符串"T" 必须保持存在,而其他两个递归调用都在执行....
    • ...希望您可以看到,在堆栈最深的时候,所有字符串都必须存在,占用内存,因此需要添加长度以确定如何占用了很多空间。
    • @committedandroider 请查看我的编辑。您的问题的答案是“是的,只是事情并非如此运作”。
    猜你喜欢
    • 2021-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-16
    • 2022-10-25
    • 1970-01-01
    • 1970-01-01
    • 2021-04-08
    相关资源
    最近更新 更多