针对这个高度抽象的代码,回答涉及复杂性的问题有一定难度。根据Java文档中的描述,append
函数的复杂度没有明确保证。正如其他人所指出的那样,应当(也可以)将StringBuffer
类编写为字符串添加的复杂度不依赖于StringBuffer
当前持有的字符串长度。
然而,我怀疑仅仅告诉提问者“你的书是错的!”并没有太多帮助。我们应该看看做了哪些假设,并明确作者想要表达的意思。
您可以做出以下假设:
- 创建一个
new StringBuffer
是 O(1)
- 获取下一个字符串
w
在 words
中是 O(1)
- 返回
sentence.toString
的复杂度最多为 O(n)。
问题实际上是关于 sentence.append(w)
的顺序,这取决于它在 StringBuffer
内部的实现方式。幼稚的方法是像Shlemiel the Painter一样进行操作。
愚蠢的方法
假设您使用 C 风格的空结束字符串来存储 StringBuffer 的内容。要查找这样一个字符串的末尾,您需要逐个读取每个字符,直到找到空字符 - 然后将新字符串 S 追加到 StringBuffer 字符串中,可以开始从 S 复制字符到 StringBuffer 字符串(以另一个空字符结尾) 。如果按照这种方式编写 append 函数,则其时间复杂度为 O(a+b) ,其中 a 是当前 StringBuffer 中的字符数,b 是新单词中的字符数。如果您循环遍历单词数组,并每次在追加新单词之前都必须读取您刚刚追加的所有字符,那么循环的复杂度就是 O(n^2),其中 n 是所有单词中的字符总数(也是最终句子中的字符数)。
更好的方法:
另一方面,假设 StringBuffer 的内容仍然是字符数组,但我们还存储了一个名为 size 的整数,它告诉我们字符串有多长(即字符数)。现在,我们不再需要读取 StringBuffer 中的每个字符以查找字符串的结尾;我们只需在数组中查找索引 size,其复杂度为O(1),而不是O(a)。然后,append 函数现在仅依赖于要附加的字符数,即O(b)。在这种情况下,循环的复杂度为 O(n),其中 n 是所有单词中的字符总数。
但是这还不够!
最后,还有一个实现方面的问题没有涉及到,那就是由教科书答案提出的内存分配问题。每次你想要往StringBuffer
中添加更多的字符时,不能保证字符数组中有足够的空间来容纳新的单词。如果没有足够的空间,你的计算机需要先在一个清洁的内存区域中分配一些额外的空间,然后将旧的StringBuffer
数组中的所有信息复制过去,然后才能像以前一样继续操作。像这样复制数据需要O(a)时间(其中a是要复制的字符数)。
在最坏的情况下,每次添加新单词时都必须分配更多的内存。这基本上把我们带回到了原点,使得循环的复杂度为O(n^2),这也是书中所暗示的。如果假设没有什么疯狂的事情发生(单词不以指数增长的速度变长!),那么通过让分配的内存呈指数增长的方式,你可以将内存分配的数量减少到类似于O(log(n))的数量级。如果那是内存分配的数量,并且内存分配总体上是O(a),那么循环中仅与内存管理有关的总复杂度就是O(n log(n))。由于添加操作的复杂度是O(n),并且小于内存管理的复杂度,因此该函数的总复杂度为O(n log(n))。
同样,Java文档在StringBuffer
容量增长的方式方面没有帮助我们,它只说“如果内部缓冲区溢出,则自动扩大”。根据扩大容量的方式,你可能会得到O(n^2)或O(n log(n))的总体复杂度。
作为留给读者的练习:找到一种简单的方法来修改该函数,使总体复杂度为O(n),消除内存重新分配问题。