Сложность конкатенации строк в С++ и 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
с такой же сложностью?
Ответы
Ответ 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 на это.