次のコードを検討してください。
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
のような同じ複雑さのメソッドはありますか?
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&)
を指定したすべての型を文字列化できますオーバーライド。
これはあなたの質問にいくらか正接しますが、それでも関連があります。 (コメントには大きすぎます!!)
連結ごとに文字列の新しいコピーが作成されるため、全体的な複雑度は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
の場合、サイズ変数N
is sb.append(s)
のO(M*N)
へのM
呼び出しのamortized複雑さ。ここでのキーワードはamortizedです。 StringBuilder
に文字を追加する場合、実装で内部配列を拡張する必要がある場合があります。しかし、拡張戦略はアレイのサイズを2倍にすることです。そして、計算を行うと、バッファ内の各文字が、append
呼び出しのシーケンス全体で、平均して1回余分にコピーされることがわかります。そのため、シーケンス全体は引き続きO(M*N)
...として機能します。また、場合によっては、M*N
は文字列の全長です。
したがって、最終結果は正しいですが、append
への1回の呼び出しの複雑さに関するステートメントは正しくありません。 (私はあなたが何を意味するのか理解していますが、あなたがそれを言う方法は顔面で正しくありません。)
最後に、Javaでは、needする必要がない限り、StringBuilder
ではなくStringBuffer
を使用する必要があることに注意してください。スレッドセーフ。
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への右辺値参照+も良いかもしれませんが、これに対する右辺値参照を実装するコンパイラはほとんどありません。