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

267

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

int recursiveFun1(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun1(n-1);
}

int recursiveFun2(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun2(n-5);
}

int recursiveFun3(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun3(n/5);
}

void recursiveFun4(int n, int m, int o)
{
    if (n <= 0)
    {
        printf("%d, %d\n",m, o);
    }
    else
    {
        recursiveFun4(n-1, m+1, o);
        recursiveFun4(n-1, m, o+1);
    }
}

int recursiveFun5(int n)
{
    for (i = 0; i < n; i += 2) {
        // do something
    }

    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun5(n-5);
}
Michael_19
источник
4
Если вы не хотите проходить анализ каждый раз, есть метод черного ящика, называемый методом Master. Но с предположением, что все рекурсивные разбиения входов имеют одинаковый размер в каждом случае.
Вивек Кришна

Ответы:

345

Временная сложность в обозначениях Big O для каждой функции находится в числовом порядке:

  1. Первая функция вызывается рекурсивно n раз до достижения базового случая, поэтому ее O(n)часто называют линейной .
  2. Вторая функция вызывается n-5 каждый раз, поэтому мы вычитаем пять из n перед вызовом функции, но также n-5 O(n). (На самом деле называется порядок n / 5 раз. И, O (n / 5) = O (n)).
  3. Эта функция представляет собой log (n) основание 5, каждый раз, когда мы делим на 5 перед вызовом функции, поэтому ее O(log(n))(основание 5), часто называемое логарифмическим и чаще всего анализом обозначений и сложности Big O, использует основание 2.
  4. В четвертом, это O(2^n)или экспоненциально , так как каждый вызов функции вызывает себя дважды, если только он не был повторен n раз.
  5. Что касается последней функции, цикл for принимает n / 2, так как мы увеличиваем на 2, а рекурсия - n-5, и поскольку цикл for вызывается рекурсивно, следовательно, сложность времени находится в (n-5) * (n / 2) = (2n-10) * n = 2n ^ 2-10n, из-за асимптотического поведения и соображений сценария наихудшего случая или из-за верхней границы, к которой стремится большой O, нас интересует только самый большой член O(n^2).

    Удачи на ваших промежуточных условиях;)

кодировщик
источник
ваше право насчет пятого, n будет уменьшаться для цикла for, но для четвертого я не думаю, что его n ^ 2 для его как дерева каждый раз, когда вы вызываете рекурсию дважды, так что должно быть 2 ^ n плюс, что было вашим ответ в комментарии ранее.
кодер
2
@MJGwater Пусть время цикла составляет м. Когда рекурсивный прогон 1 раз, для выполнения цикла требуется m. Когда рекурсивный запуск 2 раза, цикл также запускается 2 раза, поэтому он занимает 2 метра ... и так далее. Так что это «*», а не «^».
BJC
3
@coder Объяснение 5 кажется странным. Если увеличение на 2 приводит к n/2итерациям forцикла, почему уменьшение на 5 не приведет к n/5рекурсивным вызовам? Это все равно приведет к, O(n^2)но кажется более интуитивным объяснением. Зачем смешивать вычитание и деление, если они необходимы, делая одно и то же?
Джек,
1
@ кодер, так что для # 4, если бы в определении функции было 3 рекурсивных вызова, он имел бы временную сложность O (3 ^ n)? И для 5 рекурсивных вызовов это будет O (5 ^ n), правильно?
rmutalik
1
@ Джек Да, мне тоже было интересно. Она должна быть n/5не n-5. И в конечном итоге все сводится к O(N^2).
Anuj
128

Для случая , когда n <= 0, T(n) = O(1). Следовательно, сложность времени будет зависеть от того, когда n >= 0.

Мы рассмотрим случай n >= 0в части ниже.

1.

T(n) = a + T(n - 1)

где а - некоторая постоянная

По индукции:

T(n) = n * a + T(0) = n * a + b = O(n)

где a, b - некоторая константа.

2.

T(n) = a + T(n - 5)

где а некоторая постоянная

По индукции:

T(n) = ceil(n / 5) * a + T(k) = ceil(n / 5) * a + b = O(n)

где a, b некоторая постоянная и k <= 0

3.

T(n) = a + T(n / 5)

где а некоторая постоянная

По индукции:

T(n) = a * log5(n) + T(0) = a * log5(n) + b = O(log n)

где a, b некоторая постоянная

4.

T(n) = a + 2 * T(n - 1)

где а некоторая постоянная

По индукции:

T(n) = a + 2a + 4a + ... + 2^(n-1) * a + T(0) * 2^n 
     = a * 2^n - a + b * 2^n
     = (a + b) * 2^n - a
     = O(2 ^ n)

где a, b - некоторая константа.

5.

T(n) = n / 2 + T(n - 5)

где n - некоторая постоянная

Перепишите, n = 5q + rгде q и r являются целыми числами, а r = 0, 1, 2, 3, 4

T(5q + r) = (5q + r) / 2 + T(5 * (q - 1) + r)

Мы имеем q = (n - r) / 5, и так как г <5, мы можем считать его константой, поэтомуq = O(n)

По индукции:

T(n) = T(5q + r)
     = (5q + r) / 2 + (5 * (q - 1) + r) / 2 + ... + r / 2 +  T(r)
     = 5 / 2 * (q + (q - 1) + ... + 1) +  1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * (q + 1) * q + 1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * q^2 + 5 / 4 * q + 1 / 2 * q * r + 1 / 2 * r + T(r)

Поскольку r <4, мы можем найти некоторую константу b так, чтобы b >= T(r)

T(n) = T(5q + r)
     = 5 / 2 * q^2 + (5 / 4 + 1 / 2 * r) * q + 1 / 2 * r + b
     = 5 / 2 * O(n ^ 2) + (5 / 4 + 1 / 2 * r) * O(n) + 1 / 2 * r + b
     = O(n ^ 2)
nhahtdh
источник
1
Недавно я провалил вопрос об интервью (и, соответственно, интервью), связанный с анализом пространственно-временной сложности рекурсивной функции Фибоначчи. Этот ответ эпичен, и он очень помог, мне это нравится, я хотел бы проголосовать за тебя дважды. Я знаю, что он старый, но у вас есть что-то похожее для расчета пространства - может быть, ссылка, что-нибудь?
Димитар Димитров
Для № 4, даже если результат один и тот же, разве индукция не должна быть следующей? T (n) = a + 2T (n-1) = a + 2a + 4T (n-1) = 3a + 4a + 8T (n-1) = a * (2 ^ n - 1) + 2 ^ n * T (0) = a * (2 ^ n - 1) + b * 2 ^ n = (a + b) * 2 ^ n - a = O (2 ^ n)
Снежная рыба
27

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

Complexity = length of tree from root node to leaf node * number of leaf nodes
  1. Первая функция будет иметь длину nи номер конечного узла, 1поэтому сложность будетn*1 = n
  2. Вторая функция снова будет иметь длину n/5и количество конечных узлов, 1поэтому сложность будет n/5 * 1 = n/5. Это должно быть приближено кn

  3. Для третьей функции, поскольку nона делится на 5 при каждом рекурсивном вызове, длина рекурсивного дерева будет равна log(n)(base 5), а число конечных узлов снова 1, поэтому сложность будетlog(n)(base 5) * 1 = log(n)(base 5)

  4. Для четвертой функции, поскольку каждый узел будет иметь два дочерних узла, число конечных узлов будет равно, (2^n)а длина рекурсивного дерева будет nтакой же сложной (2^n) * n. Но поскольку nон незначителен перед ним (2^n), его можно игнорировать, а сложность можно только сказать (2^n).

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

Примечание. Это быстрый и грязный способ вычисления сложности (ничего официального!). Хотелось бы услышать отзывы об этом. Спасибо.

Shubham
источник
Отличный ответ! У меня вопрос по четвертой функции. Если бы у него было три рекурсивных вызова, ответ был бы (3 ^ n). Или вы все еще просто скажете (2 ^ n)?
Бен Форсруп
@Shubham: # 4 мне не кажется правильным. Если количество листьев равно, 2^nвысота дерева должна быть n, а не log n. Высота будет только в том log nслучае, если nпредставлено общее количество узлов в дереве. Но это не так.
Джулиан А.
@BenForsrup: это будет 3 ^ n, потому что у каждого узла будет три дочерних узла. Лучший способ убедиться в этом - нарисовать рекурсивное дерево с фиктивными значениями.
Шубхам
№ 2 должен быть n-5, а не n / 5
Fintasys
7

Мы можем доказать это математически, чего мне не хватало в приведенных выше ответах.

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

  1. T(n) = T(n-1) + 1Это означает, что время, необходимое для завершения метода, равно тому же самому методу, но с n-1, который T(n-1)мы сейчас и добавим, + 1потому что это время, которое требуется для выполнения общих операций (кроме T(n-1)). Теперь мы собираемся найти T(n-1)следующим образом : T(n-1) = T(n-1-1) + 1. Похоже, что теперь мы можем сформировать функцию, которая может дать нам какое-то повторение, чтобы мы могли полностью понять. Мы разместим с правой стороны , T(n-1) = ...а не T(n-1)внутри метода , T(n) = ...который даст нам: T(n) = T(n-1-1) + 1 + 1что является T(n) = T(n-2) + 2или другими словами , мы должны найти наш отсутствующий k: T(n) = T(n-k) + k. Следующим шагом является принятие n-kи утверждение, что, n-k = 1поскольку в конце рекурсии потребуется ровно O (1), когдаn<=0, Из этого простого уравнения мы теперь знаем это k = n - 1. Давайте поместим kв наш последний метод: T(n) = T(n-k) + kкоторый даст нам: T(n) = 1 + n - 1который точно nили O(n).
  2. То же самое, что и 1. Вы можете проверить это сами и увидеть, что вы получаете O(n).
  3. T(n) = T(n/5) + 1как и раньше, время завершения этого метода равно времени того же метода, но с n/5которым он ограничен T(n/5). Давайте найдем T(n/5)как в 1: T(n/5) = T(n/5/5) + 1что есть T(n/5) = T(n/5^2) + 1. Давайте место T(n/5)внутри T(n)для окончательного расчета: T(n) = T(n/5^k) + k. Опять же, как и раньше, n/5^k = 1что n = 5^kэто именно так , спрашивая , что в силе 5, даст нам п, ответ log5n = k(журнал базы 5). Поместим наши выводы в T(n) = T(n/5^k) + kследующим образом : T(n) = 1 + lognчтоO(logn)
  4. T(n) = 2T(n-1) + 1то, что мы имеем здесь, в основном то же, что и раньше, но на этот раз мы вызываем метод рекурсивно 2 раза, таким образом, мы умножаем его на 2. Давайте найдем, T(n-1) = 2T(n-1-1) + 1что есть T(n-1) = 2T(n-2) + 1. Наше следующее место, как и прежде, давайте разместим наш вывод: T(n) = 2(2T(n-2)) + 1 + 1что это T(n) = 2^2T(n-2) + 2нам дает T(n) = 2^kT(n-k) + k. Давайте найдем k, утверждая, n-k = 1что есть k = n - 1. Разместим kследующим образом: T(n) = 2^(n-1) + n - 1что примерноO(2^n)
  5. T(n) = T(n-5) + n + 1Это почти то же самое, что 4, но теперь мы добавляем, nпотому что у нас есть один forцикл. Давайте найдем T(n-5) = T(n-5-5) + n + 1что есть T(n-5) = T(n - 2*5) + n + 1. Поместим его: T(n) = T(n-2*5) + n + n + 1 + 1)что T(n) = T(n-2*5) + 2n + 2)и для к: T(n) = T(n-k*5) + kn + k)снова: n-5k = 1что является , n = 5k + 1что примерно n = k. Это даст нам: T(n) = T(0) + n^2 + nчто примерно O(n^2).

Теперь я рекомендую прочитать остальные ответы, которые сейчас дадут вам лучшую перспективу. Удачи, выиграв эти большие O :)

OhadM
источник
1

Ключевым моментом здесь является визуализация дерева вызовов. После этого сложность такова:

nodes of the call tree * complexity of other code in the function

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

Вместо этого общие узлы полного дерева вычисляются как

                  C^L - 1
                  -------  , when C>1
               /   C - 1
              /
 # of nodes =
              \    
               \ 
                  L        , when C=1

Где C - количество дочерних элементов каждого узла, а L - количество уровней дерева (включая корень).

Это легко визуализировать дерево. Начните с первого вызова (корневого узла), затем нарисуйте количество дочерних элементов, равное количеству рекурсивных вызовов в функции. Также полезно записать параметр, переданный во вспомогательный вызов, как «значение узла».

Итак, в приведенных выше примерах:

  1. дерево вызовов здесь C = 1, L = n + 1. Сложность остальной функции O (1). Следовательно, общая сложность L * O (1) = (n + 1) * O (1) = O (n)
n     level 1
n-1   level 2
n-2   level 3
n-3   level 4
... ~ n levels -> L = n
  1. дерево вызовов здесь C = 1, L = n / 5. Сложность остальной функции O (1). Следовательно, общая сложность L * O (1) = (n / 5) * O (1) = O (n)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
  1. дерево вызовов здесь C = 1, L = log (n). Сложность остальной функции O (1). Следовательно, общая сложность L * O (1) = log5 (n) * O (1) = O (log (n))
n
n/5
n/5^2
n/5^3
... ~ log5(n) levels -> L = log5(n)
  1. дерево вызовов здесь C = 2, L = n. Сложность остальной функции O (1). На этот раз мы используем полную формулу для числа узлов в дереве вызовов, потому что C> 1. Поэтому общая сложность равна (C ^ L-1) / (C-1) * O (1) = (2 ^ n - 1 ) * O (1) = O (2 ^ n) .
               n                   level 1
      n-1             n-1          level 2
  n-2     n-2     n-2     n-2      ...
n-3 n-3 n-3 n-3 n-3 n-3 n-3 n-3    ...     
              ...                ~ n levels -> L = n
  1. дерево вызовов здесь C = 1, L = n / 5. Сложность остальной функции O (n). Следовательно, общая сложность L * O (1) = (n / 5) * O (n) = O (n ^ 2)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
higlu
источник