这是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;)。我对空间复杂性的分析是否正确?
答案 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
调用其他方法,无论是method2
,method1
递归,System.out.println
还是其他方法,都会在其中创建一个新框架堆栈指针现在指向。该帧可以比以前的method2
帧更小,相等或更大。它将占用method2
帧所在的部分或全部内存。如果它是对method2
的另一次调用,那么使用相同或不同的参数调用它并不重要。这无关紧要,因为该程序不记得上次使用的参数。它只知道从堆栈指针开始的内存区域是空的并且可以使用。该计划不知道最近在那里生活的框架。那个框架消失了,走了,走了。
如果您可以遵循这一点,您可以看到在计算空间复杂性时以及仅查看堆栈使用的空间量时,唯一重要的是,任何一个堆栈上可以存在多少帧时间点?无论调用方法的参数是什么,过去可能存在但不再存在的帧与计算无关。
(P.S。如果有人计划指出我在这个或那个细节上的技术错误 - 我已经知道这是一个粗略的过度简化。)