C ++和Java中的字符串连接复杂性[重复]
Posted
技术标签:
【中文标题】C ++和Java中的字符串连接复杂性[重复]【英文标题】:String concatenation complexity in C++ and Java [duplicate] 【发布时间】:2021-11-28 20:05:48 【问题描述】:考虑这段代码:
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
那样复杂度相同的方法?
【问题讨论】:
看到这个帖子了吗? ***.com/questions/2462951/… 或者这个:***.com/questions/7156122 另外,我不知道StringBuilder
/StringBuffer
上有任何时间复杂度保证。
嗯.. 我看到了那个帖子,但我不太同意复杂性部分。
那么在这种情况下是什么:字符串的数量,还是字符串的总长度?大家会在这里选对吗?
【参考方案1】:
这与您的问题有些相干,但仍然相关。 (而且评论太大了!!)
在每次连接时都会创建一个新的字符串副本,因此总体复杂度为 O(n^2)。
在 Java 中,s1.concat(s2)
或 s1 + s2
的复杂度为 O(M1 + M2)
,其中 M1
和 M2
是各自的字符串长度。一般来说,将其转化为串联序列的复杂性是很困难的。但是,如果您假设 N
串联长度为 M
的字符串,那么复杂度确实是 O(M * N * N)
,与您在问题中所说的相符。
幸运的是,在 Java 中,我们可以使用
StringBuffer
解决这个问题,每个附加的复杂度为O(1)
,那么总体复杂度将是O(n)
。
在StringBuilder
的情况下,对于大小为M
的字符串,N
调用sb.append(s)
的摊销复杂性是O(M*N)
。这里的关键词是摊销。当您将字符附加到StringBuilder
时,实现可能需要扩展其内部数组。但是扩展策略是将数组的大小加倍。如果您进行数学计算,您会发现在整个append
调用序列中,缓冲区中的每个字符平均将被复制一次。所以整个序列仍然是 O(M*N)
... 并且,碰巧M*N
是总字符串长度。
所以你的最终结果是正确的,但是你关于单个调用 append
的复杂性的陈述是不正确的。 (我明白你的意思,但你说的方式表面上不正确。)
最后,我要注意的是,在 Java 中,您应该使用 StringBuilder
而不是 StringBuffer
,除非您需要缓冲区是线程安全的。
【讨论】:
【参考方案2】: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&)
覆盖的类型进行字符串化.
【讨论】:
请注意,如果您的std::string
使用指数超调保留策略,即使没有保留,这也是线性的。如果它太小时增长了 1.5 倍,则每个 char 平均被复制 1/(1-1/1.5) 或 3 次:常数因子或 3 或 4 意味着我们仍然是 O(n )。我不知道标准是否规定了这种策略。
指数调整大小似乎是常识,即使使用自动调整大小,您也可能经常看到线性时间性能......但我不记得在标准中看到任何这样的要求。 (不过,除了性能考虑之外,保留正确的大小不太可能使堆碎片化。)
"未指定,但通常在新字符串长度中达到线性。"在cplusplus.com/reference/string/string/operator+= 中提到。这是否意味着它是 O(N^2)
@sww:这意味着循环可以,在某些病理情况下。当您总是一次添加足够多的字符时,就会出现这些情况,因此后备数组必须不断调整大小(阅读:复制到更大的数组)。因此,先发制人地提到reserve
ing 预先确定您需要的大小。但这只是一个问题,如果后备数组没有以指数方式调整大小(例如,每次它必须调整大小时增加一倍或更多),这显然是罕见的,不会引起太严重的关注。指数调整大小将循环减少到摊销的线性时间。【参考方案3】:
作为一个在 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) 时间。但我同意这很尴尬。
将std::move
与上述StringAppender
(即sa += std::move(s1)
)一起使用将显着提高非短字符串的性能(或将其与xvalues 等一起使用)
我不知道std::ostringstream
的复杂性,但ostringstream
用于漂亮的打印格式化输出,或者高性能不重要的情况。我的意思是,它们还不错,它们甚至可以胜过脚本/解释/字节码语言,但如果你赶时间,你需要别的东西。
像往常一样,您需要进行分析,因为常数因素很重要。
一个 rvalue-reference-to-this operator+ 也可能是一个不错的选择,但很少有编译器实现对 this 的 rvalue 引用。
【讨论】:
log n
来自哪里?另外,您为什么认为这比仅附加到字符串更好?您将每个字符串复制两次(一次复制到 operator+=
的参数变量中,一次复制到最终字符串中)。 (大多数)std::string 实现使用的标准指数重新分配算法平均复制每个字符两次(给或取一点)。
你是对的,没有 lg k 或 n 因素。我复制每个字符两次,而不是每个字符串。如果我的餐巾纸数学正确,则 1.5 指数重新分配平均复制每个字符 3 次。我平均移动每个字符串 4 次(一次在重新分配期间,1/(1-1/1.5)),但这涉及 0 个字符副本(忽略短字符串优化)。简而言之,缓存一致性很容易让 += 击败我(它的副本对缓存友好):但如果你给我 xvalue 字符串,我想我可以击败 += (每个字符 1 个副本)。
我刚刚检查了 libcxx (clang) 和 libstdc++ (gcc),它们都使用了倍增指数算法。所以每个字符被复制两次(大致:多一点,因为最终字符串的大小不是 2 的幂,但少一点,因为中间字符串在将它们推到边缘的字符串被复制之前被加倍。)
以上是关于C ++和Java中的字符串连接复杂性[重复]的主要内容,如果未能解决你的问题,请参考以下文章
Java - 将引号附加到数组中的字符串并连接数组中的字符串