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),其中 M1M2 是各自的字符串长度。一般来说,将其转化为串联序列的复杂性是很困难的。但是,如果您假设 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 &lt;&lt;(ostream&amp;, its_type&amp;) 覆盖的类型进行字符串化.

【讨论】:

请注意,如果您的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:这意味着循环可以,在某些病理情况下。当您总是一次添加足够多的字符时,就会出现这些情况,因此后备数组必须不断调整大小(阅读:复制到更大的数组)。因此,先发制人地提到reserveing 预先确定您需要的大小。但这只是一个问题,如果后备数组没有以指数方式调整大小(例如,每次它必须调整大小时增加一倍或更多),这显然是罕见的,不会引起太严重的关注。指数调整大小将循环减少到摊销的线性时间。【参考方案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 中的 String 相加

[转载]C#如何清除字符串数组中的重复项

Java - 将引号附加到数组中的字符串并连接数组中的字符串

如何使用 Java Regex 查找字符串中的所有重复字符序列?

确定字符串是不是是Java中的整数[重复]

关于C中的指针和字符串的问题[重复]