В чем сложность этого простого кода?
Я вставляю этот текст из книги, которую у меня есть. Он говорит о сложности, если O (n 2), а также дает объяснение, но я не вижу, как.
Вопрос: Каково время работы этого кода?
public String makeSentence(String[] words) {
StringBuffer sentence = new StringBuffer();
for (String w : words) sentence.append(w);
return sentence.toString();
}
Ответ, полученный в книге:
O (n 2), где n - количество букв в предложении. Вот почему: каждый раз, когда вы добавьте строку в предложение, вы создадите копию предложения и пропустите все буквы в предложение, чтобы скопировать их. Если вам нужно перебирать до n символов каждый раз в loop и youre looping как минимум n раз, что дает вам время O (n 2). Ой!
Может кто-нибудь объяснить этот ответ более четко?
Ответы
Ответ 1
Это, кажется, вопрос обман, потому что мне довелось сейчас прочитать эту книгу. Эта часть текста в книге - опечатка! Вот контекст:
=============================================== ====================
Вопрос: Каково время работы этого кода?
1 public String makeSentence(String[] words) {
2 StringBuffer sentence = new StringBuffer();
3 for (String w : words) sentence.append(w);
4 return sentence.toString();
5 }
Ответ: O (n 2), где n - количество букв в предложении. Вот почему: каждый раз, когда вы добавляете строку в предложение, вы создаете копию предложения и просматриваете все буквы в предложении, чтобы скопировать их. Если вам нужно перебирать до n символов каждый раз в цикле, а вы зацикливаете не менее n раз, это дает вам время выполнения O (n 2). Ой!
С помощью StringBuffer (или StringBuilder) вы можете избежать этой проблемы.
1 public String makeSentence(String[] words) {
2 StringBuffer sentence = new StringBuffer();
3 for (String w : words) sentence.append(w);
4 return sentence.toString();
5 }
=============================================== ======================
Вы заметили, что автор испортил это? Решение O (n 2), которое она упомянула (первое), было точно таким же, как "оптимизированное" (последнее). Итак, мой вывод заключается в том, что автор пытался отобразить что-то еще, например, всегда копировать старое предложение в новый буфер при добавлении каждой следующей строки в качестве примера алгоритма O (n 2), StringBuffer не должен быть таким глупым, как автор также упомянул: "С StringBuffer (или StringBuilder) может помочь вам избежать этой проблемы".
Ответ 2
Немного сложно ответить на вопрос о сложности этого кода, когда он написан на высоком уровне, который абстрагирует детали реализации. Документация документации Java, похоже, не дает никаких гарантий с точки зрения сложности функции append
. Как указывали другие, класс StringBuffer
может (и должен) записываться так, чтобы сложность добавляемых строк не зависела от текущей длины строки, хранящейся в StringBuffer
.
Однако, я подозреваю, что это не так полезно для человека, задающего этот вопрос, просто сказать: "Ваша книга не так!" - вместо этого давайте посмотрим, какие предположения сделаны, и дайте понять, что автор пытался сказать.
Вы можете сделать следующие предположения:
- Создание
new StringBuffer
- O (1)
- Получение следующей строки
w
в words
есть O (1)
- Возврат
sentence.toString
не более O (n).
Вопрос в том, каков порядок sentence.append(w)
, и это зависит от того, как это происходит внутри StringBuffer
. Наивный способ - сделать это как Шлемиэль Живописец.
Глупый путь
Предположим, вы используете строку с нулевым символом C-стиля для содержимого StringBuffer
. То, как вы находите конец такой строки, - это чтение каждого символа один за другим, пока не найдете нулевой символ - затем добавьте новую строку S, вы можете начать копировать символы из S в строку StringBuffer
(заканчивая с другим нулевым символом). Если вы пишете append
таким образом, это O (a + b), где a - количество символов, находящихся в настоящее время в StringBuffer
, а b - количество символов в новом слове. Если вы зацикливаете массив слов и каждый раз, когда вам нужно прочитать все символы, которые вы только что добавили, перед добавлением нового слова, тогда сложность цикла равна O (n ^ 2), где n - общее количество символов во всех словах (также количество символов в последнем предложении).
Лучший способ
С другой стороны, предположим, что содержимое StringBuffer
все еще является массивом символов, но мы также сохраняем целое число size
, которое сообщает нам, как долго строка (количество символов). Теперь нам больше не нужно читать каждый символ в StringBuffer
, чтобы найти конец строки; мы можем просто найти индекс size
в массиве, который является O (1) вместо O (a). Тогда функция append
теперь зависит только от количества добавляемых символов, O ( b). В этом случае сложность цикла равна O (n), где n - общее количество символов во всех словах.
... Мы еще не закончили!
Наконец, есть еще один аспект реализации, который еще не был рассмотрен, и это тот, который действительно вызван ответом в учебнике - выделение памяти. Каждый раз, когда вы хотите записать больше символов в свой StringBuffer
, вам не гарантировано будет достаточно места в вашем массиве символов, чтобы оно действительно соответствовало новому слову. Если места недостаточно, ваш компьютер должен сначала выделить некоторые больше места в чистой части памяти, а затем скопируйте всю информацию в старый массив StringBuffer
, а затем он может продолжаться по-прежнему. Для копирования таких данных потребуется время O (a) (где a - количество символов, которые нужно скопировать).
В худшем случае вам нужно выделять больше памяти каждый раз, когда вы добавляете новое слово. Это в основном возвращает нас к квадрату, где цикл имеет сложность O (n ^ 2), и это то, что, по-видимому, предлагает книга. Если вы предполагаете, что ничего сумасшедшего не происходит (слова не доходят до экспоненциальной скорости!), То вы, вероятно, можете уменьшить количество распределений памяти на что-то большее, чем O (log (n)) за счет увеличения выделенной памяти экспоненциально. Если количество распределений памяти и распределение памяти в общем случае O (a), то общая сложность, связанная с управлением памятью в цикле, равна O (n log (n)). Поскольку работа по добавлению O (n) и меньше сложности управления памятью, общая сложность функции O (n log (n)).
Опять же, документация Java не помогает нам с точки зрения увеличения емкости StringBuffer
, она просто говорит: "Если внутренний буфер переполняется, он автоматически становится больше". В зависимости от того, как это происходит, вы можете в итоге получить либо O (n ^ 2), либо O (n log (n)).
Как упражнение осталось читателю: найдите простой способ изменить функцию, чтобы общая сложность O (n), устраняя проблемы перераспределения памяти.
Ответ 3
Принятый ответ просто неверен. StringBuffer
имеет амортизацию O (1) append, поэтому n добавляет O (n).
Если бы не O (1) append, StringBuffer
не имел бы причин существовать, так как запись этого цикла с простым конкатенацией String
также была бы O (n ^ 2)!
Ответ 4
Я попытался проверить его с помощью этой программы
public class Test {
private static String[] create(int n) {
String[] res = new String[n];
for (int i = 0; i < n; i++) {
res[i] = "abcdefghijklmnopqrst";
}
return res;
}
private static String makeSentence(String[] words) {
StringBuffer sentence = new StringBuffer();
for (String w : words) sentence.append(w);
return sentence.toString();
}
public static void main(String[] args) {
String[] ar = create(Integer.parseInt(args[0]));
long begin = System.currentTimeMillis();
String res = makeSentence(ar);
System.out.println(System.currentTimeMillis() - begin);
}
}
И результат был, как и ожидалось, O (n):
java Test 200000 - 128 мс
java Test 500000 - 370 мс
java Test 1000000 - 698 мс
Версия 1.6.0.21
Ответ 5
Я думаю, что этот текст в книге должен быть опечаткой, я думаю, что правильный контент ниже, я его исправляю:
=============================================== ====================
Вопрос: Каково время работы этого кода?
public String makeSentence(String[] words) {
String sentence = new String("");
for (String w : words) sentence+=W;
return sentence;
}
Ответ: O (n 2), где n - количество букв в предложении. Вот почему: каждый раз, когда вы добавляете строку в предложение, вы создаете копию предложения и просматриваете все буквы в предложении, чтобы скопировать их. Если вам нужно перебирать до n символов каждый раз в цикле, а вы зацикливаете не менее n раз, это дает вам время выполнения O (n 2). Ой! С помощью StringBuffer (или StringBuilder) вы можете избежать этой проблемы.
public String makeSentence(String[] words) {
StringBuffer sentence = new StringBuffer();
for (String w : words) sentence.append(w);
return sentence.toString();
}
=============================================== ======================
Я прав?
Ответ 6
Это действительно зависит от реализации StringBuffer
. Предполагая, что .append()
было постоянным временем, ясно, что у вас есть алгоритм O(n)
во времени, где n = length of the words array
. Если .append
не является постоянным временем, вам потребуется несколько ваших O (n) по сложности времени метода. Если в действительности текущая реализация StringBuffer
копирует строки по-символам, тогда алгоритм выше
Θ(n*m)
, или O(n*m)
, где n
- количество слов, а m
- средняя длина слова, а ваша книга неверна. Я предполагаю, что вы ищете строгую границу.
Простой пример неправильного ответа книги:
String[] words = ['alphabet']
По определению книги n=8
, поэтому алгоритм будет ограничен 64 шагами. Это так? Ясно не строго. Я вижу 1 назначение и 1 операцию копирования с n символами, поэтому вы получаете около 9 шагов. Такое поведение предсказывается границами O(n*m)
, как я проиллюстрировал выше.
Я сделал рытье, и это явно не простая копия персонажа. Похоже, что память копируется навалом, что возвращает нас к O(n)
, прежде всего, к решению.
/* StringBuffer is just a proxy */
public AbstractStringBuilder append(String str)
{
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
/* java.lang.String */
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, offset, dst, dstBegin, count);
}
Ваша книга старая, ужасная или и то, и другое. Я недостаточно разбираюсь в JDK-версиях, чтобы найти менее оптимальную реализацию StringBuffer, но, возможно, существует.
Ответ 7
В этой книге есть опечатка.
1-й случай:
public String makeSentence(String[] words) {
String sentence = new String();
for (String w : words) sentence += w;
return sentence;
}
Сложность: O (n ^ 2) → (n слов) x (n символов, скопированных на каждой итерации, для копирования текущего предложения в StringBuffer)
Второй случай:
public String makeSentence(String[] words) {
StringBuffer sentence = new StringBuffer();
for (String w : words) sentence.append(w);
return sentence.toString();
}
Сложность: O (n) → (n слов) x O (1) (амортизированная сложность для конкатенации StringBuffer)
Ответ 8
Как объяснение, данное в книге, навсегда слово в массиве строк создает новый объект предложения, и этот объект предложения сначала копирует предыдущее предложение, а затем проходит до конца массива и затем добавляет новое слово, следовательно, сложность n^2
.
- Сначала 'n', чтобы скопировать предыдущее предложение в новый объект
- Второй 'n', чтобы пройти этот массив, а затем добавить его
Следовательно, n*n
будет n^2
.
Ответ 9
Похож на O (n) на меня (с n
- общее количество букв во всех словах). Вы в основном повторяете каждый символ в words
, чтобы добавить его в StringBuffer
.
Единственный способ, которым я мог видеть это как O (n ^ 2), - это если append()
выполняет итерацию всего содержимого в буфере перед добавлением новых символов. И это может на самом деле делать это иногда, если количество символов превышает текущую выделенную длину буфера (ему нужно выделить новый буфер, а затем скопировать все из текущего буфера в новый буфер). Но это не произойдет на каждой итерации, поэтому вы все равно не будете иметь O (n ^ 2).
В лучшем случае у вас будет O (m * n), где m
- это количество раз, когда длина буфера увеличивается. И поскольку StringBuffer
будет удваивать свой размер буфера каждый раз, когда он выделяет более крупный буфер, мы можем определить, что m
примерно равно log2(n)
(фактически log2(n) - log2(16)
, поскольку размер начального буфера по умолчанию равен 16 вместо 1).
Итак, реальный ответ заключается в том, что пример книги - это O (n log n), и вы можете получить его до O (n), предварительно распределив StringBuffer
с достаточной емкостью для хранения всех ваших букв.
Обратите внимание, что в Java, добавляемом к строке с использованием +=
, проявляется неэффективное поведение, описанное в объяснении книги, поскольку оно должно выделять новую строку и копировать все данные из обеих строк в нее. Поэтому, если вы это сделаете, это O (n ^ 2):
String sentence = "";
for (String w : words) {
sentence += w;
}
Но использование StringBuffer
не должно генерировать то же поведение, что и в приведенном выше примере. Такого рода одна из основных причин существования StringBuffer
в первую очередь.
Ответ 10
Здесь мой расчет того, как они получили O (n ^ 2)
Мы проигнорируем время CPU для объявления StringBuffer, так как оно не зависит от размера финальной строки.
При вычислении сложности O мы имеем дело с наихудшим случаем, это произойдет, когда есть 1 строковые буквы. Я объясню после этого примера:
Скажем, у нас есть 4 однобуквенных строки: 'A', 'B', 'C', 'D'.
Читайте в A:
CPU-время, чтобы найти конец StringBuffer: 0
CPU-time для добавления "A": 1
Читайте в B:
CPU-время для поиска конца StringBuffer: 1
CPU-time для добавления "B": 1
Читайте на C:
CPU-time, чтобы найти конец StringBuffer: 2
CPU-time для добавления 'C': 1
Чтение в D:
CPU-время для поиска конца StringBuffer: 3
CPU-time для добавления "D": 1
CPU-time для копирования StringBuffer в String в конце: 4
Общее время CPU = 1 + 2 + 3 + 4 + 4
Если мы обобщим это на n 1-буквенных слов:
1 + 2 + 3 +...... + n + n = 0,5n (n + 1) + n
Я сделал это, используя формулу для суммы арифметической последовательности.
O (0,5n ^ 2 + 1,5n) = O (n ^ 2).
Если мы используем многобуквенные слова, нам придется найти конец StringBuffer реже, что приведет к более низкому времени CPU и "лучшему".