web-dev-qa-db-ja.com

この単純なコードの複雑さは何ですか?

私が持っている電子ブックからこのテキストを貼り付けています。 O(n2)そしてそれについても説明しますが、私はその方法がわかりません。

質問:このコードの実行時間はどのくらいですか?

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

本が与えた答え:

オン2)、ここでnは文中の文字数です。その理由は次のとおりです。文に文字列を追加するたびに、文のコピーを作成し、文内のすべての文字を実行してコピーします。ループ内で毎回最大n文字を繰り返す必要がある場合は、次のようになります。少なくともn回ループすると、O(n2)実行時。痛い!

誰かがこの答えをより明確に説明できますか?

31
Someone

私はたまたまその本を読んだので、これは誤解を招く問題のようです。本のテキストのこの部分はタイプミスです!コンテキストは次のとおりです。

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

質問:このコードの実行時間はどのくらいですか?

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(n2)、ここでnは文の文字数です。その理由は次のとおりです。文に文字列を追加するたびに、文のコピーを作成し、文内のすべての文字を調べてコピーします。ループ内で毎回最大n文字を繰り返す必要があり、少なくともn回ループしている場合は、O(n2)実行時。痛い! 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(n2)彼女が言及した解決策(最初の解決策)は、「最適化された」解決策(後者)とまったく同じでした。したがって、私の結論は、O(nの例として、次のすべての文字列を追加するときに常に古い文を新しいバッファにコピーするなど、作成者が何か他のものをレンダリングしようとしていたということです。2)アルゴリズム。著者も「StringBuffer(またはStringBuilder)を使用すると、この問題を回避できる」と述べているため、StringBufferはそれほどばかげているべきではありません。

23
Han Zhou

受け入れられた答えはちょうど間違っています。 StringBufferは償却済みO(1)追加なので、n追加はO(n)になります。

O(1) appendでない場合、StringBufferは存在する理由がありません。これは、単純なString連結でループを記述するとO( n ^ 2)も!

17
porges

このコードが実装の詳細を抽象化する高レベルで記述されている場合、このコードの複雑さに関する質問に答えるのは少し難しいです。 Javaドキュメント は、append関数の複雑さに関して何の保証も与えていないようです。他の人が指摘しているように、StringBufferクラスは、文字列の追加の複雑さがStringBufferに保持されている文字列の現在の長さに依存しないように記述できます(そしてそうすべきです)。

しかし、この質問をしている人が単に「あなたの本は間違っている!」と言うのはそれほど役に立たないと思います。 -代わりに、どのような仮定が行われているのかを確認し、作成者が何を言おうとしていたのかを明確にしましょう。

次の仮定を行うことができます。

  1. _new StringBuffer_の作成はO(1)です
  2. wで次の文字列wordsを取得するのはO(1)です
  3. _sentence.toString_を返すのは最大でO(n)です。

問題は、実際にはsentence.append(w)の順序が何であるかであり、それはStringBuffer内でどのように発生するかによって異なります。素朴な方法は、 Shlemiel the Painter のようにすることです。

ばかげた方法

StringBufferの内容にCスタイルのnullで終了する文字列を使用するとします。このような文字列の終わりを見つける方法は、ヌル文字が見つかるまで各文字を1つずつ読み取ることです。次に、新しい文字列Sを追加するには、SからStringBufferへの文字のコピーを開始できます。文字列(別のヌル文字で終了)。このようにappendと書くと、O(a + b)になります。ここで、aは現在の文字数です。 StringBufferにあり、bは新しいWordの文字数です。単語の配列をループし、新しい単語を追加する前に追加したばかりのすべての文字を読み取る必要がある場合、ループの複雑さはO(n ^ 2)です。ここで、nは文字の総数です。すべての単語で(最後の文の文字数も)。

より良い方法

一方、StringBufferの内容はまだ文字の配列であると仮定しますが、文字列の長さ(文字数)を示す整数sizeも格納します。文字列の終わりを見つけるために、StringBufferのすべての文字を読み取る必要がなくなりました。配列内のインデックスsizeを検索できます。これは、O(a)ではなくO(1)です。次に、append関数は、追加される文字数O(b)のみに依存するようになりました。この場合、ループの複雑さはO(n)です。ここで、nはすべての単語の文字の総数です。

...まだ終わっていません!

最後に、まだカバーされていない実装のもう1つの側面があります。それは、教科書の回答によって実際に取り上げられたものです。それはメモリ割り当てです。 StringBufferにさらに文字を書き込むたびに、新しいWordを実際に収めるのに十分なスペースが文字配列にあるとは限りません。十分なスペースがない場合、コンピューターは次のことを行う必要があります。最初にメモリのクリーンなセクションにさらにスペースを割り当て、次に古いStringBuffer配列のすべての情報をコピーして、以前と同じように続行できます。このようなデータのコピーには、O(a)時間がかかります(ここで、aはコピーされる文字数です)。

最悪の場合、新しいWordを追加するたびにより多くのメモリを割り当てる必要があります。これは基本的に、ループが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)になるように関数を変更する簡単な方法を見つけてください。

17
Brian L

本の中のこれらのテキストはタイプミスであるに違いないと思います、私は正しい内容が以下にあると思います、私はそれを修正します:

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

質問:このコードの実行時間はどのくらいですか?

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

回答:O(n2)、ここでnは文中の文字数です。その理由は次のとおりです。文に文字列を追加するたびに、文のコピーを作成し、文内のすべての文字を調べてコピーします。ループ内で毎回最大n文字を繰り返す必要があり、少なくともn回ループしている場合は、O(n2)実行時。痛い! StringBuffer(またはStringBuilder)を使用すると、この問題を回避できます。

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

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

私は正しいですか?

11
Tyler.Zhang

このプログラムを使って確認してみました

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テスト200000-128ミリ秒

Javaテスト500000-370ミリ秒

Javaテスト1000000-698ミリ秒

バージョン1.6.0.21

11

それは本当にStringBufferの実装に依存します。 .append()が一定時間だったとすると、_n = length of the words array_の時間内にO(n)アルゴリズムがあることは明らかです。 _.append_ そうではない一定の時間の場合、メソッドの時間計算量でO(n)を乗算する必要があります。 StringBufferの現在の実装では、文字列が1文字ずつコピーされるため、上記のアルゴリズムは次のようになります。

Θ(n*m)、またはO(n*m)、ここでnは単語数、mは平均単語長であり、あなたの本は間違っています。私はあなたが厳格な限界を探していると思います。

本の答えが正しくないという簡単な例:_String[] words = ['alphabet']_本の定義では_n=8_なので、アルゴリズムは64ステップで制限されます。これは本当ですか?明らかに厳密ではありません。 n文字の割り当てとコピー操作が1つあるので、約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);
}
_

あなたの本は古いか、ひどいか、あるいはその両方です。 StringBufferの最適ではない実装を見つけるために、JDKバージョンを掘り下げるのに十分な決意はありませんが、おそらく1つ存在します。

2
Stefan Kendall

この本にはタイプミスがあります。


最初のケース

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

複雑さ:O(n ^ 2)->(nワード)x(現在の文をStringBufferにコピーするために、各反復でn文字がコピーされます)


2番目のケース

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連結の償却された複雑さ)

2
Dimos

この本で説明されているように、文字列配列内のWordは常に新しい文のオブジェクトが作成され、その文オブジェクトは最初に前の文をコピーし、次に配列の最後までトラバースしてから新しいWordを追加するため、複雑になります。 n^2の。

  1. 前の文を新しいオブジェクトにコピーする最初の「n」
  2. 2番目の「n」はその配列をトラバースしてから追加します

したがって、n*nn^2になります。

1
tryit

私にはO(n)のように見えます(nはすべての単語の文字の総数です)。基本的にはwordsそれをStringBufferに追加します。

これをO(n ^ 2)と見なす唯一の方法は、append()が新しい文字を追加する前に、バッファー内のすべての内容を反復する場合です。また、文字数が現在割り当てられているバッファ長を超える場合、実際にこれを行うことがあります(新しいバッファを割り当ててから、現在のバッファからすべてを新しいバッファにコピーする必要があります)。ただし、すべての反復で発生するわけではないため、O(n ^ 2)はありません。

せいぜいO(m * n)になります。ここで、mはバッファ長が増加した回数です。また、StringBufferバッファサイズの2倍 より大きなバッファを割り当てるたびに、mlog2(n)とほぼ等しいと判断できるため(デフォルトの初期バッファサイズは1)ではなく16であるため、実際にはlog2(n) - log2(16)です。

したがって、本当の答えは、本の例はO(n log n)であり、容量のあるStringBufferを事前に割り当てることで、O(n)に下げることができるということです。あなたのすべての手紙を保持するのに十分な大きさ。

Java +=を使用して文字列に追加すると、新しい文字列を割り当て、両方の文字列からすべてのデータをコピーする必要があるため、本の説明で説明されている非効率的な動作を示すことに注意してくださいこれを行うと、O(n ^ 2)になります。

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

ただし、StringBufferを使用しても、上記の例と同じ動作は生成されません。これが、そもそもStringBufferが存在する主な理由の1つです。

0
aroth