web-dev-qa-db-ja.com

C ++およびJavaでの文字列連結の複雑さ

次のコードを検討してください。

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

連結ごとに文字列の新しいコピーが作成されるため、全体的な複雑度はO(n^2)になります。幸いなことにJavaでこれを解決できます。StringBufferは、各追加に対してO(1)の複雑さを持つため、全体の複雑さはO(n)になります。 。

C++では、std::string::append()の複雑度はO(n)であり、stringstreamの複雑さについてはよくわかりません。

C++では、StringBufferのような同じ複雑さのメソッドはありますか?

21
ethanjyx

C++文字列は変更可能であり、StringBufferと同じくらい動的にサイズ設定できます。 Javaの同等のコードとは異なり、このコードは毎回新しい文字列を作成しません。現在のものに追加するだけです。

std::string joinWords(std::vector<std::string> const &words) {
    std::string result;
    for (auto &Word : words) {
        result += Word;
    }
    return result;
}

事前に必要なサイズをreserve場合、これは線形時間で実行されます。問題は、ベクトルをループしてサイズを取得する方が、文字列を自動サイズ変更させるよりも遅くなるかどうかです。それは私には言えませんでした。時間を計る。 :)

std::string自体をなんらかの理由で使用したくない場合(そして考慮する必要があります。これは完全に立派なクラスです)、C++にも文字列ストリームがあります。

#include <sstream>
...

std::string joinWords(std::vector<std::string> const &words) {
    std::ostringstream oss;
    for (auto &Word : words) {
        oss << Word;
    }
    return oss.str();
}

それはおそらくstd::stringを使用するよりも効率的ではありませんが、他の場合には少し柔軟です-ほぼすべてのプリミティブ型と、operator <<(ostream&, its_type&)を指定したすべての型を文字列化できますオーバーライド。

23
cHao

これはあなたの質問にいくらか正接しますが、それでも関連があります。 (コメントには大きすぎます!!)

連結ごとに文字列の新しいコピーが作成されるため、全体的な複雑度はO(n ^ 2)になります。

Javaでは、s1.concat(s2)またはs1 + s2の複雑さはO(M1 + M2)で、M1およびM2はそれぞれの文字列の長さです。これを一連の連結の複雑さに変換することは、一般に困難です。ただし、長さがNの文字列のM連結を想定すると、複雑さは実際にO(M * N ^ 2)になり、質問での発言と一致します。

幸い、Javaでこれを解決するには、StringBufferを使用します。これは、追加ごとにO(1)の複雑さを持つため、全体の複雑さはO(n)になります。

StringBuilderの場合、サイズ変数Nis sb.append(s)O(M*N)へのM呼び出しのamortized複雑さ。ここでのキーワードはamortizedです。 StringBuilderに文字を追加する場合、実装で内部配列を拡張する必要がある場合があります。しかし、拡張戦略はアレイのサイズを2倍にすることです。そして、計算を行うと、バッファ内の各文字が、append呼び出しのシーケンス全体で、平均して1回余分にコピーされることがわかります。そのため、シーケンス全体は引き続きO(M*N) ...として機能します。また、場合によっては、M*Nは文字列の全長です。

したがって、最終結果は正しいですが、appendへの1回の呼び出しの複雑さに関するステートメントは正しくありません。 (私はあなたが何を意味するのか理解していますが、あなたがそれを言う方法は顔面で正しくありません。)

最後に、Javaでは、needする必要がない限り、StringBuilderではなくStringBufferを使用する必要があることに注意してください。スレッドセーフ。

13
Stephen C

C++ 11でO(n)の複雑さを持つ本当に単純な構造の例として:

template<typename TChar>
struct StringAppender {
  std::vector<std::basic_string<TChar>> buff;
  StringAppender& operator+=( std::basic_string<TChar> v ) {
    buff.Push_back(std::move(v));
    return *this;
  }
  explicit operator std::basic_string<TChar>() {
    std::basic_string<TChar> retval;
    std::size_t total = 0;
    for( auto&& s:buff )
      total+=s.size();
    retval.reserve(total+1);
    for( auto&& s:buff )
      retval += std::move(s);
    return retval;
  }
};

使用する:

StringAppender<char> append;
append += s1;
append += s2;
std::string s3 = append;

これはO(n)をとります。ここで、nは文字数です。

最後に、すべての文字列の長さがわかっている場合は、reserveを十分なスペースで実行するだけで、appendまたは+=に合計O(n) =時間ですが、それは厄介です。

上記のStringAppender(つまりsa += std::move(s1))でstd::moveを使用すると、短い文字列(またはxvaluesなどで使用)のパフォーマンスが大幅に向上します。

std::ostringstreamの複雑さはわかりませんが、ostringstreamはフォーマットされた出力をきれいに印刷するため、または高いパフォーマンスが重要ではない場合のためのものです。つまり、それらは悪くはなく、スクリプト化/解釈/バイトコード言語を実行する可能性もありますが、Rushを使用している場合は、別のものが必要です。

一定の要素が重要であるため、いつものように、プロファイルを作成する必要があります。

Thisへの右辺値参照+も良いかもしれませんが、これに対する右辺値参照を実装するコンパイラはほとんどありません。