Подтвердить что ты не робот

Сложность конкатенации строк в С++ и 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).

Пока в С++, std::string::append() имеет сложность O(n), и я не понимаю сложность stringstream.

В С++ существуют ли методы типа StringBuffer с такой же сложностью?

4b9b3361

Ответ 1

Строки С++ изменяемы и в значительной степени динамически значимы как 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 по какой-либо причине (и вы должны это рассмотреть, это вполне уважаемый класс), С++ также имеет потоки строк.

#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&).

Ответ 2

Это несколько касательно вашего Вопроса, но тем не менее актуально. (И слишком большой для комментария!!)

При каждой конкатенации создается новая копия строки, так что общая сложность 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 вызывает sb.append(s) для строк размера M - O(M*N). Ключевое слово здесь амортизируется. Когда вы добавляете символы в StringBuilder, реализации, возможно, потребуется расширить свой внутренний массив. Но стратегия расширения - удвоить размер массива. И если вы выполните математику, вы увидите, что в среднем каждый символ в буфере будет скопирован за одно дополнительное время в течение всей последовательности вызовов append. Таким образом, вся последовательность все еще работает как O(M*N)... и, как это бывает, M*N - общая длина строки.

Итак, ваш конечный результат правильный, но ваше утверждение о сложности одного вызова append неверно. (Я понимаю, что вы имеете в виду, но, как вы говорите, это неправильно.)

Наконец, я хотел бы отметить, что в Java вы должны использовать StringBuilder, а не StringBuffer, если вам не нужен буфер для потокобезопасности.

Ответ 3

В качестве примера действительно простой структуры с O(n) сложностью в С++ 11:

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 + также может быть хорошим, но лишь немногие компиляторы реализуют ссылки rvalue на это.