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
方法是否具有相同的复杂性?
C ++字符串是可变的,并且几乎与StringBuffer一样动态可变。 与Java中的等价物不同,此代码每次都不会创建新的字符串; 它只是附加到当前的一个。
std::string joinWords(std::vector const &words) { std::string result; for (auto &word : words) { result += word; } return result; }
如果您预先reserve
所需的尺寸,则会以线性时间运行。 问题是,循环向量以获取大小是否比让字符串自动resize要慢。 那个,我不能告诉你。 时间吧。 🙂
如果由于某种原因你不想使用std::string
本身(你应该考虑它;它是一个非常值得尊敬的类),C ++也有字符串流。
#include ... std::string joinWords(std::vector const &words) { std::ostringstream oss; for (auto &word : words) { oss << word; } return oss.str(); }
它可能不比使用std::string
更有效,但在其他情况下它更灵活一点 - 你可以用它来串行化任何基本类型,以及任何指定了operator <<(ostream&, its_type&)
类型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
情况下,对于大小为M
字符串, N
调用sb.append(s)
的分摊复杂度为O(M*N)
。 这里的关键词是摊销 。 将字符追加到StringBuilder
,实现可能需要扩展其内部数组。 但扩展策略是将arrays的大小加倍。 如果你进行数学计算,你会发现缓冲区中的每个字符平均会在整个append
调用序列中被复制一次。 所以整个序列仍然可以作为O(M*N)
……并且,当它发生时, M*N
是总的字符串长度。
所以你的最终结果是正确的,但你关于单个append
调用的复杂性的陈述是不正确的。 (我明白你的意思,但你说它的方式是不正确的。)
最后,我要注意在Java中你应该使用StringBuilder
而不是StringBuffer
除非你需要缓冲区是线程安全的。
作为在C ++ 11中具有O(n)
复杂度的非常简单的结构的示例:
template struct StringAppender { std::vector> buff; StringAppender& operator+=( std::basic_string v ) { buff.push_back(std::move(v)); return *this; } explicit operator std::basic_string () { std::basic_string 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 append; append += s1; append += s2; std::string s3 = append;
这需要O(n),其中n是字符数。
最后,如果您知道所有字符串的长度,只需要有足够空间的reserve
使得append
或+=
总共花费O(n)时间。 但我同意这很尴尬。
使用std::move
与上面的StringAppender
(即sa += std::move(s1)
)将显着提高非短字符串的性能(或使用xvalues等)
我不知道std::ostringstream
的复杂性,但是ostringstream
用于漂亮的打印格式化输出,或者高性能并不重要的情况。 我的意思是,它们并不坏,甚至可以执行脚本/解释/字节码语言,但如果你匆忙,你还需要别的东西。
像往常一样,您需要分析,因为恒定因素很重要。
rvalue-reference-to-this operator +也可能是一个好的,但是很少有编译器实现rvalue引用。