这段简单代码的复杂性是什么?

时间:2011-08-23 04:01:01

标签: java complexity-theory big-o time-complexity stringbuffer

我正在从我的电子书中粘贴这个文本。它说O(n 2 )的复杂性,并给出了解释,但我没有看到如何。

问题:此代码的运行时间是多少?

public String makeSentence(String[] words) {
    StringBuffer sentence = new StringBuffer();
    for (String w : words) sentence.append(w);
    return sentence.toString();
}

这本书给出了答案:

  

O(n 2 ),其中n是句子中的字母数。这就是原因:每次你   将一个字符串附加到句子,你创建一个句子的副本,并贯穿所有的字母   复制它们的句子如果你必须每次迭代最多n个字符   循环,你至少循环n次,这给你一个O(n 2 )运行时间。哎哟!

有人可以更清楚地解释这个答案吗?

10 个答案:

答案 0 :(得分:23)

这似乎是一个误导的问题,因为我刚刚读了那本书。书中的这部分内容是拼写错误!以下是上下文:

=============================================== ====================

问题:此代码的运行时间是多少?

1 public String makeSentence(String[] words) {
2 StringBuffer sentence = new StringBuffer();
3 for (String w : words) sentence.append(w);
4 return sentence.toString();
5 }

答案:O(n 2 ),其中n是句子中的字母数。原因如下:每次将一个字符串附加到句子上时,您都会创建一个句子副本并遍历句子中的所有字母以将其复制。如果你必须在循环中每次迭代最多n个字符,并且你至少循环n次,那么就会给你一个O(n 2 )运行时。哎哟! 使用StringBuffer(或StringBuilder)可以帮助您避免此问题。

1 public String makeSentence(String[] words) {
2 StringBuffer sentence = new StringBuffer();
3 for (String w : words) sentence.append(w);
4 return sentence.toString();
5 }

=============================================== ======================

您是否注意到作者搞砸了?她提到的O(n 2 )解决方案(第一个)与“优化”解决方案(后者)完全相同。因此,我的结论是作者试图渲染其他内容,例如在附加每个下一个字符串时总是将旧句子复制到新缓冲区,作为O(n 2 )算法的示例。 StringBuffer不应该太傻,因为作者还提到'With StringBuffer(或StringBuilder)可以帮助你避免这个问题'。

答案 1 :(得分:17)

接受的答案是错的。 StringBuffer已分摊O(1)追加,因此 n 追加将为O( n )。

如果不是O(1)追加,StringBuffer没有理由存在,因为用普通String连接编写该循环将是O( n ^ 2)以及!

答案 2 :(得分:17)

在高级编写代码时,回答关于代码复杂性的问题有点困难,这会抽象出实现的细节。 Java documentation似乎没有对append函数的复杂性提供任何保证。正如其他人所指出的那样,StringBuffer类可以(并且应该)编写,以便附加字符串的复杂性不依赖于StringBuffer中保存的字符串的当前长度。

但是,我怀疑这个问这个问题的人只是简单地说“你的书错了!”。 - 相反,让我们看看正在做出什么样的假设,并明确作者试图说的是什么。

您可以做出以下假设:

  1. 创建new StringBuffer是O(1)
  2. w中获取下一个字符串words是O(1)
  3. 返回sentence.toString最多为O(n)。
  4. 问题实际上是sentence.append(w)的顺序是什么,这取决于它在StringBuffer内的发生方式。天真的方式是像Shlemiel the Painter那样做。

    愚蠢的方式

    假设您对StringBuffer的内容使用C样式的以null结尾的字符串。找到这样一个字符串结尾的方法是逐个读取每个字符,直到找到空字符 - 然后附加一个新字符串S,就可以开始将字符从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)。

答案 3 :(得分:11)

我尝试使用此程序检查它

public class Test {

    private static String[] create(int n) {
        String[] res = new String[n];
        for (int i = 0; i < n; i++) {
            res[i] = "abcdefghijklmnopqrst";
        }
        return res;
    }
    private static String makeSentence(String[] words) {
        StringBuffer sentence = new StringBuffer();
        for (String w : words) sentence.append(w);
        return sentence.toString();
    }


    public static void main(String[] args) {
        String[] ar = create(Integer.parseInt(args[0]));
        long begin = System.currentTimeMillis();
        String res = makeSentence(ar);
        System.out.println(System.currentTimeMillis() - begin);
    }
}

正如预期的那样,结果是O(n):

java Test 200000 - 128 ms

java Test 500000 - 370 ms

java Test 1000000 - 698 ms

版本1.6.0.21

答案 4 :(得分:11)

我认为书中的这些文字必须是拼写错误,我认为正确的内容如下,我解决了:

=============================================== ====================

问题:此代码的运行时间是多少?

public String makeSentence(String[] words) {
    String sentence = new String("");
    for (String w : words) sentence+=W;
    return sentence;
}

答案:O(n 2 ),其中n是句子中的字母数。原因如下:每次将一个字符串附加到句子上时,您都会创建一个句子副本并遍历句子中的所有字母以将其复制。如果你必须在循环中每次迭代最多n个字符,并且你至少循环n次,那么就会给你一个O(n 2 )运行时。哎哟!使用StringBuffer(或StringBuilder)可以帮助您避免此问题。

public String makeSentence(String[] words) {
    StringBuffer sentence = new StringBuffer();
    for (String w : words) sentence.append(w);
    return sentence.toString();
}

=============================================== ======================

我是对的吗?

答案 5 :(得分:2)

这实际上取决于StringBuffer的实施。假设.append()是恒定时间,很明显您在O(n)的时间内有n = length of the words array算法。如果.append 不是常量时间,则需要按方法的时间复杂度将O(n)加倍。如果StringBuffer的当前实现确实逐个字符地复制字符串,那么上面的算法是

Θ(n*m)O(n*m),其中n是字数,m是平均字长,而您的图书是错误的。我假设你正在寻找一个严格的约束。

本书的答案不正确的简单示例: String[] words = ['alphabet']根据本书的定义n=8,所以算法将受到64个步骤的限制。是这样的吗?显然不严格。我看到1个赋值和1个带n个字符的复制操作,所以你得到大约9个步骤。正如我在上面所说明的那样,这种行为是由O(n*m)的界限预测的。

我做了一些挖掘,这显然不是一个简单的角色副本。看起来内存正在被批量复制,这使我们回到O(n),这是您对解决方案的第一次猜测。

/* StringBuffer is just a proxy */
public AbstractStringBuilder append(String str) 
{
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}

/* java.lang.String */
void getChars(char dst[], int dstBegin) {
             System.arraycopy(value, offset, dst, dstBegin, count);
}

你的书要么旧又可怕,要么两者兼而有之。我没有足够的决心挖掘JDK版本以找到一个不太优化的StringBuffer实现,但也许存在一个。

答案 6 :(得分:2)

这本书中有一个拼写错误。

第一个案例

public String makeSentence(String[] words) {
    String sentence = new String();
    for (String w : words) sentence += w;
    return sentence;
}

复杂性:O(n ^ 2) - > (n个单词)x(每次迭代时复制n个字符,用于将当前句子复制到StringBuffer中)

第二个案例

public String makeSentence(String[] words) {
    StringBuffer sentence = new StringBuffer();
    for (String w : words) sentence.append(w);
    return sentence.toString();
}

复杂性:O(n) - > (n字)x O(1)(StringBuffer连接的摊销复杂性)

答案 7 :(得分:1)

正如书中给出的解释,对于字符串数组中的任何单词,都会创建一个新的句子对象,并且该句子对象首先复制前一个句子,然后遍历到数组的末尾,然后附加新单词,因此n^2的复杂性。

  1. 首先将'n'复制上一句话成新对象
  2. 第二个'n'遍历该数组,然后追加它
  3. 因此n*n将为n^2

答案 8 :(得分:0)

看起来像O(n)给我(n是所有单词中的字母总数)。你基本上迭代words中的每个字符,将其附加到StringBuffer

我认为这是O(n ^ 2)的唯一方法是,如果append()在附加任何新字符之前迭代缓冲区中的所有内容。如果字符数超过当前分配的缓冲区长度(它必须分配一个新缓冲区,然后将所有内容从当前缓冲区复制到新缓冲区),它实际上可能会偶尔执行此操作。但它不会在每次迭代中发生,所以你仍然不会有O(n ^ 2)。

最多你有O(m * n),其中m是缓冲区长度增加的次数。并且因为StringBuffer每次分配更大的缓冲区double its buffer size时我们都可以确定m大致等于log2(n)(实际为log2(n) - log2(16),因为默认初始缓冲区大小为16而不是1)。

所以真正的答案是本书的例子是O(n log n),你可以通过预先分配一个容量足够大的StringBuffer来保持你的所有你的O(n)字母。

请注意,在使用+=附加到字符串的Java中,确实表现出本书解释中描述的低效行为,因为它必须分配新字符串并将两个字符串中的所有数据复制到其中。所以如果你这样做,那就是O(n ^ 2):

String sentence = "";
for (String w : words) {
    sentence += w;
}

但使用StringBuffer不应生成与上例相同的行为。这是StringBuffer首先存在的主要原因之一。

答案 9 :(得分:-1)

这是我对他们如何获得O(n ^ 2)

的计算

我们将忽略声明StringBuffer的CPU时间,因为它不会随着最终字符串的大小而变化。

在计算O复杂度时,我们关注最坏的情况,当有1个字母的字符串时会发生这种情况。我将在这个例子后解释:

假设我们有4个单字母字符串:'A','B','C','D'。

读入A: 查找StringBuffer结束的CPU时间:0 附加'A'的CPU时间:1

读入B: 查找StringBuffer结束的CPU时间:1 附加'B'的CPU时间:1

读入C: 查找StringBuffer结束的CPU时间:2 附加'C'的CPU时间:1

读入D: 查找StringBuffer结束的CPU时间:3 附加'D'的CPU时间:1

最后将StringBuffer复制到String的CPU时间:4

总CPU时间= 1 + 2 + 3 + 4 + 4

如果我们将其概括为n个1个字母的单词:

1 + 2 + 3 + ...... + n + n = 0.5n(n + 1)+ n

我是通过使用算术序列之和的公式来完成的。

O(0.5n ^ 2 + 1.5n)= O(n ^ 2)。

如果我们使用多字母单词,我们将不得不频繁地找到StringBuffer的结尾,从而导致较低的CPU时间和“更好”的情况。