Визначення складності для рекурсивних функцій (велика нотація 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);
}

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. Ця функція є базовою 5 log (n), оскільки кожен раз ми ділимо на 5 перед тим, як викликати функцію, тому її O(log(n))(база 5), яку часто називають логарифмічною і найчастіше для позначення великого 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 Нехай час циклу циклу дорівнює m. Коли рекурсивний запуск 1 раз, для виконання циклу потрібно m. Коли рекурсивний пробіг 2 рази, цикл також виконується 2 рази, тому він займає 2 м ... і так далі. Отже, це "*", а не "^".
bjc

3
@coder Пояснення для 5 видається дивним. Якщо збільшення на 2 призводить до n/2ітерацій forциклу, чому декрементування на 5 не призведе до n/5рекурсивних викликів? Це все-таки призведе до, O(n^2)але здається, більш зрозумілим поясненням. Навіщо поєднувати віднімання та ділення, коли вони є важливими, роблячи те саме?
Джек

1
@coder, тож для №4, якби у визначенні функції було 3 рекурсивні виклики, вона мала б часову складність O (3 ^ n)? І для 5 рекурсивних дзвінків було б O (5 ^ n), правильно?
rmutalik

1
@Jack Так, мені теж було цікаво. Вона повинна бути n/5НЕ n-5. І врешті-решт, ціле зведеться до O(N^2).
Ануй

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, і оскільки r <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)

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складністю і обумовленою циклом n. Загальна складність буде n*n.

Примітка. Це швидкий і брудний спосіб розрахунку складності (нічого офіційного!). Хочеться почути відгуки про це. Дякую.


Відмінна відповідь! У мене питання щодо четвертої функції. Якби у нього було три рекурсивні дзвінки, відповідь була б (3 ^ n). Або ти все одно просто скажеш (2 ^ n)?
Бен Форсруп

@Shubham: # 4 мені не здається правильним. Якщо кількість листя - 2^nто висота дерева повинна бути n, а не log n. Висота була б лише, log nякщо nпредставлена ​​загальна кількість вузлів на дереві. Але це не так.
Джуліан А.

@BenForsrup: Це буде 3 ^ n, оскільки кожен вузол матиме три дочірні вузли. Найкращий спосіб бути впевненим у цьому - намалювати рекурсивне дерево за допомогою фіктивних значень.
Shubham

# 2 має бути n-5, а не п / 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, дасть нам n, відповідь є 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 :)


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.