Существуют ли случаи, когда вы предпочитаете O(log n)
сложность по времени для O(1)
временной сложности? Или O(n)
до O(log n)
?
Есть ли у вас примеры?
Существуют ли случаи, когда вы предпочитаете O(log n)
сложность по времени для O(1)
временной сложности? Или O(n)
до O(log n)
?
Есть ли у вас примеры?
Может быть много причин предпочесть алгоритм с более высокой сложностью O-времени по сравнению с более низкой:
10^5
, лучше с точки зрения big-O, чем 1/10^5 * log(n)
(O(1)
против O(log(n)
), но для большинства разумных n
первое будет работать лучше. Например, лучшая сложность для умножения матриц - O(n^2.373)
но константа настолько высока, что никакие (насколько мне известно) вычислительные библиотеки не используют ее.O(n*log(n))
или O(n^2)
.O(log log N)
для поиска элемента, но есть также двоичное дерево, которое находит то же самое в O(log n)
. Даже для огромных чисел n = 10^20
разница незначительна.O(n^2)
и требует O(n^2)
памяти. Это может быть предпочтительнее O(n^3)
времени и O(1)
пространства, когда n не очень велико. Проблема в том, что вы можете подождать долгое время, но сильно сомневаетесь, что сможете найти достаточно большой объем ОЗУ, чтобы использовать его с вашим алгоритмом.O(n^2)
, хуже, чем быстрая сортировка или сортировка слиянием, но в качестве оперативного алгоритма она может эффективно сортировать список значений по мере их получения (как ввод данных пользователем), где большинство другие алгоритмы могут эффективно работать только с полным списком значений.Всегда существует скрытая константа, которая может быть ниже по алгоритму O (log n). Таким образом, он может работать быстрее на практике для реальных данных.
Есть также проблемы с пространством (например, работа на тостерах).
Кроме того, проблема времени разработчика - O (log n) может быть 1000 × проще для реализации и проверки.
Я удивлен, что никто еще не упомянул приложения, привязанные к памяти.
Может быть алгоритм, который имеет меньше операций с плавающей запятой либо из-за своей сложности (то есть O (1) < O (log n)), либо потому, что константа перед сложностью меньше (т.е. 2 n 2 < 6 n 2). Несмотря на это, вы можете по-прежнему предпочесть алгоритм с большим количеством FLOP, если более низкий алгоритм FLOP больше связан с памятью.
Что я подразумеваю под "привязкой к памяти", так это то, что вы часто получаете доступ к данным, которые постоянно находятся вне кэша. Чтобы извлечь эти данные, вам нужно вытащить память из вашего фактического пространства памяти в ваш кеш, прежде чем вы сможете выполнить свою операцию на нем. Этот шаг выборки часто довольно медленный - намного медленнее, чем ваша операция.
Следовательно, если вашему алгоритму требуется больше операций (все же эти операции выполняются с данными, которые уже находятся в кеше [и, следовательно, не требуется выборка]), он все равно будет выполнять ваш алгоритм с меньшим количеством операций (которые должны выполняться на данные из кэша [и, следовательно, требуют выборки]) с точки зрения фактического времени стены.
В условиях, когда безопасность данных является проблемой, более сложный алгоритм может быть предпочтительнее менее сложного алгоритма, если более сложный алгоритм имеет лучшее сопротивление методам синхронизации.
Алистра прибила его, но не представила никаких примеров, поэтому я буду.
У вас есть список из 10 000 кодов UPC для вашего магазина. 10-значный UPC, целое число по цене (цена в копейках) и 30 символов описания квитанции.
ПодходO (log N): у вас есть отсортированный список. 44 байта, если ASCII, 84, если Unicode. Альтернативно, рассматривайте UPC как int64, и вы получите 42 и 72 байта. 10 000 записей - в самом высоком случае вы немного под мегабайтом хранилища.
ПодходO (1): не храните UPC, вместо этого вы используете его как запись в массив. В самом низком случае вы просматриваете почти треть терабайта памяти.
Какой подход вы используете, зависит от вашего оборудования. В любой разумной современной конфигурации вы будете использовать подход log N. Я могу представить, что второй подход является правильным ответом, если по какой-то причине вы работаете в среде, где оперативная память критически короткая, но у вас много массового хранилища. Треть терабайта на диске не имеет большого значения, поэтому ваши данные в одном зонде диска стоят чего-то. В среднем простой двоичный подход занимает 13. (Обратите внимание, однако, что кластеризацией ваших ключей вы можете получить это до гарантированного 3 чтения, и на практике вы будете кэшировать первый.)
Рассмотрим красно-черное дерево. Он имеет доступ, поиск, вставку и удаление O(log n)
. Сравните с массивом, имеющим доступ к O(1)
, а остальные операции - O(n)
.
Таким образом, для приложения, в которое мы вставляем, удаляем или выполняем поиск чаще, чем доступ, и выбор между этими двумя структурами, мы предпочли бы красно-черное дерево. В этом случае вы можете сказать, что мы предпочитаем красно-черное дерево более громоздким временем доступа O(log n)
.
Почему? Потому что доступ не является нашей главной проблемой. Мы делаем компромисс: на производительность нашего приложения в большей степени влияют другие факторы, кроме этого. Мы позволяем этому конкретному алгоритму пострадать от производительности, потому что мы делаем большие выигрыши, оптимизируя другие алгоритмы.
Таким образом, ответ на ваш вопрос заключается в следующем: , когда скорость роста алгоритма не является тем, что мы хотим оптимизировать, когда мы хотим оптимизировать что-то еще. Все остальные ответы являются особыми случаями этого. Иногда мы оптимизируем время выполнения других операций. Иногда мы оптимизируем память. Иногда мы оптимизируем безопасность. Иногда мы оптимизируем ремонтопригодность. Иногда мы оптимизируем время разработки. Даже переопределяющая постоянная, которая достаточно мала для решения вопроса, оптимизирует время выполнения, когда вы знаете, что скорость роста алгоритма не оказывает наибольшего влияния на время выполнения. (Если ваш набор данных находился за пределами этого диапазона, вы бы оптимизировали скорость роста алгоритма, потому что он в конечном итоге будет доминировать в константе.) Все имеет стоимость, и во многих случаях мы торгуем стоимостью более высоких темпов роста для алгоритм для оптимизации чего-то еще.
Да.
В реальном случае мы выполнили некоторые тесты при выполнении поиска таблиц с помощью коротких и длинных строковых ключей.
Мы использовали std::map
, a std::unordered_map
с хешем, который сэмплирует не более 10 раз по длине строки (наши ключи имеют тенденцию быть ориентировочными, поэтому это прилично) и хэш, который показывает каждый символ (теоретически уменьшенные столкновения), несортированный вектор, где мы сравниваем ==
, и (если я правильно помню) несортированный вектор, в котором мы также храним хеш, сначала сравниваем хэш, а затем сравниваем символы.
Эти алгоритмы варьируются от O(1)
(unordered_map) до O(n)
(линейный поиск).
Для скромного размера N довольно часто O (n) избивает O (1). Мы подозреваем, что это связано с тем, что контейнеры на основе node потребовали, чтобы наш компьютер больше перескакивал в памяти, в то время как контейнеры на основе линейки не делали.
O(lg n)
существует между ними. Я не помню, как это было.
Разница в производительности была не такой большой, и на больших наборах данных хэш-балл выполнялся намного лучше. Таким образом, мы застряли с неупорядоченной картой на основе хешей.
На практике для разумного размера n, O(lg n)
составляет O(1)
. Если на вашем компьютере имеется только 4 миллиарда записей, то O(lg n)
ограничено выше 32
. (lg (2 ^ 32) = 32) (в информатике lg является короткой для логарифмической основы 2).
На практике алгоритмы lg (n) медленнее, чем O (1) алгоритмы не из-за логарифмического фактора роста, а потому, что часть lg (n) обычно означает, что алгоритм имеет определенный уровень сложности, и что сложность добавляет больший постоянный фактор, чем любой "рост" из lg (n).
Однако сложные алгоритмы O (1) (например, хеш-отображение) могут легко иметь одинаковый или больший постоянный множитель.
Возможность параллельного выполнения алгоритма.
Я не знаю, есть ли пример для классов O(log n)
и O(1)
, но для некоторых проблем вы выбираете алгоритм с более сложным классом, когда алгоритм проще выполнять параллельно.
Некоторые алгоритмы не могут быть распараллелены, но имеют такой класс низкой сложности. Рассмотрим другой алгоритм, который достигает одного и того же результата и может быть легко распараллелен, но имеет более высокий класс сложности. Когда выполняется на одной машине, второй алгоритм работает медленнее, но при выполнении на нескольких машинах реальное время выполнения становится все ниже и ниже, в то время как первый алгоритм не может ускоряться.
Предположим, вы внедряете черный список во встроенную систему, где числа от 0 до 1 000 000 могут быть внесены в черный список. Это оставляет вам две возможности:
Доступ к битете будет иметь гарантированный постоянный доступ. С точки зрения временной сложности, это оптимально. Как теоретический, так и практический вид точки (это O (1) с чрезвычайно низкими постоянными служебными данными).
Тем не менее, вы можете предпочесть второе решение. Особенно, если вы ожидаете, что число черных списков целых чисел будет очень маленьким, так как оно будет более эффективным с точки зрения памяти.
И даже если вы не разрабатываете встроенную систему, в которой недостаточно памяти, я могу увеличить произвольный лимит от 1 000 000 до 1 000 000 000 000 и сделать тот же аргумент. Тогда для битового набора потребуется около 125 ГБ памяти. Наличие гарантированной наихудшей сложности O (1) может не убедить вашего босса предоставить вам такой мощный сервер.
Здесь я бы предпочел бы бинарный поиск (O (log n)) или двоичное дерево (O (log n)) по набору O (1). И, вероятно, хэш-таблица со своей наихудшей сложностью O (n) на практике побьет их всех.
Мой ответ здесь Быстрый случайный взвешенный выбор по всем строкам стохастической матрицы является примером, когда алгоритм со сложностью O (m) быстрее, чем один со сложностью O (log (m)), когда m
не слишком большой.
Люди уже ответили на ваш точный вопрос, поэтому я займу немного другой вопрос, о котором люди могут подумать, когда приедут сюда.
Многие алгоритмы "O (1) time" фактически используют только ожидаемый O (1) раз, что означает, что их среднее время работы равно O (1), возможно, только в некоторые предположения.
Общие примеры: hashtables, расширение "списков массивов" (массивы/векторы с динамическим размером a.k.a.).
В таких сценариях вы можете использовать структуры данных или алгоритмы, время которых гарантировано абсолютно ограничено логарифмически, даже если они могут в среднем хуже.
Поэтому примером может служить сбалансированное двоичное дерево поиска, чье время работы в среднем хуже, но лучше в худшем случае.
Более общий вопрос заключается в том, что бывают ситуации, когда алгоритм O(f(n))
следует использовать алгоритму O(g(n))
, хотя g(n) << f(n)
как n
стремится к бесконечности. Как уже говорили другие, ответ явно "да" в случае, когда f(n) = log(n)
и g(n) = 1
. Иногда да, даже в случае, когда f(n)
является полиномиальным, но g(n)
является экспоненциальным. Известным и важным примером является метод Simplex Algorithm для решения задач линейного программирования. В 1970-х годах было показано, что O(2^n)
. Таким образом, его худшее поведение невозможно. Но - его поведение в среднем случае чрезвычайно хорошее, даже для практических проблем с десятками тысяч переменных и ограничений. В 1980-х годах были обнаружены полиномиальные алгоритмы времени (такие как Кармаркарский алгоритм внутренней точки) для линейного программирования, но через 30 лет алгоритм симплексов по-прежнему представляется алгоритмом выбор (за исключением некоторых очень больших проблем). Это по очевидной причине, что поведение в среднем случае часто более важно, чем поведение худшего случая, но также и по более тонкой причине, что симплекс-алгоритм в некотором смысле более информативен (например, информация о чувствительности легче извлечь).
Чтобы поставить 2 цента в:
Иногда вместо лучшего алгоритма выбирается худший алгоритм сложности, когда алгоритм работает в определенной аппаратной среде. Предположим, что наш алгоритм O (1) не последовательно обращается к каждому элементу очень большого массива фиксированного размера для решения нашей проблемы. Затем поместите этот массив на механический жесткий диск или магнитную ленту.
В этом случае алгоритм O (logn) (предположим, что он обращается к диску последовательно) становится более благоприятным.
Существует хороший вариант использования алгоритма O (log (n)) вместо алгоритма O (1), который игнорируют многочисленные другие ответы: неизменность. В хэш-картах O (1) помещает и получает, предполагая хорошее распределение хеш-значений, но они требуют изменчивого состояния. Неизменяемые карты деревьев имеют O (log (n)) puts and gets, что асимптотически медленнее. Однако неизменность может быть достаточно ценной, чтобы компенсировать худшую производительность, и в случае, когда необходимо сохранить несколько версий карты, неизменность позволяет избежать копирования карты, которая является O (n), и, следовательно, может улучшить производительность.
Просто: потому что коэффициент - затраты, связанные с настройкой, хранением и временем выполнения этого шага, - может быть намного больше, чем меньшая проблема с большим О, чем с более крупным. Big-O является лишь мерой масштабируемости алгоритмов.
Рассмотрим следующий пример из словаря хакеров, предложив алгоритм сортировки, основанный на Интерпретация множественных миров квантовой механики:
- Перенесите массив случайным образом с помощью квантового процесса,
- Если массив не отсортирован, уничтожьте юниверс.
- Все остальные юниверсы теперь отсортированы [включая ту, в которой вы находитесь].
(Источник: http://catb.org/~esr/jargon/html/B/bogo-sort.html)
Обратите внимание на то, что большой-O этого алгоритма O(n)
, который превосходит любой известный алгоритм сортировки на дату общих элементов. Коэффициент линейного шага также очень низок (поскольку это только сравнение, а не своп, которое выполняется линейно). Подобный алгоритм можно было бы использовать для решения любой проблемы как в NP, так и co-NP в полиномиальное время, так как каждое возможное решение (или возможное доказательство отсутствия решения) может быть сгенерировано с использованием квантового процесса, а затем проверено в полиномиальное время.
Однако в большинстве случаев мы, вероятно, не хотим рисковать тем, что множественные миры могут быть неправильными, не говоря уже о том, что действие второго шага по-прежнему остается "упражнением для читателя".
В любой точке, когда n ограничено, а константный множитель алгоритма O (1) выше, чем оценка на log (n). Например, сохранение значений в хэш-наборе равно O (1), но может потребовать дорогостоящего вычисления хэш-функции. Если элементы данных могут быть тривиально сопоставлены (по отношению к некоторому порядку), а оценка по n такова, что log n значительно меньше, чем вычисление хэша на любом одном элементе, то сохранение в сбалансированном двоичном дереве может быть быстрее, чем сохранение в hashset.
В ситуации реального времени, когда вам нужна твердая верхняя граница, вы должны выбрать, например. гепсорт, в отличие от Quicksort, потому что среднее поведение хапсортора также является наихудшим поведением.
Добавление к уже хорошим ответам. Практическим примером могут быть индексы Hash vs B-tree в базе данных postgres.
Индексы хеши формируют индекс хеш-таблицы для доступа к данным на диске, тогда как btree, как предлагает название, использует структуру данных Btree.
В режиме Big-O это O (1) vs O (logN).
Индексы хэшей в настоящее время не поощряются в postgres, поскольку в реальной ситуации, особенно в системах баз данных, достижение хэширования без столкновения очень сложно (может привести к сложности наихудшего случая O (N)), и из-за этого это еще больше сложнее сделать их аварийными с ошибками (так называемая запись вперед в журнале - WAL в postgres).
Этот компромисс сделан в этой ситуации, поскольку O (logN) достаточно хорош для индексов, а реализация O (1) довольно сложна, и разница во времени не имеет большого значения.
Когда n
мал, а O(1)
постоянно медленнее.
или
Это часто бывает для приложений безопасности, которые мы хотим разработать для проблем, алгоритмы которых медленны для того, чтобы кто-то не смог получить ответ на проблему слишком быстро.
Вот несколько примеров от верхней части головы.
O(2^n)
время, когда n
- длина бит ключа (это грубая сила).В другом месте в CS, Быстрая сортировка O(n^2)
в худшем случае, но в общем случае это O(n*log(n))
. По этой причине анализ "Big O" иногда - это не единственное, что вам нужно, анализируя эффективность алгоритма.