【问题标题】:String concatenation complexity in C++ and Java [duplicate]C ++和Java中的字符串连接复杂性[重复]
【发布时间】:2013-03-02 06:49:59
【问题描述】:

考虑这段代码:

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 那样复杂度相同的方法?

【问题讨论】:

  • 看到这个帖子了吗? stackoverflow.com/questions/2462951/…
  • 另外,我不知道StringBuilder/StringBuffer 上有任何时间复杂度保证。
  • 嗯.. 我看到了那个帖子,但我不太同意复杂性部分。
  • 那么在这种情况下是什么:字符串的数量,还是字符串的总长度?大家会在这里选对吗?

标签: java c++ string time-complexity


【解决方案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 的幂,但少一点,因为中间字符串在将它们推到边缘的字符串被复制之前被加倍。)
    猜你喜欢
    • 2019-01-16
    • 1970-01-01
    • 2015-09-18
    • 2015-07-31
    • 1970-01-01
    • 1970-01-01
    • 2017-01-05
    • 2018-01-06
    相关资源
    最近更新 更多