Что с O (1)?

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

В принципе, O (1) означает ограниченное постоянное время и (как правило) фиксированное пространство. Некоторые довольно фундаментальные операции - это O (1), хотя использование промежуточных языков и специальных виртуальных машин имеет тенденцию искажать мышление здесь (например, как амортизировать сборщик мусора и другие динамические процессы над тем, что в противном случае было бы O (1) деятельностью).

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

Хотя я заметил это раньше, пример только что появился в вопросе Pandincus "," Правильная коллекция для использования в O (1) времени в С#.NET? ".

Как я заметил там, единственная коллекция, которую я знаю об этом, обеспечивает доступ O (1) как гарантированную привязку - это фиксированный массив с целым индексом. Предполагается, что массив реализуется путем некоторого сопоставления с оперативной памятью, которая использует операции O (1), чтобы найти ячейку, имеющую этот индекс.

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

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

ВОПРОС: Вся эта подготовка действительно стоит вопроса. Какова случайность вокруг O (1) и почему она принимается так слепо? Известно ли, что даже O (1) может быть нежелательно большим, хотя и почти постоянным? Или O (1) просто присвоение понятия "вычислительная сложность" для неформального использования? Я озадачен.

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

Ответ 1

Я понимаю, что O (1) не обязательно постоянна; скорее, он не зависит от рассматриваемых переменных. Таким образом, хэш-поиск можно назвать O (1) относительно количества элементов в хеше, но не относительно длины хэшированных данных или отношения элементов к ведрам в хеше.

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

Ответ 2

Проблема в том, что люди действительно небрежны с терминологией. Здесь есть 3 важных, но разных класса:

O (1) наихудший вариант

Это просто: все операции занимают не более чем постоянное время в худшем случае, и поэтому во всех случаях. Доступ к элементу массива O(1) наихудший.

O (1) амортизированный наихудший случай

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

Например, стандартный Dynamic Array, который удваивает его емкость, когда он заполняется, требует O(1) амортизированного времени для вставки элемента в конце, хотя для некоторых вставок требуется время O(N) - всегда есть вставки O(1), в которые вставляются N элементов, всегда занимает общее время O(N).

O (1) средний случай

Этот самый сложный. Существует два возможных определения среднего случая: один для рандомизированных алгоритмов с фиксированными входами и один для детерминированных алгоритмов со случайными входами.

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

В другом случае нам нужно распределение вероятности по входам. Например, если бы мы измеряли алгоритм сортировки, одним из таких вероятностных распределений было бы распределение, которое имеет все N! возможные перестановки ввода одинаково вероятны. Затем среднее время работы - среднее время работы по всем возможным входам, взвешенное по вероятности каждого входа.

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

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

См. также Отказ от обслуживания по алгоритмической сложности. В этой статье авторы обсуждают, как они использовали некоторые недостатки в хэш-функциях по умолчанию, используемые двумя версиями Perl для генерации большого количества строк с хэш-коллизиями. Вооруженные этим списком строк, они сгенерировали атаку типа "отказ в обслуживании" на некоторых веб-серверах, подав им эти строки, что привело к наихудшему поведению O(N) в хэш-таблицах, используемых веб-серверами.

Ответ 3

O (1) означает постоянное время и (обычно) фиксированное пространство

Просто, чтобы прояснить это два отдельных заявления. Вы можете иметь O (1) во времени, но O (n) в пространстве или что-то еще.

Известно ли, что даже O (1) может быть нежелательно большим, хотя почти постоянная?

O (1) может быть непрактично ОГРОМНЫМ, и он все еще O (1). Часто пренебрегают тем, что, если вы знаете, что у вас будет очень маленький набор данных, константа важнее, чем сложность, а для достаточно небольших наборов данных - баланс двух. Алгоритм O (n!) Может выполнять O (1), если константы и размеры наборов данных имеют соответствующий масштаб.

Обозначение

O() - это мера сложности - не время, которое примет алгоритм, или чистая мера того, как "хороший" данный алгоритм для данной цели.

Ответ 4

Я вижу, что вы говорите, но я думаю, что существует пара базовых предположений, лежащих в основе утверждения о том, что look-ups в таблице Hash имеют сложность O (1).

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

Худшая сложность поиска таблицы Hash - O (n), но это крайне маловероятно, учитывая приведенные выше предположения.

Ответ 5

Hashtables - это структура данных, которая поддерживает поиск и вставку O (1).

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

Поскольку вставка и поиск зависят только от результата хэш-функции, а не от размера хеш-таблицы или количества сохраненных элементов, хэш-таблица имеет вложение и поиск O (1).

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

Когда происходит хеш-столкновение, поиск или вставка не могут быть выполнены в течение O (1). Тем не менее, хорошие алгоритмы разрешения конфликтов могут уменьшить количество попыток найти другое доступное пустое место или увеличить размер хэш-таблицы, может уменьшить количество столкновений в первую очередь. p >

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


Посмотрим на пример. Пусть используется хэш-таблица для хранения следующих пар (key, value):

  • (Name, Bob)
  • (Occupation, Student)
  • (Location, Earth)

Мы реализуем back-end хэш-таблицы с массивом из 100 элементов.

key будет использоваться для определения элемента массива для хранения пары (key, value). Для определения элемента используется hash_function:

  • hash_function("Name") возвращает 18
  • hash_function("Occupation") возвращает 32
  • hash_function("Location") возвращает 74.

Из приведенного выше результата мы назначим пары (key, value) в элементы массива.

array[18] = ("Name", "Bob")
array[32] = ("Occupation", "Student")
array[74] = ("Location", "Earth")

Вставка требует только использования хеш-функции и не зависит от размера хеш-таблицы или ее элементов, поэтому ее можно выполнить в O (1) раз.

Аналогично, поиск элемента использует хеш-функцию.

Если мы хотим найти ключ "Name", мы выполним hash_function("Name"), чтобы узнать, какой элемент в массиве находится в нужном значении.

Кроме того, поиск не зависит от размера хеш-таблицы и количества хранимых элементов, поэтому операция O (1).

Все хорошо. Попробуйте добавить дополнительную запись ("Pet", "Dog"). Однако есть проблема, поскольку hash_function("Pet") возвращает 18, что является тем же самым хэшем для клавиши "Name".

Следовательно, нам нужно решить эту хэш-коллизию. Предположим, что используемая нами функция разрешения столкновений хешей обнаружила, что новый пустой элемент 29:

array[29] = ("Pet", "Dog")

Так как в этой вставке было столкновение хэшей, наша производительность была не совсем O (1).

Эта проблема также возникает при попытке найти ключ "Pet", поскольку попытка найти элемент, содержащий ключ "Pet", выполнив hash_function("Pet"), всегда будет возвращать 18 изначально.

Как только мы посмотрим на элемент 18, мы найдем ключ "Name", а не "Pet". Когда мы найдем эту несогласованность, нам нужно будет разрешить столкновение, чтобы получить правильный элемент, содержащий фактический ключ "Pet". Снятие хеш-коллизии является дополнительной операцией, которая делает хеш-таблицу не выполняемой в O (1) раз.

Ответ 6

Я не могу говорить с другими обсуждениями, которые вы видели, но есть хотя бы один алгоритм хеширования, гарантированный O (1).

Хеширование кукушки поддерживает инвариант, так что в хэш-таблице нет цепочки. Вставка амортизируется O (1), поиск всегда равен O (1). Я никогда не видел его реализации, это то, что было недавно обнаружено, когда я учился в колледже. Для относительно статических наборов данных он должен быть очень хорошим O (1), так как он вычисляет две хеш-функции, выполняет два поиска и сразу же знает ответ.

Помните, это предполагает, что вычисление хеширования равно O (1). Вы можете утверждать, что для строк длины K любой хэш минимально O (K). В действительности вы можете легко связать K, скажем K < 1000. O (K) ~ = O (1) для K < 1000.

Ответ 7

Может возникнуть концептуальная ошибка в отношении того, как вы понимаете нотацию Big-Oh. Это означает, что, учитывая алгоритм и набор входных данных, верхняя граница времени выполнения алгоритма зависит от значения O-функции, когда размер набора данных стремится к бесконечности.

Когда говорят, что алгоритм принимает время O (n), это означает, что время выполнения для наихудшего случая алгоритма линейно зависит от размера входного набора.

Когда алгоритм принимает время O (1), единственное, что он имеет в виду, это то, что при заданной функции T (f), которая вычисляет время выполнения функции f (n), существует натуральное положительное число k такое, что T (f) k для любого входа n. По сути, это означает, что верхняя граница времени выполнения алгоритма не зависит от его размера и имеет фиксированный конечный предел.

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

Например, поиск экземпляра значения в связанном списке является операцией O (n). Но если я скажу, что список имеет не более 8 элементов, то O (n) становится O (8), становится O (1).

В этом случае мы использовали структуру данных trie как словарь (дерево символов, где лист node содержит значение для строки, используемой в качестве ключа), если ключ ограничен, то время его поиска можно считать O (1) (если я определяю поле символов как имеющее не более k символов в длину, что может быть разумным предположением для многих случаев).

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

В заключение, O (1) время может быть переоценено для многих вещей. Для больших структур данных сложность адекватной хеш-функции может быть не тривиальной, и существуют достаточные угловые случаи, когда количество столкновений приводит к тому, что она ведет себя как структура данных O (n), и переименование может стать чрезмерно дорогостоящим. В этом случае структура O (log (n)), такая как AVL или B-дерево, может быть превосходной альтернативой.

Ответ 8

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


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

Ответ 9

В целом, я думаю, что люди используют их сравнительно без учета точности. Например, структуры данных на основе хэша - это O (1) (средний), если они хорошо разработаны, и у вас хороший хеш. Если все хэши для одного ведра, то это O (n). Как правило, хотя один использует хороший алгоритм, и ключи разумно распределены, поэтому удобно говорить об этом как O (1) без всякой квалификации. Аналогично со списками, деревьями и т.д. Мы имеем в виду определенные реализации, и просто говорить о них проще, обсуждая общие положения, без квалификации. Если, с другой стороны, мы обсуждаем конкретные реализации, то, вероятно, это будет более точно.

Ответ 10

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

Ответ 11

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

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

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

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

Кроме того, помимо проблемы с копией и компактностью по сравнению с меткой и разверткой, детали реализации могут существенно повлиять на возникающие сложности:

  • Инкрементальные сборщики мусора, которые отслеживают грязные кусочки и т.д., могут полностью исключить эти большие повторные обходы.
  • Это зависит от того, работает ли ваш GC периодически на основе времени настенных часов или работает пропорционально количеству распределений.
  • Независимо от того, является ли алгоритм стиля метки и развертки параллельным или stop-the-world
  • Отмечает ли он новые выделения черного цвета, если он оставляет их белыми до тех пор, пока они не упадут в черный контейнер.
  • Независимо от того, допускает ли ваш язык модификации указателей, некоторые сборщики мусора могут работать за один проход.

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

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

Ответ 12

Я думаю, что когда многие люди бросают вокруг термина "O (1)", они неявно имеют в виду "небольшую" константу, независимо от того, что "малое" означает в их контексте.

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

Ответ 13

Реализации таблиц Hash на практике не используются "точно" O (1), если вы их протестируете, вы обнаружите, что они усредняют около 1,5 поисков, чтобы найти заданный ключ в большом наборе данных

(из-за того, что происходят столкновения DO, а при столкновении необходимо назначить другое место)

Кроме того, на практике HashMaps поддерживаются массивами с начальным размером, который "вырос" до двойного размера, когда он достигает 70% заполненности в среднем, что дает относительно хорошее пространство адресации. После 70% -ного уровня полноты столкновений растут быстрее.

Теория Big O утверждает, что если у вас есть алгоритм O (1) или даже алгоритм O (2), то решающим фактором является степень отношения между размером входного набора и шагами для вставки/извлечения одного из них, O (2) все еще является постоянным временем, поэтому мы просто аппроксимируем его как O (1), потому что это означает более или менее то же самое.

В действительности, существует только один способ иметь "идеальную хэш-таблицу" с O (1), и для этого требуется:

  • Генератор ключей Perfect Perfect Hash
  • Неограниченное пространство адресации.

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

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

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

Суффиксная заметка Примечание. Здесь я использую O (1.5) и O (2). На самом деле этого не существует в big-o. Это просто то, что люди, которые не знают, что большой, предполагают, являются обоснованием.

Если что-то занимает 1,5 шага, чтобы найти ключ, или 2 шага, чтобы найти этот ключ, или 1 шаг, чтобы найти этот ключ, но количество шагов не превышает 2 и требуется ли 1 шаг или 2 полностью случайным образом, тогда он по-прежнему большой-O O (1). Это связано с тем, что независимо от того, сколько элементов вы добавляете к размеру набора данных, он по-прежнему поддерживает < 2 шага. Если для всех таблиц > 500 ключей требуется 2 шага, вы можете предположить, что эти 2 шага на самом деле одношаговые с 2 частями,... которые все еще O (1).

Если вы не можете сделать это предположение, то вы не будете думать о Big-O вообще, потому что тогда вы должны использовать число, которое представляет количество конечных вычислительных шагов, необходимых для выполнения всего, и "одношаговый" не имеет смысла тебе. Просто поймите в голову, что нет прямой корреляции между Big-O и количеством задействованных циклов выполнения.