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

В чем сложность этого простого кода?

Я вставляю этот текст из книги, которую у меня есть. Он говорит о сложности, если 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). Ой!

Может кто-нибудь объяснить этот ответ более четко?

4b9b3361

Ответ 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 и "лучшему".