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

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

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

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

22 ответов


я ассистент профессора в моем местном университете на курсе структур данных и алгоритмов. Я сделаю все возможное, чтобы объяснить это здесь на простых условиях, но предупреждаю, что эта тема занимает у моих студентов пару месяцев, чтобы, наконец, понять. Вы можете найти более подробную информацию о Глава 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 числа вычислительных шагов.

мы собираемся добавить индивидуальное количество шагов функции, и ни объявление локальной переменной или оператор return зависят от размера 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() скачать polynomium в своем standard form.
  3. разделить условиям polynomium и сортировать их по темпам роста.
  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. Взгляните: индекс 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() is 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 to 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²)

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 описывает верхнюю границу для алгоритма (omega-для нижней границы, а тета-для жесткой границы).

когда мы доберемся до вложенного петли:

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

  • ...

хорошее введение введение в анализ алгоритмов Р. Седжвик и P. Flajolet.

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


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

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

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

(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)


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


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

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

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

O (log N) N) N log N) N2) Nk) n) n!)


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

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

например,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 = около .01 бит. Вы узнали очень мало! Второе решение не намного лучше. Вот почему линейный поиск идет так медленно. На самом деле это экспоненциально по количеству битов тебе нужно учиться.

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

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

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

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


начнем с самого начала.

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

  1. арифметические операции (например, + или %).
  2. логические операции (например, &&).
  3. операции сравнения (например,
  4. операции доступа к структуре (например индексирование массива как[i] или указатель fol- мычание с оператором ->).
  5. простое назначение, такое как копирование значения в переменную.
  6. вызовы библиотечных функций (например, scanf, printf).

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

  1. операторы присваивания, которые не включают вызовы функций в своих выражениях.
  2. читать высказывания.
  3. Write операторы, которые не требуют вызовов функций для оценки аргументов.
  4. заявления скачка ломают, продолжают, Гото, и возвращаемое выражение, где выражение не содержит вызова функции.

в C многие for-циклы формируются путем инициализации переменной индекса до некоторого значения и приращение этой переменной на 1 каждый раз вокруг петли. Цикл for-цикл заканчивается, когда индекс достигает некоторого предела. Например, 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. Он увеличивает i на 1 каждый раз вокруг цикла, а итерации остановитесь, когда я достигну n-1.

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

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

в простейшем случае, когда время, проведенное в теле цикла, одинаково для каждого итерация,мы можем умножить большую верхнюю границу для тела на число время вокруг петли. Строго говоря, мы должны ... --44-->добавить 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) для приращения i и проверить, является ли i

инициализация i = 0 внешнего цикла и (n + 1)st-тест условия i O(n^2) бег времени.


более практический пример.

enter image description here


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

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


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

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


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

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


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

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

поскольку мы можем найти медиану в o(n) времени и разделить массив на две части в O(n) времени, работа, выполняемая на каждом узле, равна 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)

Так этот алгоритм работает за время quadradic!


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


менее полезно вообще, я думаю, но для полноты есть и Большая Омега Ω, которая определяет нижнюю границу сложности алгоритма, и Большой Тета Θ, который определяет как верхнюю и нижнюю границу.


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

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


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

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


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

в качестве очень простого примера скажем, что вы хотели сделать проверку здравомыслия о скорости сортировки списка .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);

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

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

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


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

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

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

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


большой вопрос!

Если вы используете 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.


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

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

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

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


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

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


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

  1. Если у нас есть сумма терминов, термин с наибольшим темпом роста сохраняется, а другие термины опущены.
  2. Если у нас есть произведение нескольких факторов, постоянные факторы опущены.

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