Очень интересная статья Shen Huang о нотации О большое: Big O notation: why it matters, and why it doesn’t
В статье присутствует математические формулы, формальные определения, математические доказательства и т.п. При переводе возможно были допущены не точности в этих понятиях. Но тем не менее статья очень интересна для получения базовых знаний, о том что такое нотации Большое О, зачем оно нужно и какие бывают варианты нотаций.
Вы действительно понимаете что такое О большое (Big O)? Если это так, то эта статья поможет вам освежить ваши знания перед собеседованием. Если нет, эта статья расскажет вам о том что это такое и зачем нужно об этом знать.
Нотация О большое является одним из самых фундаментальных инструментов анализа сложности алгоритма. Эта статья написана с предположением, что вы уже являетесь программистом и за вашими плечами большое количество написанного кода. Кроме того, для лучшего понимания материала потребуется знания основ математики средней школы. И так если вы готовы, давайте начнем!
«Нотация О большое — это математическая нотация, которая описывает ограничивающее поведение функции, когда аргумент стремится к определенному значению или бесконечности. Он является членом семейства нотаций, изобретенных Полом Бахманом, Эдмундом Ландау и другими, которые в совокупности называются нотациями Бахмана-Ландау или асимптотическими нотациями ».
Проще говоря, нотация О большое описывает сложность вашего кода с использованием алгебраических терминов.
Чтобы понять, что такое О большое, мы можем взглянуть на типичный пример O (n²), который обычно произносится как «Большой O в квадрате». Буква «n» здесь представляет размер входных данных, а функция «g (n) = n²» внутри «O ()» дает нам представление о том, насколько сложен алгоритм по отношению к количеству входных данных.
Типичным алгоритмом со сложностью O(n²) будет алгоритм сортировки выбором. Сортировка выбором — это алгоритм сортировки, который выполняет итерацию по списку, чтобы гарантировать, что каждый элемент с индексом i является i-м наименьшим/наибольшим элементом списка. Наглядный пример.
Алгоритм может быть описан следующим кодом. Чтобы убедиться, что i-й элемент является i-м наименьшим элементом в списке, этот алгоритм сначала просматривает список с помощью цикла for. Затем для каждого элемента он использует другой цикл for, чтобы найти наименьший элемент в оставшейся части списка.
SelectionSort(List) { for(i from 0 to List.Length) { SmallestElement = List[i] for(j from i to List.Length) { if(SmallestElement > List[j]) { SmallestElement = List[j] } } Swap(List[i], SmallestElement) } }
В этом сценарии мы рассматриваем переменную List как входные данные, поэтому размер ввода n — это количество элементов внутри List. Предположим, что оператор if, а присвоение значения, ограниченное оператором if, занимает постоянное время. Затем мы можем найти О большое для функции SelectionSort, проанализировав, сколько раз выполняются операторы.
Сначала внутренний цикл for выполняет операторы внутри n раз. А затем, после увеличения i, внутренний цикл for выполняется n-1 раз … пока он не будет запущен еще один раз, тогда оба цикла for достигнут своих условий завершения.
На самом деле это в конечном итоге дает нам геометрическую сумму, и благодаря математики средней школе мы обнаружим, что внутренний цикл будет повторяться 1 + 2… + n раз, что равно n(n-1)/2 раза. Если мы умножим это, мы получим n²/2-n/2.
Когда мы вычисляем О большое, мы заботимся только о доминирующих операторах, и не заботимся о коэффициентах. Таким образом, мы выбираем n² как О большое. Мы записываем его как O(n²), а произносим как «О большое в квадрате».
Теперь вам может быть интересно, что это за «доминирующие операторы»? И почему нас не волнуют коэффициенты? Не волнуйтесь, мы рассмотрим эти вопросы по очереди далее.
Когда-то давно жил один индийский король, который захотел наградить мудреца. Мудрец попросил в качестве награды только пшеницы, которая бы заполнила все шахматную доску.
Но с определенным условием: в первой клетке должно быть 1 зернышко пшеницы, затем 2 зернышка на второй клетке, затем 4 на следующей … и так далее на каждой клетке шахматной доске должно быть вдвое больше количество зерен, чем на предыдущей. Наивный король согласился без колебаний, думая, что это слишком простое условие…
Так сколько зерна пшеницы должен царь мудрецу? Мы знаем, что шахматная доска имеет 8 квадратов на 8 квадратов, что в сумме составляет 64 фишки, поэтому в итоговой фишке должно быть 2⁶⁴ зерна пшеницы. Если вы проведете самостоятельный расчет, вы получите 1,8446744 * 10¹⁹, то есть около 18, за которыми следуют 18 нулей. Предполагая, что каждое зерно пшеницы весит 0,01 грамма, это дает нам 184 467 440 737 тонн пшеницы. А 184 миллиарда тонн — это много, не правда ли?
Числа могут расти довольно быстро, не так ли? Та же логика относится и к компьютерным алгоритмам. Если требуемые усилия для выполнения задачи растут экспоненциально по отношению к размеру входных данных, алгоритм может в конечном итоге стать чрезвычайно затратным.
Если вы продолжите удвоение 2⁶⁴, то результат будет быстро потерян за пределами значащих цифр. Вот почему, когда мы смотрим на темпы роста, мы заботимся только о доминирующих операторах. И поскольку мы хотим проанализировать рост по отношению к размеру ввода, коэффициенты, которые только умножают число, и не растут с размером ввода, не содержат никакой полезной информации.
Ниже приведено формальное определение Большого O:
Формальное определение полезно, когда вам нужно выполнить математическое доказательство. Например, сложность для сортировки выбором может быть определена функцией f (n) = n²/2-n/2, как мы обсуждали в предыдущем разделе.
Если мы допустим, чтобы наша функция g(n) была n², мы можем найти константу c = 1 и N₀ = 0, при условии, N > N₀, где N² всегда будет больше, чем N²/2-N/2. Мы можем легко доказать это, вычитая N²/2 из обеих функций, тогда мы можем видеть, что N²/2 > -N/2 будет истинно, когда N>0. Поэтому мы можем прийти к выводу, что f(n) = O (n²), в другом порядке выбора это «О большое квадрат».
Вы могли заметить небольшую хитрость. То есть, если вы заставите g(n) расти быстрее, чем что-либо другое, O(g(n)) всегда будет достаточно большим. Например, для любой полиномиальной функции вы всегда будете правы, говоря, что оно являются O(2ⁿ), потому что 2ⁿ в конечном итоге будет всегда больше любого полинома.
С математической точки зрения вы будете правы, но обычно, когда мы говорим о Большом O, нам нужно знать жесткую границу (tight bound) функции. Вы узнаете об этом больше, в следующем раздел.
Но прежде чем мы пойдем дальше, давайте проверим ваше понимание следующим вопросом. Ответ будет приведен в следующем разделе.
Вопрос: Допустим изображение представлено двумерным массивом пикселей. Вы используете вложенный цикл for для итерации по каждому пикселю (то есть у вас есть цикл for, проходящий по всем столбцам, и другой цикл внутри, чтобы пройти по всем строкам). Какая в этом случае будет сложность алгоритма?
Ниже приведены формальные математические определения этих нотаций:
О большое: «f(n) есть O(g (n))» тогда и только тогда, когда некоторые константы c и N₀, f(N) ≤ cg(N) для всех N> N₀
Малое O: «f(n) есть o (g(n))», если f(n) есть O(g(n)) и f(n) не есть Θ(g(n))
Омега: «f(n) есть Ω(g(n))» тогда и только тогда, когда некоторые константы c и N₀, f(N) ≥ cg(N) для всех N> N₀
Тета: «f (n) есть Θ(g (n))» тогда и только тогда, когда f(n) есть O(g(n)), а f(n) есть Ω(g(n))
Простыми словами:
Например, функция g(n) = n² + 3n — это O(n³), o(n⁴), Θ(n²) и Ω (n). Но вы все равно были бы правы, если бы сказали, что это Ω(n²) или O(n²).
Вообще, когда мы говорим о Большом O, мы на самом деле имеем в виду Тета (Θ Theta). Бессмысленно, когда вы определяете верхнюю границу, намного превышающую объем анализа. Это было бы похоже на решение неравенств путем помещения ∞ на большую сторону, что почти всегда формально сделает вас правым.
Но как определить, какие функции сложнее других?
Когда мы пытаемся выяснить О большое для конкретной функции g(n), мы заботимся только о доминирующем операторе (dominant term) функции. Доминирующий оператор — это такой оператор, который растет быстрее всего.
Например, n² растет быстрее, чем n, поэтому, если у нас есть что-то вроде g(n) = n² + 5n + 6, то О большое будет (n²). Если вы когда нибудь проводили некоторые исчисления, это очень похоже на сокращение пределов для дробных многочленов, когда вам важен только доминирующий оператор для числителей и знаменателей в конце.
Но какая функция растет быстрее, чем другие? На самом деле существует довольно много правил.
Часто называемый «постоянный по времени», если вы можете создать алгоритм для решения проблемы с O(1), то это будет лучший выбор алгоритма. В некоторых сценариях сложность может выходить за пределы O(1), тогда мы можем проанализировать их, найдя ее аналог O(1/g(n)). Например, O(1/n) является более сложным, чем O(1/n²).
Поскольку сложность часто связана с алгоритмами «разделяй и властвуй», O (log(n)) — это, как правило, хорошая сложность, которую можно достичь для алгоритмов сортировки. O (log(n)) является менее сложным, чем O (√n), потому что функцию квадратного корня можно считать полиномом, где показатель степени равен 0,5.
Например, O (n⁵) является более сложным, чем O (n⁴).
O (2ⁿ) является более сложным, чем O (n⁹⁹), но O (2ⁿ) на самом деле является менее сложным, чем O(1). Обычно мы берем 2 в качестве основы для степеней и логарифмов, потому что в компьютерных науках все имеет тенденцию быть двоичным, но степень можно изменить, изменив коэффициенты. Если не указано иное, основание для логарифмов принимается равным 2.
Если вам интересны доказательства, посмотрите Гамма-функцию (Gamma function), это аналитическое продолжение факториала. Краткое доказательство состоит в том, что и факториалы, и степень имеют одинаковое количество умножений, но числа, которые умножаются, растут для факториалов, оставаясь неизменными для степени.
При умножении сложность будет больше, чем оригинал, но не больше, чем эквивалентность умножения чего-то более сложного. Например, O (n*log (n)) является более сложным, чем O (n), но менее сложным, чем O (n²), потому что O (n²) = O (n * n), а n более сложный, чем log (n). ).
Если хотите можете проверить свое понимание. Попробуйте ранжировать следующие функции от самых сложных до менее. Решения с подробными объяснениями можно будет найти в следующем разделе. Некоторые из них достаточно сложные и могут потребовать более глубокого понимания математики. Когда вы доберетесь до решения, вы узнаете об этом больше.
Вопрос: Расставьте следующие функции от самых сложных до менее.
Решение для вопроса из Раздела 2:
На самом деле это был вопрос с подвохом, чтобы проверить ваше понимание. Возможно вы бы предположили что ответ равен O (n²), потому что есть вложенный цикл for. Но нужно понять, что входными данными является массив изображений, и каждый пиксель в алгоритме проходится только один раз, то ответ на самом деле будет O (n). В следующем разделе будут рассмотрены другие примеры, подобные этому.
До сих пор мы обсуждали только временную сложность алгоритмов. То есть мы заботимся только о том, сколько времени потребуется программе для выполнения задачи. Что также важно, так это место, занимаемое программой в памяти для выполнения задачи. Пространственная сложность связана с тем, сколько памяти будет использовать программа, и, следовательно, также является важным фактором для анализа.
Пространственная сложность работает аналогично временной сложности. Например, сортировка выбором имеет пространственную сложность O(1), потому что она хранит только одно минимальное значение и свой индекс для сравнения, максимальное используемое пространство не увеличивается с размером ввода.
Некоторые алгоритмы, такие как блочная сортировка, имеют пространственную сложность O (n), но при этом в ней можно уменьшить временную сложность до O (1). Блочная сортировка это такая сортировка в котором сортируемые элементы распределяются между конечным числом отдельных блоков (карманов, корзин) так, чтобы все элементы в каждом следующем по порядку блоке были всегда больше (или меньше), чем в предыдущем. Каждый блок затем сортируется отдельно, либо рекурсивно тем же методом, либо другим. Затем элементы помещаются обратно в массив.
Сложность также может быть проанализирована как лучший случай, наихудший случай, средний случай и ожидаемый случай.
Для примера давайте рассмотрим сортировку вставками (insertion sort). Сортировка вставками выполняет итерацию по всем элементам в списке. Если элемент больше, чем его предыдущий элемент, он вставляет элемент назад, пока он не станет больше, чем предыдущий элемент.
Если массив изначально отсортирован, обмен вообще не будет произведен. Алгоритм будет просто пройдет итерацию по массиву один раз, что приведет к временной сложности O (n). Следовательно, мы бы сказали, что наилучшая временная сложность сортировки вставками — O (n). Сложность O (n) также часто называют линейной сложностью.
Иногда алгоритму может просто не повезти. Например, быстрая сортировка будет должна пройти через список за O (n), если элементы отсортированы в обратном порядке, но в среднем этот алгорит сортирует массив за O (n * log(n)). Как правило, когда мы оцениваем временную сложность алгоритма, мы смотрим на ее худшую производительность. Подробнее об этом и быстрой сортировке мы поговорим в следующем разделе.
Средняя сложность описывает ожидаемую производительность алгоритма. Иногда включает в себя расчет вероятности каждого сценария. Ниже приведена шпаргалка по временной и пространственной сложности типичных алгоритмов.
Осматривая функции, мы можем начать с ранжирования следующих полиномов от наиболее сложного до менее по правилу 3. Где корень квадратный из n равен просто n в степени 0,5.
Тогда, применяя правила 2 и 6, мы получим следующее. Логарифм с основанием 3 может быть преобразован в с основанием 2 (log base conversions). Логарифм с основанием 3 по-прежнему растет немного медленнее, чем с основанием 2, и поэтому в рейтинге получает место после него.
Остальные могут показаться немного сложными, но давайте попробуем быть немного повнимательнее и посмотреть, как же все такие их можно расположить.
Прежде всего, 2 в степени 2 в степени n больше 2 в степени n, а +1 еще больше его увеличивает.
Чтобы степень log (n) с основанием 2 была равна n, мы можем преобразовать следующее. Логарифм от 0,001 растет немного больше, чем просто константы, но меньше, чем почти все остальное.
Выражение, у которого n в степени log (log (n)), на самом деле является вариацией квазиполинома (quasi-polynomial), который больше полинома, но меньше экспоненты. Поскольку log (n) растет медленнее, чем n, его сложность немного меньше. Выражение с обратным логарифмом сходится к константе, поскольку 1 / log (n) расходится к бесконечности.
Факториалы могут быть представлены умножением и, следовательно, могут быть преобразованы в сложения вне логарифмической функции. «N select 2» может быть преобразовано в полином с кубическим членом, являющимся наибольшим.
И, наконец, мы можем ранжировать функции от самых сложных до наименее сложных.
!!! — WARNING — !!!
Идея, обсуждаемая далее, обычно не принимается большинством программистов в мире. Озвучивайте ее при собеседованиях на свой страх и риск. Бывали случаи когда, люди провалили собеседование в Google, потому что они подвергли сомнению необходимости нотаций.
!!! — WARNING — !!!
Поскольку ранее мы узнали, что сложность по времени в наихудшем случае для быстрой сортировки будет O (n²), а для сортировки слиянием (merge sort) будет O (n * log (n)), то сортировка слиянием должна быть быстрее, верно? Ну, вы, наверное, догадались, что ответ неверен. Чтобы это продемонстрировать, я выложил этот пример сюда trinket.io. Он сравнивает время для быстрой сортировки (quick sort) и сортировки слиянием (merge sort). Мне удалось протестировать его только на массивах длиной до 10000 значений, но, как вы можете видеть, время сортировки слиянием растет быстрее, чем быстрой сортировки. Несмотря на быструю сортировку, имеющую худшую сложность O (n²), вероятность этого действительно низка. Когда дело доходит до увеличения скорости, быстрая сортировка имеет более высокую скорость чем сортировка слиянием, ограниченная сложностью O (n * log (n)), быстрая сортировка заканчивается в среднем с лучшей производительностью.
Я также сделал график, чтобы сравнить соотношение между временем, которое они занимают, так как это время трудно увидеть при более низких значениях. И, как вы можете видеть, процент времени, требуемый для быстрой сортировки, очень быстро уменьшается.
Мораль этой истории в том, что нотация О большое — это всего лишь математический анализ, который дает представление о ресурсах, потребляемых алгоритмом. Практически результаты могут быть разными. Но, как правило, это хорошая практика — пытаться снизить сложность наших алгоритмов.
Мне нравится кодить, изучать новые вещи и делиться ими с сообществом. Если есть что-то, что вас особенно интересует, пожалуйста, дайте мне знать. Я обычно пишу о веб-дизайне, архитектуре программного обеспечения, математике и науке о данных. Вы можете найти несколько замечательных статей, которые я написал ранее, если вас интересует какая-либо из вышеперечисленных тем.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Переводчику на заметку: В русскоязычной литературе принято называть "О большое", а не "Большое О"
Спасибо за комментарий
"cost of an algorithm" != "сложности алгоритма"
Тут речь идет скорее не о СЛОЖНОСТИ, а о ресурсоемкости.
Алгоритм может быть прост как 3 копейки, но жрать память и/или выполняться веками.
Да уж, над переводчиком грех не поржать, раз он "dominant term" переводит как "доминирующий оператор". Это никакой не доминирующий оператор, а всего навсего "основной член". Никакие операторы тут ни при чём. Переводчик боится слова "член" что ли?
Простите, что? Вы вообще хоть раз читали свой перевод? О(1) - это САМЫЙ быстрый алгоритм, а 2^n - это один из наихудших случаев.
Space complexity это не сложность пространства, а пространственная сложность, ну или "сложность по памяти".
В общем, если вы не понимаете то, что переводите, попросите программиста вычитать текст и исправьте ошибки. Их довольно много, читать очень тяжело. Медвежья услуга для читателя.
Отличная статья! Перевод может и содержит некоторые неточности, но они никаким образом не влияют на понимание сути. Автору и переводчику спасибо!
n(n-1)/2 не равно 1 + 2… + n, а вот n(n+1)/2 равно