Пространственная сложность добавления строк Java char за char

При построении строки Java char за символом через цикл через «сложение» можно заметить, что сложность времени выполнения этой операции невелика: она квадратична O(n^2) время. Тем не менее, мне интересно, является ли пространственная сложность «добавление» String char-by-char через цикл также плохой.

Вот минимальный пример программы, которая выполняет построение строки путем посимвольного сложения с секундомером. Результаты, показанные ниже, ясно показывают квадратичную кривую:

/**Create a String of n 'A's char-by-char.
 * @param n Number of 'A's to be appended
 * @return The finished string
 */
public static String appendString(int n) {
    String str = "";
    // Timed run starts here
    long t = System.currentTimeMillis();
    // String concatenation occurs here
    for (int k = 0; k < n; k++)
        str += "A";
    t = System.currentTimeMillis() - t;
    // Timed run ends
    System.out.printf("%d\t%d\n", n, t);
    return str;
}

Хотя я могу получить временную сложность программы, мне интересно, какова пространственная сложность построения строки char-by-char?

Предпочтителен подробный ответ, касающийся управления памятью. (Я подозреваю, что это также квадратично, потому что строки неизменяемы, и каждый раз вам приходится выделять новую память для постоянно увеличивающихся строк)

Примечание. Это не дубликат сложности конкатенации строк. в C++ и Java, потому что он не учитывает пространственную сложность. Я специально прошу подробный анализ пространственной сложности.


person Community    schedule 01.07.2018    source источник
comment
Ну, квадратичный в краткосрочной перспективе. Но старые неиспользуемые строки должны быть удалены сборщиком мусора, поэтому необходимый объем памяти должен быть только пропорционален размеру (длине) строк, которые нужны в любой момент времени.   -  person markspace    schedule 01.07.2018
comment
Так действительно ли это линейное пространство в среднем случае? Особенно для больших строк?   -  person    schedule 01.07.2018
comment
Не линейное время, линейное пространство (отпечаток памяти). И я тоже не уверен насчет среднего, но в долгосрочной перспективе да, думаю, да.   -  person markspace    schedule 01.07.2018
comment
Есть ли лучшие методы, чем объединение строк за строкой. Есть ли какие-либо улучшения пространственной сложности при использовании StringBuilder?   -  person    schedule 01.07.2018
comment
Обязательная ссылка: stackoverflow.com/questions/504103/   -  person lexicore    schedule 01.07.2018
comment
Ну в общем случае сложно сказать. Иногда, когда требуется оптимизация, я рассматривал возможность написания собственного строкового класса, но до сих пор мне удавалось этого избежать. Я думаю, что лучшие методы зависят от приложения и модели использования. Как реализация общего назначения, String и StringBuilder являются хорошо реализованными классами.   -  person markspace    schedule 01.07.2018
comment
Таким образом, в целом квадратичная сложность (как по времени, так и по пространству) подразумевает, что добавление строк посимвольно не рекомендуется для более длинных строк.   -  person    schedule 01.07.2018
comment
Я думаю, что это утверждение может быть слишком широким. Это отключает мой датчик преждевременной оптимизации. Реализуйте код простым методом, применяя только очевидные оптимизации. Затем проверьте производительность реального работающего кода и оптимизируйте только там, где это необходимо. Если построение строкового символа по символам кажется правильным, используйте этот метод.   -  person markspace    schedule 01.07.2018
comment
Спасибо. Интересно, где канонический вопрос о том, как лучше всего добавить строку char-by-char в зависимости от длины?   -  person    schedule 01.07.2018
comment
Не думаю, что у тебя получится лучше, чем у StringBuilder. Он был разработан с учетом такого сценария.   -  person Dawood ibn Kareem    schedule 02.07.2018


Ответы (2)


Он использует квадратичное пространство. Или, скорее, выделяется квадратичное количество места, потому что каждая итерация цикла (по крайней мере, в коде, с которым JIT не делает ничего умного) выделяет новый массив символов:

new char[1]
new char[2]
new char[3]
// Etc.

Причина квадратичного быстродействия заключается в копировании строк в эти постоянно увеличивающиеся массивы.

person Andy Turner    schedule 01.07.2018
comment
Спасибо. Я разместил этот вопрос, чтобы решить проблему сложности пространства, чтобы подчеркнуть, что добавление строк посимвольно не рекомендуется с точки зрения сложности. Итак, рекомендуется ли добавлять строки посимвольно только для небольших строк? - person ; 01.07.2018
comment
Пожалуйста, объясните квадратное пространство. Как больше места для 2n+1 нужно? - person lexicore; 01.07.2018
comment
Я бы не сказал, что это когда-либо будет рекомендовано. Это может быть просто целесообразным выбором, чтобы что-то реализовать. - person Andy Turner; 01.07.2018
comment
@lexicore запустит new char[1], new char[2], new char[3] и т. д., чтобы создать новый экземпляр строки. Возможно, JIT сможет оптимизировать его для более эффективной работы, но это наивный подход. - person Andy Turner; 01.07.2018
comment
Это неправильно, конечно. Как только вы попадете в очень большие строки, сборщик мусора соберет любой из массивов, кроме двух самых больших. Таким образом, требования к пространству должны быть линейными, а не квадратичными. Поначалу она будет казаться квадратичной, конечно, когда программа запустится. - person Dawood ibn Kareem; 02.07.2018
comment
@DawoodibnKareem Я думаю, это зависит от того, считаете ли вы действие GC частью семантики этого кода. Вот почему я добавил или, вернее...: квадратичное пространство выделено; когда (и будет ли) это очищено, программист не может контролировать. - person Andy Turner; 02.07.2018
comment
@AndyTurner Я думаю, что единственным разумным соображением будет теоретически идеальный GC, который проясняет ситуацию сразу после того, как в ней больше нет необходимости. Потому что, если вы позволите GC быть неидеальным, вообще нет смысла рассуждать о пространственной сложности. Однако мне не удалось найти хороших ссылок по теме. - person lexicore; 02.07.2018

Ваш код довольно неэффективен, потому что создается неявный StringBuilder для добавления одного символа в ваш цикл. Этот,

for (int k = 0; k < n; k++)
    str += "A";

эквивалентно чему-то вроде

for (int k = 0; k < n; k++)
    str = new StringBuilder(str).append("A").toString();

Вы можете значительно улучшить производительность пространства (и времени), используя вместо этого один StringBuilder (и вы можете явно указать его размер). Нравиться,

StringBuilder sb = new StringBuilder(n);
for (int k = 0; k < n; k++)
    sb.append("A");
String str = sb.toString();

Или, в Java 8+ вы можете использовать генератор и ограничить его до n раз и собрать это. Нравиться,

return Stream.generate(() -> "A").limit(n).collect(Collectors.joining());
person Elliott Frisch    schedule 01.07.2018
comment
Подход может быть еще более эффективным с использованием char[] cs = new char[n]; Arrays.fill(cs, 'A'); String str = new String(cs);. - person Andy Turner; 02.07.2018
comment
Чтобы сделать ваш вопрос по теме, не могли бы вы также указать временную сложность? Я прошу подробного анализа пространственной сложности того, как Java управляет памятью при выполнении этого неэффективного кода. - person ; 02.07.2018
comment
Временная сложность O(n) (как и ваше решение). Постоянные факторы отбрасываются при обсуждении временной сложности. Мы могли бы уменьшить его до O(log n), используя более длинные последовательности A. - person Elliott Frisch; 02.07.2018