Big O, как вы его вычисляете/приближаете?

Большинство людей со степенью в CS наверняка знают, что Big O означает. Это помогает нам измерить, насколько эффективен алгоритм, и если вы знаете в в какой категории проблема, которую вы пытаетесь решить, лежит в вас может выяснить, сможет ли еще выжать эту небольшую дополнительную производительность. 1

Но мне любопытно, как вы вычисляете или приближаете сложность ваших алгоритмов?

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

Ответ 1

Я сделаю все возможное, чтобы объяснить это здесь простыми терминами, но имейте в виду, что эта тема займет у моих студентов пару месяцев, чтобы наконец понять. Вы можете найти больше информации о Главе 2 книги " Структуры данных и алгоритмы в Java".


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

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

Цель проста: сравнить алгоритмы с теоретической точки зрения без необходимости выполнения кода. Чем меньше количество шагов, тем быстрее алгоритм.

Например, допустим, у вас есть этот кусок кода:

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

Эта функция возвращает сумму всех элементов массива, и мы хотим создать формулу для подсчета вычислительной сложности этой функции:

Number_Of_Steps = f(N)

Итак, у нас есть функция f(N) для подсчета количества вычислительных шагов. Ввод функции - это размер обрабатываемой структуры. Это означает, что эта функция называется так:

Number_Of_Steps = f(data.length)

Параметр N принимает значение data.length. Теперь нам нужно фактическое определение функции f(). Это делается из исходного кода, в котором каждая интересующая строка пронумерована от 1 до 4.

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

Мы собираемся добавить индивидуальное количество шагов функции, и ни объявление локальной переменной, ни оператор возврата не зависят от размера массива data.

Это означает, что строки 1 и 4 занимают по C шагов, и функция выглядит примерно так:

f(N) = C + ??? + C

Следующая часть заключается в определении значения оператора for. Помните, что мы рассчитываем количество вычислительных шагов, а это означает, что тело оператора for выполняется N раз. Это так же, как добавление C, N раз:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

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

Чтобы получить реальный BigOh, нам понадобится асимптотический анализ функции. Это примерно сделано так:

  1. Уберите все константы C
  2. Из f() получаем полином в его standard form.
  3. Разделите члены полинома и рассортируйте их по скорости роста.
  4. Оставьте тот, который увеличивается, когда N приближается к infinity.

У нашего f() есть два условия:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

Убираем все константы C и лишние части:

f(N) = 1 + N ^ 1

Поскольку последний член является тем, который становится больше, когда f() приближается к бесконечности (думайте о пределах), это аргумент BigOh, а функция sum() имеет значение BigOh:

O(N)

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

Например, этот код может быть легко решен с помощью суммирования:

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

Первое, что вам нужно было спросить, это порядок выполнения foo(). Хотя обычно это O(1), вы должны спросить об этом своих профессоров. O(1) означает (почти, в основном) постоянную C, независимую от размера N

for заявления о количестве предложений один сложнее. Хотя индекс заканчивается на 2 * N, приращение делается на два. Это означает, что первый for выполняется только за N шагов, и нам нужно разделить счет на два.

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

Предложение номер два еще сложнее, поскольку оно зависит от значения i. Посмотрите: индекс я принимает значения: 0, 2, 4, 6, 8,..., 2 * N, а второй for выполнения: N раз первый, N - 2 второй, N - 4 третий... до стадии N/2, на котором второй for никогда не запускается на выполнение.

По формуле это означает:

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

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

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(Мы предполагаем, что foo() есть O(1) и выполняет шаги C)

У нас здесь проблема: когда i беру значение N/2 + 1 вверх, внутреннее суммирование заканчивается отрицательным числом! Это невозможно и неправильно. Нам нужно разделить суммирование на две части, являясь поворотной точкой в момент, когда i беру N/2 + 1.

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

Поскольку поворотный момент i > N/2, внутренний for не будет выполнен, и мы предполагаем постоянную сложность выполнения C в его теле.

Теперь суммирования можно упростить, используя некоторые правила идентификации:

  1. Суммирование (w от 1 до N) (C) = N * C
  2. Суммирование (w от 1 до N) (A (+ / -) B) = Суммирование (w от 1 до N) (A) (+ / -) Суммирование (w от 1 до N) (B)
  3. Суммирование (w от 1 до N) (w * C) = C * Суммирование (w от 1 до N) (w) (C является константой, независимой от w)
  4. Суммирование (w от 1 до N) (w) = (N * (N + 1))/2

Применяя некоторую алгебру:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

И BigOh это:

O(N²)

Ответ 2

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

Несколько примеров того, как он используется в коде C.

Скажем, у нас есть массив из n элементов

int array[n];

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

x = array[0];

Если бы мы хотели найти число в списке:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

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

Когда мы получаем вложенные циклы:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

Это O (n ^ 2), так как для каждого прохода внешнего цикла (O (n)) нам нужно снова пройти весь список, чтобы n умножить на нас с квадратом n.

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

Ответ 3

Хотя знание того, как определить время Big O для вашей конкретной проблемы, полезно, зная, что некоторые общие случаи могут значительно помочь вам принять решения в вашем алгоритме.

Вот некоторые из наиболее распространенных случаев, снятых с http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions:

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

O (logn) - Поиск элемента в отсортированном массиве с двоичным поиском

O (n) - поиск элемента в несортированном списке; добавление двух n-значных чисел

O (n 2) - умножение двух n-значных чисел на простой алгоритм; добавление двух матриц n × n; сортировка пузырьков или сортировка вставки

O (n 3) - Умножение двух n × n-матриц простым алгоритмом

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

O (n!) - Решение проблемы коммивояжера с помощью поиска грубой силы

O (n n) - Часто используется вместо O (n!) для получения более простых формул для асимптотической сложности

Ответ 4

Маленькое напоминание: обозначение big O используется для обозначения асимптотической сложности (т.е. когда размер проблемы возрастает до бесконечности), и он скрывает константу.

Это означает, что между алгоритмом в O (n) и одним в O (n 2) самый быстрый не всегда является первым (хотя всегда существует такое значение n, что для проблемы размерa > n, первый алгоритм является самым быстрым).

Обратите внимание, что скрытая константа очень сильно зависит от реализации!

Кроме того, в некоторых случаях среда исполнения не является детерминированной функцией размера n ввода. Например, сортировка выполняется с использованием быстрой сортировки: время, необходимое для сортировки массива из n элементов, не является константой, но зависит от начальной конфигурации массива.

Существуют различные временные сложности:

  • Худший случай (обычно самый простой способ выяснить, хотя и не всегда очень значимый)
  • Средний размер (обычно гораздо сложнее выяснить)

  • ...

Хорошее введение - это введение в анализ алгоритмов Р. Седжуика и П. Флейолета.

Как вы говорите, premature optimisation is the root of all evil и (если возможно) профилирование действительно должно всегда использоваться при оптимизации кода. Это может даже помочь вам определить сложность ваших алгоритмов.

Ответ 5

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

Также я хотел бы добавить, как это делается для рекурсивных функций:

предположим, что у нас есть функция типа (код схемы):

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

который рекурсивно вычисляет факториал данного числа.

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

Таким образом, производительность для тела: O (1) (константа).

Затем попробуйте и определите это для количества рекурсивных вызовов. В этом случае мы имеем n-1 рекурсивных вызовов.

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

Затем поместите эти два вместе, и тогда вы получите производительность для всей рекурсивной функции:

1 * (n-1) = O (n)


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

Ответ 6

Если ваша стоимость является полиномом, просто держите член высшего порядка без его множителя. Например:.

O ((n/2 + 1) * (n/2)) = O (n 2/4 + n/2) = O (n 2/4) = O (n 2)

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

O (log N) O (N) O (N log N) O (N 2) < O (N k) < O (e n) < O (п!)

Ответ 7

Я думаю об этом с точки зрения информации. Любая проблема состоит в изучении определенного количества бит.

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

Например, оператор if, имеющий две ветки, одинаково вероятные, имеет энтропию 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Таким образом, его энтропия равна 1 бит.

Предположим, что вы ищете таблицу из N элементов, например N = 1024. Это 10-битная проблема, потому что log (1024) = 10 бит. Поэтому, если вы можете искать его с помощью операторов IF, которые имеют одинаково вероятные результаты, он должен принимать 10 решений.

Что вы получаете с бинарным поиском.

Предположим, что вы выполняете линейный поиск. Вы смотрите на первый элемент и спрашиваете, хочет ли он тот, который вы хотите. Вероятности - 1/1024, что и есть, и 1023/1024, что это не так. Энтропия этого решения составляет 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * около 0 = около 0,01 бит. Вы узнали очень мало! Второе решение не намного лучше. Вот почему линейный поиск настолько медленный. Фактически это экспоненциально в количестве бит, которое вам нужно изучить.

Предположим, вы делаете индексирование. Предположим, что таблица предварительно отсортирована во множество бункеров, и вы используете некоторые из всех бит в ключе, чтобы индексировать непосредственно в записи таблицы. Если имеется 1024 бункера, энтропия составляет 1/1024 * log (1024) + 1/1024 * log (1024) +... для всех 1024 возможных результатов. Это 1/1024 * 10 раз 1024 результата или 10 бит энтропии для этой операции индексирования. Вот почему поиск индексирования выполняется быстро.

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

Таким образом, сортировки, основанные на бинарных решениях, имеющих примерно одинаково вероятные результаты, все принимают за шаги O (N log N). Алгоритм сортировки O (N) возможен, если он основан на поиске индексирования.

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

Ответ 8

Давайте начнем с начала.

Прежде всего, примите принцип, что некоторые простые операции с данными можно сделать в O(1) времени, то есть во времени, которое не зависит от размера ввода. Эти примитивные операции в C состоят из

  • Арифметические операции (например, + или%).
  • Логические операции (например, &).
  • Операции сравнения (например, < =).
  • Операции доступа к структуре (например, индексирование массива, такое как A [i] или указатель fol- с помощью оператора → ).
  • Простое назначение, такое как копирование значения в переменную.
  • Вызов функций библиотеки (например, scanf, printf).

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

  • Операторы присваивания, которые не включают вызовы функций в их выражениях.
  • Чтение утверждений.
  • Записывать операторы, которые не требуют вызова функций для вычисления аргументов.
  • Операторы перехода прерывают, продолжают, goto и возвращают выражение, где выражение не содержит вызова функции.

В C многие for-loops формируются путем инициализации индексной переменной до некоторого значения и увеличивая эту переменную на 1 каждый раз вокруг цикла. Контур for-loop заканчивается, когда индекс достигает некоторого предела. Например, for-loop

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

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

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

Например, for-loop выполняет итерацию ((n − 1) − 0)/1 = n − 1 times, так как 0 является начальным значением i, n - 1 является наивысшим значением, достигаемым я (т.е. когда i достигает n-1, цикл останавливается и не происходит итерации с я = n-1), а 1 добавляется к я на каждой итерации цикла.

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


Теперь рассмотрим этот пример:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

Мы знаем, что строка (1) занимает время O(1). Ясно, что мы идем вокруг цикла n раз, так как мы можем определить, вычитая нижний предел из верхнего предела, найденного на линии (1), а затем добавление 1. Так как тело, строка (2), принимает время O (1), можно пренебречь время для увеличения j и время для сравнения j с n, оба из которых также O (1). Таким образом, время выполнения строк (1) и (2) является <сильным > произведением n и O (1), которое O(n).

Аналогично, мы можем связать время работы внешнего цикла, состоящего из строк (2) - (4), который равен

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

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

Инициализация я = 0 внешнего цикла и (n + 1) -й тест условия i < n также принимают O (1) раз и можно пренебречь. Наконец, заметим, что мы идем вокруг внешнего цикла n раз, принимая O (n) время для каждой итерации, давая общую O(n^2) время выполнения.


Более практичный пример.

enter image description here

Ответ 9

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

У этого есть несколько преимуществ перед изучением кода. Во-первых, вы можете увидеть, находитесь ли вы в диапазоне, где время выполнения приближается к его асимптотическому порядку. Кроме того, вы можете обнаружить, что какой-то код, который, по вашему мнению, был порядком O (x), действительно является порядком O (x ^ 2), например, из-за времени, затраченного на вызовы библиотеки.

Ответ 10

В основном то, что составляет 90% времени, - это просто анализ циклов. У вас есть одиночные, двойные, тройные вложенные циклы? Вы выполняете время O (n), O (n ^ 2), O (n ^ 3).

Очень редко (если вы не пишете платформу с обширной базовой библиотекой (например,.NET BCL или С++ STL), вы столкнетесь с чем-либо, что сложнее, чем просто смотреть на ваши циклы (для операторов, в то время как, goto и т.д.)

Ответ 11

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

Ответ 12

Разбейте алгоритм на части, для которых вы знаете большую нотацию O, и объединитесь с большими операторами O. Это единственный способ узнать.

Для получения дополнительной информации просмотрите страницу Википедии по этому вопросу.

Ответ 13

Нотация Big O полезна, потому что она легко работает и скрывает ненужные сложности и детали (для некоторого определения ненужного). Одним из хороших способов разработки сложностей алгоритмов разделения и покорения является метод tree. Скажем, у вас есть версия quicksort с медианной процедурой, поэтому вы каждый раз разбиваете массив на идеально сбалансированные подмассивы.

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

Поскольку мы можем найти медиану в O (n) времени и разделить массив на две части в O (n) времени, работа, выполняемая в каждом node, равна O (k), где k - размер массива, Каждый уровень дерева содержит (не более) весь массив, поэтому работа на уровне равна O (n) (размеры подмассивов складываются до n, и поскольку у нас есть O (k) на уровень, мы можем добавить это), В дереве есть только уровни log (n), так как каждый раз, когда мы вдвое уменьшаем ввод.

Поэтому мы можем ограничить верхнюю часть работы O (n * log (n)).

Однако Big O скрывает некоторые детали, которые мы иногда не можем игнорировать. Рассмотрим вычисление последовательности Фибоначчи с помощью

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

и просто предположим, что a и b - BigIntegers в Java или что-то, что может обрабатывать сколь угодно большие числа. Большинство людей сказали бы, что это алгоритм O (n), не дрогнув. Причиной является то, что у вас есть n итераций в цикле for и O (1) работают в цикле.

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

1 + 2 + 3 +... + n = n (n-1)/2 = O (n ^ 2)

Итак, этот алгоритм работает в квадратичное время!

Ответ 14

Менее полезно вообще, я думаю, но для полноты есть также Big Omega Ω, который определяет нижнюю границу на сложность алгоритма и Big Theta Θ, которая определяет как верхнюю, так и нижнюю границу.

Ответ 15

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

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

Ответ 16

Для 1-го случая внутренний цикл выполняется n-i раз, поэтому общее количество исполнений представляет собой сумму для i, идущую от 0 до n-1 (потому что ниже, не ниже или равно ) n-i. Наконец, вы получите n*(n + 1) / 2, поэтому O(n²/2) = O(n²).

Для второго цикла i находится между 0 и n, включенными для внешнего цикла; то внутренний цикл выполняется, когда j строго больше n, что невозможно.

Ответ 17

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

Как очень простой пример, вы говорите, что хотите выполнить проверку работоспособности скорости сортировки списка .NET framework. Вы можете написать что-то вроде следующего, а затем проанализировать результаты в Excel, чтобы убедиться, что они не превышают кривую n * log (n).

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

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);

Ответ 18

Мы должны забыть о небольших эффективности, скажем, около 97% время: преждевременная оптимизация - это корень всего зла.

что вся цитата кстати. так что dosnt означает никогда не оптимизировать, прежде чем перейти на стадию оптимизации.

Ответ 19

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

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

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

Ответ 20

То, что часто упускается из виду, - это ожидаемое поведение ваших алгоритмов. Он не меняет Big-O вашего алгоритма, но он относится к утверждению "преждевременная оптимизация..."

Ожидаемое поведение вашего алгоритма - очень подавленное - насколько быстро вы можете ожидать, что ваш алгоритм будет работать с данными, которые вы, скорее всего, увидите.

Например, если вы ищете значение в списке, оно O (n), но если вы знаете, что большинство списков, которые вы видите, имеют ваше значение вверх, типичное поведение вашего алгоритма выполняется быстрее.

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

Ответ 21

отличный вопрос!

Отказ от ответственности: этот ответ содержит ложные утверждения, см. Комментарии ниже.

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

Посетите этот сайт, чтобы получить прекрасное формальное определение Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html.

f (n) = O (g (n)) означает, что существуют положительные постоянные c и k, такие что 0 ≤ f (n) ≤ cg (n) для всех n ≥ k. Значения c и k должны быть фиксированы для функции f и не должны зависеть от n.


Хорошо, теперь, что мы подразумеваем под сложностями "лучший случай" и "худший случай"?

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

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

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

Ответ 22

Для кода A внешний цикл будет выполняться n+1 раз, время "1" означает процесс, который проверяет, соответствует ли я требованию. И внутренний цикл выполняется n раз, n-2 раза.... Таким образом, 0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)

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

Ответ 23

Я не знаю, как программно решить это, но первое, что люди делают, это то, что мы пробовали алгоритм для определенных шаблонов в количестве выполненных операций, скажем, 4n ^ 2 + 2n + 1, у нас есть 2 правила:

  • Если у нас есть сумма терминов, термин с наибольшей скоростью роста сохраняется, а остальные члены опущены.
  • Если у нас есть произведение нескольких факторов, то постоянные факторы опущены.

Если мы упростим f (x), где f (x) - формула для числа выполненных операций (4n ^ 2 + 2n + 1, описанная выше), получаем значение большого О [O (n ^ 2 ) в этом случае]. Но это должно было бы учитывать интерполяцию Лагранжа в программе, которую может быть трудно реализовать. И что, если реальное значение big-O было O (2 ^ n), и мы могли бы иметь что-то вроде O (x ^ n), поэтому этот алгоритм, вероятно, не был бы программируемым. Но если кто-то докажет, что я неправ, дай мне код.,.

Ответ 24

Я хотел бы объяснить Big-O в несколько ином аспекте.

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

ИМХО в формулах big-O лучше не использовать более сложные уравнения (вы можете просто придерживаться приведенных на следующем графике.) Однако вы все равно можете использовать другие более точные формулы (например, 3 ^ n, n ^ 3,...) но иногда это может вводить в заблуждение! Так что лучше держать это как можно проще.

enter image description here

Я хотел бы еще раз подчеркнуть, что здесь мы не хотим получить точную формулу для нашего алгоритма. Мы только хотим показать, как он растет, когда входы растут, и сравнить в этом смысле с другими алгоритмами. В противном случае вам лучше использовать другие методы, такие как бенчмаркинг.