Нотация Big-O (также называемая нотацией "асимптотического роста") - это то, на что "похожи" функции, когда вы игнорируете постоянные факторы и прочее около начала координат . Мы используем это, чтобы говорить о как вещь масштабируется .
Основы
для "достаточно" больших входов ...
f(x) ∈ O(upperbound)
означает f
"растет не быстрее" upperbound
f(x) ∈ Ɵ(justlikethis)
означает f
"растет точно так же как" justlikethis
f(x) ∈ Ω(lowerbound)
означает f
"растет не медленнее, чем" lowerbound
нотация big-O не заботится о постоянных факторах: говорят, что функция 9x²
«растет точно так же» 10x²
. Биг-O асимптотическая нотация не заботится о неасимптотических вещах («вещи рядом с источником» или «что происходит, когда размер проблемы мал»): функция 10x²
Говорят, "расти точно так же, как" 10x² - x + 2
.
Почему вы хотите игнорировать меньшие части уравнения? Потому что они сильно затмеваются большими частями уравнения, если учесть все большие и большие масштабы; их вклад становится незначительным и не имеет значения. (См. Пример раздела.)
Другими словами, все зависит от отношения , когда вы идете в бесконечность. Если вы поделите фактическое время, которое требуется на O(...)
, вы получите постоянный фактор в пределе больших входных данных. Интуитивно это имеет смысл: функции «масштабируются как» друг друга, если вы можете умножить одно чтобы получить другой. То есть когда мы говорим ...
actualAlgorithmTime(N) ∈ O(bound(N))
e.g. "time to mergesort N elements
is O(N log(N))"
... это означает, что для "достаточно больших" размеров задач N (если мы игнорируем материал около начала координат), существует некоторая постоянная (например, 2,5, полностью составленная ) такой что:
actualAlgorithmTime(N) e.g. "mergesort_duration(N) "
────────────────────── < constant ───────────────────── < 2.5
bound(N) N log(N)
Есть много вариантов констант; часто «лучший» выбор известен как «постоянный фактор» алгоритма ... но мы часто игнорируем его, как игнорируем не самые большие термины (см. раздел «Постоянные факторы», почему они обычно не имеют значения). Вы также можете думать о приведенном выше уравнении как о границе, говоря: « В худшем случае время, которое оно занимает, никогда не будет хуже, чем примерно N*log(N)
, с коэффициентом 2,5 (постоянный фактор, который мы не не волнует)".
В общем, O(...)
является наиболее полезным, потому что мы часто заботимся о худшем поведении. Если f(x)
представляет что-то «плохое», например, использование процессора или памяти, то «f(x) ∈ O(upperbound)
» означает «upperbound
- наихудший сценарий использования процессора / памяти».
Приложения
В качестве чисто математической конструкции нотация big-O не ограничивается разговорами о времени обработки и памяти. Вы можете использовать это, чтобы обсудить асимптотику всего, где масштабирование имеет смысл, например:
- число возможных рукопожатий среди
N
людей на вечеринке (Ɵ(N²)
, в частности N(N-1)/2
, но важно то, что оно "масштабируется как" N²
)
- вероятностное ожидаемое количество людей, которые рассматривают вирусный маркетинг как функцию времени
- как масштабируется задержка веб-сайта в зависимости от количества процессорных блоков в CPU, GPU или компьютерном кластере
- как тепловая мощность на процессоре умирает в зависимости от количества транзисторов, напряжения и т. Д.
- сколько времени требуется алгоритму для выполнения, в зависимости от размера ввода
- сколько места нужно запустить алгоритму в зависимости от размера ввода
Пример * ** тысяча девяносто-восемь * тысяча девяносто девять
В приведенном выше примере рукопожатия все в комнате пожимают друг другу руки. В этом примере #handshakes ∈ Ɵ(N²)
. Почему?
Сделайте резервную копию: количество рукопожатий в точности равно n-выбрать-2 или N*(N-1)/2
(каждый из N человек пожимает руки другим людям по N-1, но это удваивает количество рукопожатий, так что разделите на 2):
Тем не менее, для очень большого числа людей линейный член N
затмевается и фактически дает 0 к отношению (на диаграмме: доля пустых прямоугольников по диагонали по отношению к общему количеству блоков уменьшается по мере увеличения количества участников). становится больше). Следовательно, масштабирующее поведение равно order N²
, или количество рукопожатий «растет как N²».
#handshakes(N)
────────────── ≈ 1/2
N²
Как будто пустых полей на диагонали диаграммы (N * (N-1) / 2 галочки) даже не было (N 2 галочки асимптотически).
(временное отступление от «простого английского» :) Если вы хотите доказать это себе, вы можете выполнить некоторую простую алгебру отношения, чтобы разделить его на несколько терминов (lim
означает «рассматривается в пределе»). просто проигнорируйте его, если вы его еще не видели, это просто обозначение «а N действительно очень большой»):
N²/2 - N/2 (N²)/2 N/2 1/2
lim ────────── = lim ( ────── - ─── ) = lim ─── = 1/2
N→∞ N² N→∞ N² N² N→∞ 1
┕━━━┙
this is 0 in the limit of N→∞:
graph it, or plug in a really large number for N
tl; dr: количество рукопожатий «выглядит как» x² настолько для больших значений, что если бы мы записали соотношение # handshakes / x², тот факт, что нам не нужно ровно x² рукопожатия даже не будут отображаться в десятичном виде в течение сколь угодно большого времени.
например. для x = 1 миллион, отношение # рукопожатия / x²: 0,499999 ...
Интуиция здания
Это позволяет нам делать заявления вроде ...
"Для достаточно большого входного размера = N, независимо от того, каков постоянный коэффициент, если I double размер ввода ...
- ... Я удваиваю время, затрачиваемое алгоритмом O (N) ("линейное время"). "
- ... Я удваиваю квадратичное (четырехкратное) время, затрачиваемое алгоритмом O (N²) («квадратичное время»). " (например, задача 100x как большая занимает 100² = 10000x как долго ... возможно неустойчивые)
- ... Я в двойном кубе (восьмеричное) время, которое занимает алгоритм O (N³) ("кубическое время"). " (например, задача 100x большого размера занимает 100³ = 1000000x длинного ... очень неустойчивые)
- ... Я добавляю фиксированную сумму ко времени, которое занимает алгоритм O (log (N)) ("логарифмическое время"). " (дешево!)
- ... Я не меняю время, которое занимает алгоритм O (1) («постоянное время»). " (самый дешевый!)
- ... I «(в основном) удваивает» время, которое занимает алгоритм O (N log (N)). » (довольно часто)
- ... Я нелепо увеличиваю время, которое занимает алгоритм O (2 N ) ("экспоненциальное время"). " (вы удвоите (или утроите и т. Д.) Время просто увеличив проблему на единицу)
[для математически наклонных вы можете навести курсор мыши на спойлеры для незначительных признаков]
(с указанием https://stackoverflow.com/a/487292/711085)
(технически постоянный фактор может иметь значение в некоторых более эзотерических примерах, но я сформулировал выше (например, в журнале (N)), что это не так)
Это порядки роста, которые программисты и прикладные компьютерные ученые используют как ориентиры. Они видят это все время. (Таким образом, хотя вы могли бы технически подумать: «Удвоение ввода делает алгоритм O (√N) в 1,414 раз медленнее», лучше думать об этом как «это хуже, чем логарифмический, но лучше, чем линейный».)
Постоянные факторы
Обычно нас не волнует, каковы конкретные постоянные факторы, потому что они не влияют на рост функции. Например, два алгоритма могут занять O(N)
время для завершения, но один может быть в два раза медленнее, чем другой. Нас, как правило, не волнует слишком много, если коэффициент не очень велик, поскольку оптимизация - сложная задача ( Когда оптимизация преждевременна? ); Кроме того, простое действие выбора алгоритма с лучшим значением big-O часто повышает производительность на порядки.
НекоторыеАсимптотически превосходящие алгоритмы (например, несравниваемые O(N log(log(N)))
сортировки) могут иметь настолько большой постоянный коэффициент (например, 100000*N log(log(N))
) или относительно большие издержки, как O(N log(log(N)))
со скрытым + 100*N
, что они редко стоят использование даже на «больших данных».
Почему O (N) иногда является лучшим, что вы можете сделать, то есть, почему нам нужны структуры данных
O(N)
алгоритмы в некотором смысле являются «лучшими» алгоритмами, если вам нужно прочитать все ваши данные. сам акт чтения группы данных является O(N)
операцией. Загрузка его в память обычно O(N)
(или быстрее, если у вас есть аппаратная поддержка, или совсем нет времени, если вы уже прочитали данные). Однако, если вы коснетесь или даже посмотрите на каждый фрагмент данных (или даже на любой другой фрагмент данных), вашему алгоритму потребуется O(N)
время для выполнения этого просмотра. Не важно, сколько времени займет ваш фактический алгоритм, он будет по крайней мере O(N)
, потому что он потратил время на просмотр всех данных.
То же самое можно сказать и о самом акте письма . Все алгоритмы, которые распечатывают N вещей, будут занимать N времени, потому что выходной результат, по крайней мере, такой длинный (например, распечатка всех перестановок (способов перестановки) набора из N игральных карт является факториальной: O(N!)
).
Это мотивирует использование структур данных : структура данных требует чтения данных только один раз (обычно O(N)
раз), плюс некоторая произвольная величина предварительной обработки (например, O(N)
или O(N log(N))
или O(N²)
), который мы стараемся держать маленьким. После этого изменение структуры данных (вставки / удаления / и т. Д.) И выполнение запросов к данным занимают очень мало времени, например O(1)
или O(log(N))
. Затем вы приступаете к выполнению большого количества запросов! В целом, чем больше работы вы готовы выполнить раньше времени, тем меньше работы вам придется выполнять позже.
Например, скажем, у вас были координаты широты и долготы миллионов отрезков дорог, и вы хотели найти все перекрестки улиц.
- Наивный метод: если у вас есть координаты пересечения улиц и вы хотите исследовать близлежащие улицы, вам придется каждый раз проходить миллионы отрезков и проверять каждый на смежность.
- Если вам нужно было сделать это только один раз, не было бы проблемой, если бы наивный метод
O(N)
работал только один раз, но если вы хотите сделать это много раз (в данном случае, N
раз, по одному разу для каждого сегмента), нам придется выполнить O(N²)
работы или 1000000² = 1000000000000 операций. Не хорошо (современный компьютер может выполнять около миллиарда операций в секунду).
- Если мы используем простую структуру, называемую хеш-таблицей (таблица быстрого поиска, также известная как хэш-карта или словарь), мы платим небольшую стоимость, предварительно обрабатывая все за
O(N)
времени. После этого в среднем требуется только постоянное время, чтобы найти что-то по его ключу (в этом случае наш ключ - это координаты широты и долготы, округленные в сетку; мы ищем соседние сеточные пространства, которых всего 9, что является константа).
- Наша задача перешла от неосуществимого
O(N²)
к управляемому O(N)
, и все, что нам нужно было сделать, это заплатить небольшую плату за создание хеш-таблицы.
- аналогия : аналогия в данном конкретном случае представляет собой головоломку: мы создали структуру данных, которая использует некоторые свойства данных. Если наши сегменты дороги похожи на кусочки головоломки, мы группируем их по цвету и рисунку. Затем мы используем это, чтобы не выполнять дополнительную работу позже (сравнивая кусочки головоломки одинакового цвета друг с другом, а не со всеми остальными кусочками головоломки).
Мораль этой истории: структура данных позволяет нам ускорить работу. Даже более продвинутые структуры данных позволяют невероятно умным образом комбинировать, задерживать или даже игнорировать операции. Разные проблемы будут иметь разные аналогии, но все они будут включать в себя организацию данных таким образом, чтобы использовать некоторую структуру, которая нам небезразлична, или которую мы искусственно навязали ей для учета. Мы работаем раньше времени (в основном, планируя и организуя), и теперь повторять задачи намного проще!
Практический пример: визуализация порядка роста при кодировании
Асимптотическая запись по своей сути совершенно отделена от программирования. Асимптотическая нотация - это математическая основа для размышлений о масштабировании вещей, которая может использоваться во многих различных областях. Тем не менее ... вот как вы применяете асимптотические обозначения к кодированию.
Основы: всякий раз, когда мы взаимодействуем с каждым элементом в коллекции размера A (таким как массив, набор, все ключи карты и т. Д.), Или выполняем итерации цикла, то есть множитель размера A. Почему я говорю «мультипликативный фактор»? - потому что циклы и функции (почти по определению) имеют мультипликативное время выполнения: количество итераций, время работы, выполненной в цикле (или для функций: количество раз Вы вызываете функцию, время работы сделано в функции). (Это справедливо, если мы не делаем ничего сложного, например, пропускаем циклы или рано выходим из цикла, или меняем поток управления в функции на основе аргументов, что очень часто встречается.) Вот несколько примеров методов визуализации с сопровождающим псевдокодом.
(здесь x
представляют единицы работы в постоянном времени, инструкции процессора, коды операций интерпретатора и т. Д.)
for(i=0; i<A; i++) // A * ...
some O(1) operation // 1
--> A*1 --> O(A) time
visualization:
|<------ A ------->|
1 2 3 4 5 x x ... x
other languages, multiplying orders of growth:
javascript, O(A) time and space
someListOfSizeA.map((x,i) => [x,i])
python, O(rows*cols) time and space
[[r*c for c in range(cols)] for r in range(rows)]
Пример 2:
for every x in listOfSizeA: // A * (...
some O(1) operation // 1
some O(B) operation // B
for every y in listOfSizeC: // C * (...
some O(1) operation // 1))
--> O(A*(1 + B + C))
O(A*(B+C)) (1 is dwarfed)
visualization:
|<------ A ------->|
1 x x x x x x ... x
2 x x x x x x ... x ^
3 x x x x x x ... x |
4 x x x x x x ... x |
5 x x x x x x ... x B <-- A*B
x x x x x x x ... x |
................... |
x x x x x x x ... x v
x x x x x x x ... x ^
x x x x x x x ... x |
x x x x x x x ... x |
x x x x x x x ... x C <-- A*C
x x x x x x x ... x |
................... |
x x x x x x x ... x v
Пример 3:
function nSquaredFunction(n) {
total = 0
for i in 1..n: // N *
for j in 1..n: // N *
total += i*k // 1
return total
}
// O(n^2)
function nCubedFunction(a) {
for i in 1..n: // A *
print(nSquaredFunction(a)) // A^2
}
// O(a^3)
Если мы сделаем что-то немного сложное, вы все равно сможете визуально представить, что происходит:
for x in range(A):
for y in range(1..x):
simpleOperation(x*y)
x x x x x x x x x x |
x x x x x x x x x |
x x x x x x x x |
x x x x x x x |
x x x x x x |
x x x x x |
x x x x |
x x x |
x x |
x___________________|
Здесь важен наименьший узнаваемый контур, который вы можете нарисовать; треугольник - это двумерная форма (0,5 A ^ 2), точно так же, как квадрат - это двумерная форма (A ^ 2); постоянный множитель два здесь остается в асимптотическом соотношении между ними, однако мы игнорируем его, как и все факторы ... (У этой техники есть некоторые прискорбные нюансы, которые я здесь не рассматриваю; она может ввести вас в заблуждение.)
Конечно, это не значит, что циклы и функции плохие; напротив, они являются строительными блоками современных языков программирования, и мы их любим. Однако мы видим, что способ, которым мы плетем циклы, функции и условия вместе с нашими данными (поток управления и т. Д.), Имитирует использование нашей программы во времени и пространстве! Если использование времени и пространства становится проблемой, то тогда мы прибегаем к хитрости и находим простой алгоритм или структуру данных, которые мы не рассматривали, чтобы каким-то образом уменьшить порядок роста. Тем не менее, эти методы визуализации (хотя они не всегда работают) могут дать вам наивное предположение в худшем случае.
Вот еще одна вещь, которую мы можем распознать визуально:
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x
x x x x x x x x
x x x x
x x
x
Мы можем просто переставить это и увидеть, что это O (N):
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x|x x x x x x x x|x x x x|x x|x
Или, может быть, вы делаете log (N) проходов данных, за O (N * log (N)) общее время:
<----------------------------- N ----------------------------->
^ x x x x x x x x x x x x x x x x|x x x x x x x x x x x x x x x x
| x x x x x x x x|x x x x x x x x|x x x x x x x x|x x x x x x x x
lgN x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x
| x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x
v x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x
Безотносительно, но стоит упомянуть еще раз: если мы выполняем хеш (например, поиск по словарю / хеш-таблице), это является фактором O (1). Это довольно быстро.
[myDictionary.has(x) for x in listOfSizeA]
\----- O(1) ------/
--> A*1 --> O(A)
Если мы делаем что-то очень сложное, например, с помощью рекурсивной функции или алгоритма «разделяй и властвуй», , вы можете использовать Основную теорему (обычно работает) или в смешных случаях Akra Теорема Бацци (почти всегда работает) вы смотрите время выполнения вашего алгоритма в Википедии.
Но программисты так не думают, потому что в конечном итоге интуиция алгоритма просто становится второй натурой. Вы начнете кодировать что-то неэффективное и сразу же подумаете: «Я что-то делаю крайне неэффективно? ». Если ответ «да» И вы предвидите, что это действительно имеет значение, тогда вы можете сделать шаг назад и подумать о различных хитростях, чтобы заставить вещи работать быстрее (ответ почти всегда «использовать хеш-таблицу», редко «использовать дерево», и очень редко что-то более сложное).
Амортизированная и средняя сложность случая
Существует также понятие «амортизированный» и / или «средний случай» (обратите внимание, что они разные).
Средний регистр : Это не более, чем использование нотации big-O для ожидаемого значения функции, а не самой функции. В обычном случае, когда вы считаете, что все входные данные одинаково вероятны, средний случай - это просто среднее время выполнения. Например, для быстрой сортировки, даже если наихудший случай O(N^2)
для некоторых действительно плохих входных данных, средний случай - обычный O(N log(N))
(действительно плохих входных данных очень мало, поэтому их мало, и мы их не замечаем) в среднем случае).
Амортизированный наихудший случай : У некоторых структур данных сложность наихудшего случая может быть большой, но гарантируйте, что при выполнении многих из этих операций средний объем выполняемой вами работы будет лучше, чем худший случай. Например, у вас может быть структура данных, которая обычно занимает постоянное O(1)
время. Тем не менее, иногда он будет «икать» и займет O(N)
времени для одной случайной операции, потому что, возможно, ему нужно будет провести какую-то бухгалтерию или сборку мусора или что-то в этом роде ... но он обещает вам, что если он икнет, он не будет икать снова для N больше операций. В худшем случае стоимость по-прежнему составляет O(N)
на операцию, но амортизированная стоимость на протяжении многих прогонов составляет O(N)/N
= O(1)
на операцию. Поскольку крупные операции достаточно редки, можно считать, что огромное количество случайных работ сливается с остальной работой как постоянный фактор. Мы говорим, что работа «амортизируется» из-за достаточно большого числа вызовов, что она асимптотически исчезает.
Аналогия для амортизированного анализа:
Вы водите машину. Иногда вам нужно потратить 10 минут на
АЗС, а затем потратить 1 минуту, заправляя бак газом.
Если вы делали это каждый раз, когда ездили на машине куда-либо (потратить 10
минут езды до заправки, потратьте несколько секунд на заполнение
доля галлона), это было бы очень неэффективно. Но если вы заполните
один раз в несколько дней, 11 минут потратили на
АЗС «амортизируется» за достаточно большое количество поездок,
что вы можете игнорировать это и делать вид, что все ваши поездки были, возможно, на 5% длиннее.
Сравнение среднего и амортизированного наихудшего случая:
- Средний случай: мы делаем некоторые предположения о наших входных данных; то есть, если наши входы имеют разные вероятности, то наши выходы / время выполнения будут иметь разные вероятности (из которых мы берем среднее значение). Обычно мы предполагаем, что все наши входные данные одинаково вероятны (равномерная вероятность), но если реальные входные данные не соответствуют нашим предположениям о «среднем входном сигнале», вычисления среднего выходного значения / времени выполнения могут быть бессмысленными. Однако если вы ожидаете равномерно случайных входных данных, об этом полезно подумать!
- Амортизированный наихудший случай: если вы используете амортизированную структуру данных наихудшего случая, производительность гарантированно будет находиться в пределах амортизированного наихудшего случая ... в конце концов (даже если входные данные выбираются злым демоном, который знает все и пытается напортачить) Обычно мы используем это для анализа алгоритмов, которые могут быть очень «нестабильными» по производительности с неожиданными большими сбоями, но со временем работают так же, как и другие алгоритмы. (Однако, если ваша структура данных не имеет верхних пределов для большой выдающейся работы, на которую она готова откладывать, злой злоумышленник, возможно, заставит вас наверстать упущенное на максимальном объеме откладываемой работы одновременно.
Хотя, если вы достаточно обеспокоены по поводу злоумышленника, есть много других алгоритмических векторов атаки, о которых нужно беспокоиться, кроме амортизации и среднего случая.)
И средний регистр, и амортизация являются невероятно полезными инструментами для размышлений и проектирования с учетом масштабирования.
(См. Разница между средним случаем и амортизированным анализом , если интересует эта подтема.)
Многомерный биг-О
В большинстве случаев люди не понимают, что на работе более одной переменной. Например, в алгоритме поиска строки ваш алгоритм может занять время O([length of text] + [length of query])
, то есть он линейный по двум переменным, таким как O(N+M)
. Другие более наивные алгоритмы могут быть O([length of text]*[length of query])
или O(N*M)
. Игнорирование нескольких переменных является одним из наиболее распространенных недостатков, которые я вижу при анализе алгоритмов, и может помешать вам при разработке алгоритма.
Вся история
Имейте в виду, что big-O - это не вся история. Вы можете значительно ускорить некоторые алгоритмы, используя кеширование, делая их не обращающими внимания на кеш, избегая узких мест, работая с оперативной памятью вместо диска, используя распараллеливание или опережая работу - эти методы часто независимы из нотация big-O в порядке роста, хотя вы часто будете видеть количество ядер в нотации big-O параллельных алгоритмов.
Также имейте в виду, что из-за скрытых ограничений вашей программы вы можете не заботиться об асимптотическом поведении. Вы можете работать с ограниченным количеством значений, например:
- Если вы сортируете что-то вроде 5 элементов, вы не хотите использовать быструю
O(N log(N))
быструю сортировку; Вы хотите использовать сортировку вставкой, которая хорошо работает на небольших входах. Такие ситуации часто встречаются в алгоритмах «разделяй и властвуй», где вы разбиваете задачу на все более мелкие подзадачи, такие как рекурсивная сортировка, быстрые преобразования Фурье или умножение матриц.
- Если некоторые значения эффективно ограничены из-за какого-то скрытого факта (например, среднее человеческое имя мягко ограничено, возможно, 40 буквами, а человеческий возраст мягко ограничен около 150). Вы также можете наложить ограничения на ввод, чтобы эффективно сделать условия постоянными.
На практике даже среди алгоритмов, которые имеют одинаковую или сходную асимптотическую производительность, их относительные достоинства могут фактически зависеть от других факторов, таких как: другие факторы производительности (quicksort и mergesort оба O(N log(N))
, но быстрая сортировка использует преимущества Кэш процессора); соображения неэффективности, такие как простота реализации; доступна ли библиотека, насколько авторитетна и поддерживается библиотека.
Программы также будут работать медленнее на компьютере с частотой 500 МГц и 2 ГГц. На самом деле мы не рассматриваем это как часть границ ресурса, потому что мы думаем о масштабировании в терминах машинных ресурсов (например, за тактовый цикл), а не за реальную секунду. Однако есть похожие вещи, которые могут «тайно» влиять на производительность, например, работаете ли вы под эмуляцией или оптимизировал код компилятор или нет. Это может заставить некоторые базовые операции занимать больше времени (даже относительно друг друга) или даже асимптотически ускорять или замедлять некоторые операции (даже относительно друг друга). Эффект может быть небольшим или большим между различными реализациями и / или средой. Вы переключаете языки или машины, чтобы выполнить эту небольшую дополнительную работу? Это зависит от сотен других причин (необходимость, навыки, коллеги, производительность программиста, денежная ценность вашего времени, знакомство, обходные пути, почему бы не сборка или графический процессор и т. Д.), Которые могут быть важнее производительности.
Вышеупомянутые проблемы, такие как язык программирования, почти никогда не рассматриваются как часть постоянного фактора (и не должны); все же нужно знать о них, потому что иногда (хотя и редко) они могут влиять на вещи. Например, в cpython реализация очереди с собственным приоритетом асимптотически неоптимальна (O(log(N))
вместо O(1)
для вашего выбора вставки или find-min); Вы используете другую реализацию? Вероятно, нет, так как реализация C, вероятно, быстрее, и, возможно, есть другие подобные проблемы в других местах. Есть компромиссы; иногда они имеют значение, а иногда нет.
( edit : Объяснение "простого английского" на этом заканчивается.)
Math addenda
Для полноты, точное определение обозначения big-O выглядит следующим образом: f(x) ∈ O(g(x))
означает, что «f асимптотически ограничена сверху константой * g»: игнорируя все ниже некоторого конечного значения x, существует константа такая, что |f(x)| ≤ const * |g(x)|
. (Остальные символы следующие: точно так же, как O
означает ≤, Ω
означает ≥. Существуют строчные варианты: o
означает <, а <code>ω означает>.) f(x) ∈ Ɵ(g(x))
означает как f(x) ∈ O(g(x))
, так и f(x) ∈ Ω(g(x))
(верхняя и нижняя ограничены g): существуют некоторые константы, такие что f всегда будет лежать в «полосе» между const1*g(x)
и const2*g(x)
. Это самое сильное асимптотическое утверждение, которое вы можете сделать, и примерно эквивалентное ==
. (Извините, я решил отложить упоминание символов абсолютного значения до сих пор, для ясности; особенно потому, что я никогда не видел, чтобы отрицательные значения возникали в контексте информатики.)
Люди будут часто использовать = O(...)
, что, возможно, является более правильной нотацией «comp-sci» и совершенно законно для использования; «f = O (...)» читается как «f - порядок ... / f ограничен ххх ...» и рассматривается как «f - некоторое выражение, асимптотика которого ...». Меня учили использовать более строгий ∈ O(...)
. ∈
означает «элемент» (все еще читается как прежде). O(N²)
на самом деле класс эквивалентности , то есть это набор вещей, которые мы считаем одинаковыми. В данном конкретном случае O(N²)
содержит такие элементы, как {2 N²
, 3 N²
, 1/2 N²
, 2 N² + log(N)
, - N² + N^1.9
, ...} и является бесконечно большим, но это все еще набор. Обозначение =
может быть более распространенным и даже используется в работах всемирно известных компьютерных ученых. Кроме того, часто случается, что в обычной обстановке люди говорят «1439», когда они имеют в виду «1440»; это технически верно, так как набор Ɵ(exactlyThis)
является подмножеством O(noGreaterThanThis)
... и его легче набирать. ; -)