Что означает "постоянная" сложность? Время? Количество копий/ходов?

Я могу представить три операции в С++, которые могут быть описаны в некотором смысле как имеющие "постоянную" сложность. Я видел некоторые дебаты (*) по поводу этого, и мне кажется, что мы могли бы просто сказать: "все эти операции постоянны, но некоторые более постоянные, чем другие": -)

( Изменить 2: Если вы уже знаете, что знаете ответ, прочитайте некоторые из этих дебатов по этому вопросу, прежде чем поспешить слишком рано: Какие данные структура, точно, являются deques в С++? Многие люди с довольно высокими баллами спорят о том, что означает "постоянный". Мой вопрос не так очевиден, как вы думаете!)

(Изменить). Мне не нужен праймер в том, что означает "сложность". Я хочу получить четкий ответ, возможно, с кавычками из стандарта С++, который точно скажет, что должно быть постоянный. Процессорные тики, реальное время или что-то еще? В других потоках некоторые люди утверждали, что время совершенно не имеет отношения к тому, что требуется стандарту С++.)

Соответствуют ли эти требования сложности в стандарте для времени выполнения операции? Или они просто указывают (максимальное) количество копий/ходов, которые могут произойти из содержащихся объектов? И что именно означает "амортизация"?

1.. При заданном (непустом) vector<int> v, во время выполнения, очевидно, является постоянным:

swap(v.front(), v.back());

(хотя педант может указать, что это зависит от того, находятся ли данные в кеше или поменялись или что-то еще!).

2. Учитывая list<int> l, сделать push_back просто. Точно выделяется один новый элемент, и несколько указателей в связанном списке перетасовываются. Каждый push_front включает в себя одно распределение, всегда одного и того же объема памяти, поэтому это явно "постоянное". Но, конечно же, время для выделения может быть довольно переменным. Для управления памятью, возможно, потребуется много времени, чтобы найти подходящую свободную память.

3. Но сделать push_back на a vector<int> еще более непредсказуем. В большинстве случаев это будет очень быстро, но время от времени ему придется перераспределять пространство для всех данных и копировать каждый элемент в новое место. Таким образом, он менее предсказуем с точки зрения времени выполнения, чем один list::push_front, но он по-прежнему называется постоянным (амортизируется). В среднем, добавление большого количества данных в вектор займет сложность, которая не зависит от добавленной суммы, и именно поэтому она называется "амортизированным постоянным" временем. (Правильно?)

И, наконец, я спросил о int, чтобы избежать сложностей с другим типом. Например, a vector< vector<int> > может быть немного сложнее рассуждать о том, что каждый элемент вектора (векторов) может иметь разный размер и, например, замена двух элементов не такая постоянная, как в случае 1. выше. Но в идеале мы могли бы ответить за все vector<T>, а не только на T=int.

(*) Для примера обсуждений см. комментарии к этим ответам: Какая структура данных, в точности, является deques в С++?

Ответ 1

Сложность всегда указывается относительно конкретной переменной или набора переменных. Поэтому, когда стандарт говорит о постоянной вставке времени, речь идет о постоянном времени относительно количества элементов в списке. То есть, O (1) вставки означает, что количество элементов, находящихся в списке, не влияет на общую сложность вставок. Список может содержать 500 или 50000000 элементов, а сложность операции вставки будет одинаковой.

Например, std::list имеет O (1) вставки и удаления; количество элементов в списке не зависит от сложности вставок. Однако сложность распределения памяти может зависеть от количества вещей, которые уже были выделены. Но поскольку O (1) говорит о количестве элементов в списке, оно не распространяется на это. И это не предполагается, потому что тогда мы будем измерять сложность распределителя памяти, а не структуру данных.

Короче: это другое измерение.

Это означает, что мы можем реализовать наш алгоритм так сильно, как нам нравится, в том числе тот, где время на самом деле не является постоянным в каком-либо прагматическом смысле, но где мы соблюдали количество "операций" над содержащимися объектами.

Сложность не указана относительно реализаций. Он задан относительно алгоритмов. Не имеет значения, что контекст может переключаться, потому что время выполнения не является сложной задачей.

Как и выше, вы можете реализовать std::list с помощью распределителя памяти, который является O (log (n)) относительно делеций (где n - количество распределений). Однако удаление элемента в списке все равно будет O (1) по отношению к количеству элементов в списке.

Не путайте сложность с общей производительностью. Целью сложности является наличие общей метрики для алгоритмов относительно разных переменных. Цель программиста, который хочет, чтобы их код работал быстро, заключается в том, чтобы найти разумную реализацию алгоритма, который соответствует сложности, необходимой для достижения этой производительности.

Сложность - это инструмент для оценки эффективности алгоритма. Сложность не означает, что вы можете перестать думать.

А что именно означает "амортизация"?

Amortized означает следующее:

Если что-то "амортизируется X-временем", то, повторяя операцию бесконечно много раз в одной и той же структуре данных, ограничения сложности в X.

Итак, std::vector имеет "амортизированное постоянное время" вставки в задней части. Поэтому, если мы возьмем объект и выполним на нем бесконечное множество вставок, асимптотический предел сложности будет ничем не отличаться от вставки "постоянного времени".

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

Ответ 2

Насколько я понимаю, постоянная сложность означает, что операция O(1): вы можете заранее сказать, сколько элементарных операций (чтение/запись, инструкции по сборке, что угодно) будет выполняться. И эта оценка является общей границей для всех возможных состояний целевого объекта. Здесь есть улов: в многопоточной среде вы не можете предсказать потоковые коммутаторы, поэтому вы можете сделать некоторые рассуждения по истекшему времени работы только в ОС реального времени.

Об амортизированной постоянной сложности, это еще слабее. Составляя резюме ответов здесь, это гарантия того, что в среднем ваша операция является постоянной. Это означает, что число элементарных операций для N последующих операций O(N). Это означает, что число элементарных операций составляет около O(1), но допускает некоторые редкие прыжки. Например, добавление элемента к хвосту вектора обычно является постоянным, но иногда требуется дополнительная тяжелая операция; имеющее амортизированное постоянное время, означает, что дополнительная операция выполняется не так часто и занимает предсказуемое количество времени, так что общее время операции N все еще O(N). Конечно, здесь применяется и тот же улов.

Итак, отвечая на ваши вопросы:

  • Сложные гарантии стандарта применяются действительно только к числу инструкций машинного кода, необходимых для выполнения операции, и не подразумевают, что время выполнения каким-то образом ограничено. (Действительно, до недавнего времени на С++ даже не было указателя на тему, связанного с языком, поэтому со стандартной точки зрения С++ к этому времени программа была выполнена на специализированной С++-машине.)
  • Амортизация "ограничена константой в среднем", что обычно происходит в случае почти всегда постоянного ограниченного времени операции с некоторыми довольно редкими отклонениями.

Edit:
Вы можете посмотреть на, например, раздел 23.1 стандарта С++:

Все требования к сложности в этом разделе указаны исключительно в терминах количества операций над содержащимися объекты.

Ответ 3

если говорят, что операция имеет "постоянную сложность", в первую очередь это обычно относится к временной сложности. Я мог бы ссылаться на пространственную сложность, но если это так, то будет явно указано нормально.

Теперь сложность операции относится к тому, сколько времени для выполнения операции будет увеличиваться, когда количество элементов, обрабатываемых в операции, увеличивается. Для операции с постоянной сложностью функция будет занимать одинаковое количество времени, независимо от того, будут ли обработаны нулевые предметы или десять миллионов элементов.

Итак:

  • swap() - это постоянная сложность, потому что неважно, сколько элементов находится в векторе, операция займет столько же времени.

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

  • push_back() для вектора называется константой "амортизируется", поскольку в нормальном случае, когда перераспределение не должно происходить, количество времени, которое займет операция, не связано с тем, сколько элементов находится в вектор уже - такое же количество времени берется, чтобы добавить новый элемент к вектору нулевой длины относительно вектора длины 10 миллионов. Однако, если вектор необходимо перераспределить, необходимо будет иметь копию существующих элементов, и это не постоянная операция - это линейная операция. Но предполагается, что вектор должен быть сконструирован таким образом, чтобы перераспределение происходило нечасто, чтобы они могли быть amortized в течение многих операций push_back().

но сделать push_back на векторе еще более непредсказуем. В большинстве случаев это будет очень быстро, но время от времени ему придется перераспределять пространство для всех данных и копировать каждый элемент в новое место. Таким образом, он менее предсказуем с точки зрения времени выполнения, чем один список:: push_front, но он по-прежнему называется постоянным (амортизируется). В среднем, добавление большого количества данных в вектор займет сложность, которая не зависит от добавленной суммы, и именно поэтому она называется "амортизированным постоянным" временем. (Правильно?)

Ответ 4

Сложность O (1) - константа (с учетом временной сложности) означает, что время завершения алгоритма не связано с размером проблемы.

Итак, поиск значения в хешированной структуре - это O (1), потому что время, необходимое для этого, не зависит от количества его значений. Однако то же самое не относится к связанному списку, потому что мы должны сканировать значения (число которых изменяется по мере увеличения количества элементов), чтобы найти наше значение.

В случае 3, когда он копирует каждый элемент, это не операция O (1), а операция O (N) (но большую часть времени это O (1), поэтому она обычно постоянна). Амортизация учитывает это, отмечая, что алгоритм обычно завершается в O (1) раз и редко попадает в этот случай O (N).

Ответ 5

Постоянная сложность просто означает, что время выполнения операции не зависит от размера ввода. Он ничего не сообщает о затратах времени исполнения. Амортизированная постоянная сложность означает, что если вы выполняете последовательность операций, среднее время для каждой отдельной операции будет не зависящим от размера экземпляра проблемы.

Ответ 6

Один вопрос, который еще не упоминается в отношении сложности проблемы, заключается в том, что в целом подразумевается предположение о том, что размер машинного слова будет достаточно большим, чтобы сделать возможным решение, но не более. Например, если задан массив из N чисел в диапазоне 1..N, размер машинного слова считается достаточно большим для обработки значений в диапазоне 1..N, но нет никакой гарантии, что будет доступны любые "запасные" биты. Это важно, поскольку в то время как количество бит, необходимых для хранения массива, будет, конечно, O (NlgN), существует фундаментальное различие между алгоритмами, которые требуют O (NlgN) общего пространства, и те, которые требуют точно O (1) + N * lg (N) [округленное] пространство.