Що таке рекурсія і коли я повинен її використовувати?


121

Однією з тем, яка, схоже, регулярно з'являється у списках розсилки та обговорень в Інтернеті, є достоїнства (або їх відсутність) у виконанні ступеня інформатики. Аргумент, який, здається, виникає раз і знову для негативної сторони, полягає в тому, що вони кодують протягом певної кількості років і ніколи не застосовували рекурсії.

Тож питання:

  1. Що таке рекурсія?
  2. Коли я використовую рекурсію?
  3. Чому люди не використовують рекурсію?

9
А може бути , це допомагає: stackoverflow.com/questions/126756 / ...
kennytm

3
Це може допомогти зрозуміти концепцію: перейти за посиланням , представленої на другий коментар питання на цій сторінці і робити те , що коментарі говорять робити: stackoverflow.com/questions/3021 / ...
dtmland

Відповіді:


86

У цьому потоці є ряд хороших пояснень рекурсії , ця відповідь стосується того, чому ви не повинні використовувати його на більшості мов. * У більшості основних імперативних мовних реалізацій (тобто кожна основна реалізація C, C ++, Basic, Python , Ітерація Ruby, Java та C #) переважно краща для рекурсії.

Щоб зрозуміти, чому, пройдіться кроками, які описані вище мови використовують для виклику функції:

  1. на стеку вирізано простір для аргументів функції та локальних змінних
  2. Аргументи функції копіюються в цей новий простір
  3. контрольні стрибки до функції
  4. код функції працює
  5. результат функції копіюється у повернене значення
  6. стек повертається до попереднього положення
  7. керування відскакує туди, куди викликалася функція

Виконання всіх цих кроків вимагає часу, як правило, трохи більше, ніж потрібно для перегляду циклу. Однак справжня проблема полягає в кроці №1. Коли багато програм запускаються, вони виділяють один шматок пам'яті для свого стека, а коли у них вичерпається ця пам'ять (часто, але не завжди через рекурсію), програма виходить з ладу через переповнення стека .

Тож у цих мовах рекурсія проходить повільніше, і це робить вас вразливим до збоїв. Однак є ще деякі аргументи для його використання. Взагалі, код, записаний рекурсивно, коротший і трохи елегантніший, як тільки ви знаєте, як його читати.

Існує техніка, за допомогою якої реалізатори мови можуть використовувати оптимізацію хвостових викликів, яка може усунути деякі класи переповнення стека. Якщо стисло сказати: якщо зворотний вираз функції є просто результатом виклику функції, то вам не потрібно додавати новий рівень у стек, ви можете повторно використовувати поточний для функції, що викликається. На жаль, в деяких імперативних мовних реалізаціях вбудована оптимізація хвостових викликів.

* Я люблю рекурсії. Моя улюблена статична мова взагалі не використовує циклів, рекурсія - єдиний спосіб робити щось повторно. Я просто не думаю, що рекурсія, як правило, є хорошою ідеєю на мовах, які не налаштовані на неї.

** До речі, Маріо, типовою назвою для вашої функції ArrangeString є "приєднатися", і я буду здивований, якщо у вашій вибраній мові ще немає її реалізації.


1
Приємно бачити пояснення властивої рекурсії. Я торкнувся цього і у своїй відповіді. Але для мене велика сила з рекурсією - це те, що ви можете зробити зі стеком викликів. Ви можете написати лаконічний алгоритм з рекурсією, який повторно розгалужується, що дозволяє вам робити такі речі, як ієрархії сканування (стосунки батьків / дитини) з легкістю. Дивіться мою відповідь для прикладу.
Стів Вортем

7
Дуже розчарований, коли знайшов верхню відповідь на питання під назвою "Що таке рекурсія і коли я повинен її використовувати?" насправді не відповідайте ні на одне з них, не майте на увазі надзвичайно упередженого попередження щодо рекурсії, незважаючи на широке використання у більшості згаданих вами мов (немає нічого конкретно неправильного у тому, що ви сказали, але, здається, ви перебільшуєте проблему і недооцінюєте корисність).
Бернхард Баркер

2
Ви, мабуть, праві @Dukeling. У контексті, коли я писав цю відповідь, було написано багато чудових пояснень рекурсії, і я написав це, маючи намір бути доповненням до цієї інформації, а не верхньою відповіддю. На практиці, коли мені потрібно ходити по дереву або обробляти будь-яку іншу вкладену структуру даних, я зазвичай звертаюся до рекурсії, і мені ще належить потрапити в переповнення стека мого власного створення в дикій природі.
Пітер Бернс

63

Простий англійський приклад рекурсії.

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
up + for heart touch :)
Suhail Mumtaz Awan

Існує подібна історія, як ця для маленьких дітей, які не засинають у китайських народних казках, я просто згадав про це, і це нагадує мені, як працює реальна рекурсія у світі.
Харві Лін

49

У самому базовому сенсі інформатики рекурсія - це функція, яка викликає себе. Скажімо, у вас пов’язана структура списку:

struct Node {
    Node* next;
};

І ви хочете дізнатися, наскільки тривалий список пов'язаний, ви можете робити це за допомогою рекурсії:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Звичайно, це може бути зроблено і для циклу for, але це корисно як ілюстрацію концепції)


@Christopher: Це приємний, простий приклад рекурсії. Зокрема, це приклад хвостової рекурсії. Однак, як заявив Андреас, його можна легко переписати (більш ефективно) циклом for. Як я пояснюю у своїй відповіді, для рекурсії кращі способи використання.
Стів Вортем

2
Вам справді потрібна інша заява тут?
Адрієн Бе

1
Ні, це лише для ясності.
Андреас Брінк

@SteveWortham: Це не рекурсивно, як написано; length(list->next)ще потрібно повернутися до length(list)цього, щоб останній міг додати 1 до результату. Чи було написано, щоб пропустити довжину так далеко, тільки тоді ми могли забути, що абонент існував. Як int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

Щоразу, коли функція викликає себе, створюючи цикл, то це рекурсія. Як і в усьому, що є корисними та поганими для рекурсії.

Найпростіший приклад - хвостова рекурсія, де останній рядок функції - це виклик до себе:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

Однак це кульгавий, майже безглуздий приклад, оскільки його можна легко замінити більш ефективною ітерацією. Зрештою, рекурсія страждає від накладних викликів функцій, що у наведеному вище прикладі може бути суттєвим порівняно з операцією всередині самої функції.

Отже, вся причина для рекурсії, а не ітерації повинна полягати в тому, щоб скористатися стеком викликів, щоб зробити якісь розумні речі. Наприклад, якщо ви викликаєте функцію кілька разів з різними параметрами всередині одного циклу, то це спосіб досягти розгалуження . Класичний приклад - трикутник Серпінського .

введіть тут опис зображення

Ви можете намалювати один із них дуже просто за допомогою рекурсії, де стек викликів ведеться в трьох напрямках:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

Якщо ви спробуєте зробити те ж саме з ітерацією, я думаю, що ви знайдете, що потрібно зробити набагато більше коду.

Інші випадки поширеного використання можуть включати перехід ієрархій, наприклад, сканери веб-сайтів, порівняння каталогів тощо.

Висновок

На практиці рекурсія має найбільше значення, коли вам потрібно ітераційне розгалуження.


27

Рекурсія - це метод вирішення завдань, заснований на поділі та підкоренні менталітету. Основна ідея полягає в тому, що ви берете оригінальну проблему і поділяєте її на менші (легше вирішувані) екземпляри, вирішуєте ці менші екземпляри (зазвичай, використовуючи знову той самий алгоритм), а потім знову збираєте їх до остаточного рішення.

Канонічний приклад - це рутина для генерування Російського фактору. Коефіцієнт n обчислюється шляхом множення всіх чисел між 1 і n. Ітераційне рішення в C # виглядає приблизно так:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

У ітеративному рішенні немає нічого дивного, і це повинно мати сенс для всіх, хто знайомий із C #.

Рекурсивне рішення знаходимо, визнаючи, що n-й Фактор є n * Фактом (n-1). Або кажучи іншим способом, якщо ви знаєте, що таке конкретне Факторне число, ви можете обчислити наступне. Ось рекурсивне рішення в C #:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

Перша частина цієї функції відома як базовий випадок (або іноді застереження про охорону) і саме це заважає алгоритму працювати вічно. Він просто повертає значення 1 щоразу, коли функція викликається зі значенням 1 або менше. Друга частина є цікавішою і відома як Рекурсивний крок . Тут ми називаємо той самий метод з дещо зміненим параметром (зменшуємо його на 1), а потім множимо результат на нашу копію n.

При першому зіткненні це може бути дещо заплутаним, тому доцільно вивчити, як це працює під час запуску. Уявіть, що ми називаємо FactRec (5). Ми входимо в рутину, не підбираються базовою справою, і ми закінчуємо так:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

Якщо ми повторно введемо метод із параметром 4, нас знову не зупиняє охоронний пункт, і ми закінчуємо:

// In FactRec(4)
return 4 * FactRec(3);

Якщо ми замінимо це повернене значення на повернене вище значення, яке ми отримаємо

// In FactRec(5)
return 5 * (4 * FactRec(3));

Це має дати вам уявлення про те, як виходить остаточне рішення, тому ми будемо швидко простежувати та показувати кожен крок на шляху вниз:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

Ця остаточна заміна відбувається, коли спрацьовує базовий випадок. На даний момент у нас є проста алгребраїчна формула, для вирішення якої в першу чергу прирівнюється до визначення Факторіалів.

Повчально зазначити, що кожен виклик методу призводить до запуску базового випадку або виклику того ж методу, де параметри ближче до базового випадку (часто називають рекурсивним викликом). Якщо це не так, то метод буде працювати назавжди.


2
Хороше пояснення, але я думаю, що важливо зазначити, що це просто рекурсія хвоста і не дає переваги перед ітераційним рішенням. Це приблизно однакова кількість коду, і він працюватиме повільніше через накладні виклики функції.
Стів Вортем

1
@SteveWortham: Це не рецидиви хвоста. На етапі рекурсивного результату FactRec()потрібно помножити на, nперш ніж повернутися.
rvighne

12

Рекурсія - це вирішення проблеми з функцією, яка викликає себе. Хорошим прикладом цього є факторіальна функція. Факторний - це математична задача, де, наприклад, коефіцієнт 5 становить 5 * 4 * 3 * 2 * 1. Ця функція вирішує цю функцію в C # для додатних цілих чисел (не перевірена - може виникнути помилка).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

Рекурсія стосується методу, який вирішує проблему, вирішуючи меншу версію проблеми, а потім використовуючи цей результат плюс деякі інші обчислення для формулювання відповіді на вихідну проблему. Часто, в процесі вирішення меншої версії, метод вирішить ще меншу версію проблеми тощо, поки не досягне «базового випадку», який тривіально вирішити.

Наприклад, для обчислення факторіалу для числа Xможна представити його як X times the factorial of X-1. Таким чином, метод "повторюється", щоб знайти фактор X-1, а потім примножує все, що він отримав, Xщоб дати остаточну відповідь. Звичайно, щоб знайти факторіал X-1, спершу обчислимо факторіал X-2тощо. Базовим випадком буде X0 або 1; у такому випадку він знає, що повертається 1з тих пір 0! = 1! = 1.


1
Я думаю, що те, на що ви звертаєтесь, - це не рекурсія, а принцип дизайну алгоритму <a href=" en.wikipedia.org/wiki/… та Conquer</a> . org / wiki / Ackermann_function "> Ackermans function </a>.
Габріель Шчербак

2
Ні, я не маю на увазі науково-дослідну діяльність. D&C означає, що існує дві або більше підпроблем, сама рекурсія не є (наприклад, наведений тут факторний приклад не є D&C - він повністю лінійний). D&C - це по суті підмножина рекурсії.
Бурштин

3
Цитується за точної статті ви пов'язані між собою: «розділяй і володарюй алгоритм роботи по рекурсивно розчленовування завдання на дві або більше подзадач того ж (або пов'язаної) типу,»
Амбер

Я не думаю, що це є чудовим поясненням, оскільки рекурсія, суворо кажучи, зовсім не повинна вирішувати проблему. Ви могли просто назвати себе (І переповнювати).
UK-AL

Я використовую ваше пояснення у статті, яку я пишу для PHP Master, хоча я не можу приписувати це вам. Сподіваюся, ви не заперечуєте.
frostymarvelous

9

Розглянемо стару, добре відому проблему :

У математиці найбільший спільний дільник (gcd)… з двох або більше ненульових цілих чисел - це найбільше додатне ціле число, яке ділить числа без залишку.

Визначення gcd напрочуд просте:

визначення gcd

де mod - оператор модуля (тобто залишок після цілого поділу).

В англійській мові це визначення говорить, що найбільший спільний дільник будь-якого числа і нуля - це число, а найбільший спільний дільник двох чисел m і n - найбільший спільний дільник на n і решту після ділення m на n .

Якщо ви хочете знати, чому це працює, перегляньте статтю Вікіпедії про алгоритм Евкліда .

Давайте обчислимо gcd (10, 8) як приклад. Кожен крок дорівнює тому, що знаходиться безпосередньо перед ним:

  1. gcd (10, 8)
  2. gcd (10, 10 мод 8)
  3. gcd (8, 2)
  4. gcd (8, 8 мод 2)
  5. gcd (2, 0)
  6. 2

На першому кроці 8 не дорівнює нулю, тому застосовується друга частина визначення. 10 мод 8 = 2, оскільки 8 переходить у 10 один раз із залишком 2. На кроці 3 друга частина застосовується знову, але на цей раз 8 мод 2 = 0, оскільки 2 ділить 8 без залишку. На кроці 5 другий аргумент - 0, тому відповідь - 2.

Ви помітили, що gcd з’являється і з лівої, і з правої сторони знака рівності? Математик сказав, що це визначення є рекурсивним, оскільки вираз, який ви визначаєте, повторюється всередині його визначення.

Рекурсивні визначення, як правило, елегантні. Наприклад, рекурсивне визначення суми списку є

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

де headперший елемент у списку і tailрешта списку. Зауважимо, що sumповторюється всередині його визначення наприкінці.

Можливо, ви бажаєте замість цього максимальне значення у списку:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

Ви можете визначити множення невід'ємних цілих чисел рекурсивно, щоб перетворити його на ряд доповнень:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

Якщо цей біт про перетворення множення на ряд доповнень не має сенсу, спробуйте розгорнути кілька простих прикладів, щоб побачити, як це працює.

Сортування сортування має прекрасне рекурсивне визначення:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

Рекурсивні визначення є навколо, якщо ви знаєте, що шукати. Зауважте, як усі ці визначення мають дуже прості базові випадки, наприклад , gcd (m, 0) = m. Рекурсивні випадки відхиляються від проблеми, щоб перейти до простих відповідей.

З таким розумінням тепер ви можете оцінити інші алгоритми у статті Вікіпедії про рекурсію !


8
  1. Функція, яка викликає себе
  2. Коли функцію можна (легко) розкласти на просту операцію плюс ту саму функцію на деякій меншій частині проблеми. Слід сказати, швидше, що це робить його хорошим кандидатом на рекурсію.
  3. Вони роблять!

Канонічним прикладом є факториал, який виглядає так:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

Взагалі, рекурсія не обов'язково швидка (накладні виклики функцій мають тенденцію бути високими, оскільки рекурсивні функції, як правило, невеликі, див. Вище) і можуть страждати від деяких проблем (стек переповнює когось?). Деякі кажуть, що, як правило, важко отримати «правильність» у нетривіальних випадках, але я не дуже в цьому намагаюся. У деяких ситуаціях рекурсія має найбільш сенс і є найелегантнішим і зрозумілішим способом написання певної функції. Слід зазначити, що деякі мови надають перевагу рекурсивним рішенням та оптимізують їх набагато більше (на думку LISP).


6

Рекурсивна функція - це та, яка викликає себе. Найпоширенішою причиною, яку я знайшов для цього, є пересування структури дерева. Наприклад, якщо у мене є TreeView з прапорцями (думаю, що встановлення нової програми, "виберіть функції для встановлення" на сторінці), я можу захотіти кнопку "перевірити все", яка була б приблизно такою (псевдокод):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

Таким чином, ви можете бачити, що checkRecursively спочатку перевіряє вузол, через який він переданий, а потім викликає себе для кожного з дітей цього вузла.

Вам потрібно бути трохи обережними з рекурсією. Якщо ви потрапите в нескінченний рекурсивний цикл, ви отримаєте виняток переповнення стека :)

Я не можу придумати причину, чому люди не повинні його використовувати, коли це доречно. Це корисно в одних обставинах, а не в інших.

Я думаю, що оскільки це цікава техніка, деякі кодери, можливо, в кінцевому підсумку використовують її частіше, ніж повинні, без реального обґрунтування. Це дало рекурсії погану назву в деяких колах.


5

Рекурсія - це вираз, що прямо чи опосередковано посилається на себе.

Розглянемо рекурсивні абревіатури як простий приклад:

  • GNU означає, що GNU не є Unix
  • PHP означає PHP: Гіпертекстовий препроцесор
  • YAML означає YAML Ain't Markup Language
  • WINE означає Wine Not Emulator
  • VISA - Міжнародна асоціація послуг Visa

Більше прикладів у Вікіпедії


4

Рекурсія найкраще працює з тим, що я люблю називати "фрактальними проблемами", де ви маєте справу з великою річчю, яка складається з менших версій тієї великої речі, кожна з яких - це ще менша версія великої речі тощо. Якщо вам коли-небудь доводиться переходити або шукати щось на зразок дерева або вкладених однакових структур, у вас виникла проблема, яка може стати хорошим кандидатом на рекурсію.

Люди уникають рекурсії з кількох причин:

  1. Більшість людей (включаючи мене) вирізають зуби програмування на процедурне або об'єктно-орієнтоване програмування на відміну від функціонального програмування. Для таких людей ітеративний підхід (як правило, використовуючи петлі) відчуває себе більш природним.

  2. Тим із нас, хто ріже зуби програмування на процедурному або об'єктно-орієнтованому програмуванні, часто кажуть уникати рекурсії, оскільки він схильний до помилок.

  3. Нам часто кажуть, що рекурсія повільна. Виклик та повернення з звичайної процедури неодноразово передбачає багато натискання та вискоку стека, що відбувається повільніше, ніж циклічне циклічне повторення. Я думаю, що деякі мови справляються з цим краще, ніж інші, і ці мови, швидше за все, не такі, де панівна парадигма є процедурною або об'єктно-орієнтованою.

  4. Принаймні пару мов програмування, якими я користувався, я пам’ятаю, слухаючи рекомендації не використовувати рекурсію, якщо вона виходить за певну глибину, оскільки її стек не такий глибокий.


4

Рекурсивне твердження - це те, в якому ви визначаєте процес того, що робити далі, як комбінацію входів і того, що ви вже зробили.

Наприклад, візьміть факториал:

factorial(6) = 6*5*4*3*2*1

Але легко побачити факторіал (6) також є:

6 * factorial(5) = 6*(5*4*3*2*1).

Так загалом:

factorial(n) = n*factorial(n-1)

Звичайно, хитра річ про рекурсію полягає в тому, що якщо ви хочете визначити речі з точки зору того, що ви вже зробили, потрібно почати десь з чого почати.

У цьому прикладі ми просто робимо особливий випадок, визначаючи факторіал (1) = 1.

Тепер ми бачимо це знизу вгору:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

Оскільки ми визначили факторіал (1) = 1, ми доходимо до "дна".

Взагалі, рекурсивні процедури мають дві частини:

1) Рекурсивна частина, яка визначає певну процедуру з точки зору нових входів у поєднанні з тим, що ви "вже зробили" за допомогою тієї ж процедури. (тобто factorial(n) = n*factorial(n-1))

2) Базова частина, яка гарантує, що процес не повториться назавжди, даючи йому місце для початку (тобто factorial(1) = 1)

Спочатку можна трохи заплутати, але просто подивіться на приклади, і все повинно зібратися разом. Якщо ви хочете набагато глибше зрозуміти концепцію, вивчіть математичну індукцію. Також пам’ятайте, що деякі мови оптимізують для рекурсивних дзвінків, а інші -. Зробити шалено повільні рекурсивні функції досить просто, якщо ви не обережні, але існують також методи, які дозволяють зробити їх ефективними у більшості випадків.

Сподіваюся, це допомагає ...


4

Мені подобається таке визначення: під час
рекурсії рутина сама вирішує невелику частину проблеми, ділить проблему на більш дрібні частини, а потім закликає вирішити кожну з менших частин.

Мені також подобається обговорення рекурсії Стіва МакКоннелла в Code Complete, де він критикує приклади, використані в книгах з комп'ютерних наук про рекурсію.

Не використовуйте рекурсії для факторіалів чи чисел Фібоначчі

Одна з проблем підручників з інформатики полягає в тому, що вони представляють нерозумні приклади рекурсії. Типовими прикладами є обчислення факторної чи обчислення послідовності Фібоначчі. Рекурсія - це потужний інструмент, і використовувати її в будь-якому з цих випадків дуже просто. Якщо програміст, який працював на мене, використовував рекурсію для обчислення факторіалу, я б найняв когось іншого.

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

EDIT: Це відповідь не було, коли відповідь Дава - я не бачив такої відповіді, коли публікував це


6
Більшість причин, чому факториали або послідовності виправдання використовуються в якості прикладів, це те, що вони є загальними предметами, які визначені рекурсивно, і таким чином вони природним чином піддаються прикладам рекурсії для їх обчислення - навіть якщо це насправді не найкращий метод з точки зору КС.
Бурштин

Я згоден - я щойно, читаючи книгу, виявив, що це цікавий момент підняти посеред розділу про рекурсію
Robben_Ford_Fan_boy

4

1.) Метод є рекурсивним, якщо він може викликати себе; або безпосередньо:

void f() {
   ... f() ... 
}

або опосередковано:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.) Коли використовувати рекурсію

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.) Люди використовують рекурсію лише тоді, коли дуже складно написати ітераційний код. Наприклад, методи переходу дерев, такі як попереднє замовлення, післяпорядкування, можуть бути ітераційними та рекурсивними. Але зазвичай ми використовуємо рекурсивні через його простоту.


Що з зменшенням складності, коли ділимося і перемагаємо щодо перф?
mfrachet

4

Ось простий приклад: скільки елементів у наборі. (Є кращі способи порахувати речі, але це приємний простий рекурсивний приклад.)

По-перше, нам потрібно два правила:

  1. якщо набір порожній, кількість елементів у наборі дорівнює нулю (duh!).
  2. якщо набір не порожній, кількість рахунків становить один плюс кількість елементів у наборі після вилучення одного елемента.

Припустимо, у вас такий набір: [xxx]. давайте порахуємо, скільки предметів існує.

  1. набір є [xxx], який не порожній, тому ми застосовуємо правило 2. кількість елементів дорівнює одному плюс кількість елементів у [xx] (тобто ми видалили елемент).
  2. набір є [xx], тому ми знову застосовуємо правило 2: один + кількість елементів у [x].
  3. набір є [x], який досі відповідає правилу 2: один + кількість елементів у [].
  4. Тепер безліч [], що відповідає правилу 1: кількість дорівнює нулю!
  5. Тепер, коли ми знаємо відповідь на кроці 4 (0), ми можемо вирішити крок 3 (1 + 0)
  6. Аналогічно, тепер, коли ми знаємо відповідь на кроці 3 (1), ми можемо вирішити крок 2 (1 + 1)
  7. І нарешті, тепер, коли ми знаємо відповідь на кроці 2 (2), ми можемо вирішити крок 1 (1 + 2) і отримати кількість елементів у [xxx], що є 3. Ура!

Ми можемо представити це як:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

Застосовуючи рекурсивне рішення, у вас зазвичай є щонайменше 2 правила:

  • основа, простий випадок, в якому йдеться про те, що відбувається, коли ви "використали" всі свої дані. Зазвичай це певна різниця "якщо у вас немає даних для обробки, ваша відповідь - X"
  • рекурсивне правило, яке вказує, що станеться, якщо у вас ще є дані. Зазвичай це якесь правило, яке говорить: "зробіть щось, щоб зменшити набір даних, і повторно застосуйте свої правила до меншого набору даних".

Якщо перевести вищезазначене в псевдокод, ми отримаємо:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

Є набагато більше корисних прикладів (наприклад, обхід дерева), які я впевнений, що охоплюватимуть інші.


3

Ну, це досить пристойне визначення у вас. І у вікіпедії теж є хороше визначення. Тому я додам ще одне (можливо, гірше) визначення для вас.

Коли люди посилаються на "рекурсію", вони зазвичай говорять про написану ними функцію, яка викликає себе неодноразово, поки це не буде виконано з її роботою. Рекурсія може бути корисною при переміщенні ієрархій у структурах даних.


3

Приклад: Рекурсивне визначення сходів таке: сходи складаються з: - одного кроку та сходи (рекурсія) - або лише одного кроку (припинення)


2

Для повторення вирішеної проблеми: нічого не робіть, ви закінчили.
Для повторного відкриття проблеми: виконайте наступний крок, а потім повторіть решту.


2

Простий англійською мовою: припустимо, що ви можете робити 3 речі:

  1. Візьміть одне яблуко
  2. Запишіть оціночні позначки
  3. Порахуйте підрахунки марок

У вас на столі багато яблук, і ви хочете знати, скільки там яблук.

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

Процес повторення того ж самого, поки ви не закінчите, називається рекурсією.

Я сподіваюся, що це відповідь "простої англійської", яку ви шукаєте!


1
Зачекайте, у мене на столі дуже багато знаків талію, і тепер я хочу знати, скільки там знаків. Чи можу я якось використовувати для цього яблука?
Крістофер Хаммарстрем

Якщо ви берете яблуко з землі (коли ви кладете їх туди під час процесу) і кладете його на стіл кожного разу, коли ви подряпаєте одну позначку списку, поки не залишиться відміток, я впевнений, що ви в кінцевому підсумку кількість яблук на столі дорівнює кількості марок, які ви мали. Тепер просто порахуйте ці яблука для миттєвого успіху! (зауважте: цей процес вже не рекурсія, а нескінченна петля)
Бастіанські Ліндери

2

Рекурсивна функція - це функція, яка містить виклик до себе. Рекурсивна структура - це структура, яка містить примірник себе. Ви можете комбінувати два як рекурсивний клас. Ключовою частиною рекурсивного елемента є те, що він містить примірник / виклик себе.

Розгляньте два дзеркала, звернені один до одного. Ми бачили акуратний ефект нескінченності, який вони роблять. Кожне відображення - це екземпляр дзеркала, який міститься в іншому екземплярі дзеркала тощо. Дзеркало, що містить відображення самого себе, є рекурсією.

Бінарне дерево є хорошим прикладом програмування рекурсії. Структура рекурсивна для кожного Вузла, що містить 2 екземпляри Вузла. Функції для роботи над двійковим деревом пошуку також є рекурсивними.


2

Це старе питання, але я хочу додати відповідь з логістичної точки зору (тобто не з точки зору правильності алгоритму чи точки зору продуктивності).

Я використовую Java для роботи, а Java не підтримує вкладені функції. Як такий, якщо я хочу зробити рекурсію, мені, можливо, доведеться визначити зовнішню функцію (яка існує лише тому, що мій код протиставляється бюрократичному правилу Java), або мені доведеться взагалі перефактурувати код (що я дуже ненавиджу робити).

Таким чином, я часто уникаю рекурсії і використовую натомість операцію стека, тому що сама рекурсія по суті є операцією стека.


1

Ви хочете використовувати його будь-коли у вас є структура дерева. Це дуже корисно при читанні XML.


1

Рекурсія, як це стосується програмування, - це, в основному, виклик функції з її власного визначення (всередині себе), з різними параметрами для виконання завдання.


1

"Якщо у мене є молоток, зробіть все схожим на цвях".

Рекурсія - величезна стратегія вирішення проблем проблем, де на кожному кроці просто "перетворюючи 2 дрібні речі в одну більшу річ" кожен раз одним і тим же молотком.

Приклад

Припустимо, ваш стіл вкритий неорганізованим безладом 1024 папери. Як зробити один акуратний, чистий стос паперів з безладу, використовуючи рекурсію?

  1. Розділити: розкладіть усі аркуші, щоб у кожного "стопки" було лише один аркуш.
  2. Підкорити:
    1. Обійдіть, поклавши кожен аркуш поверх одного іншого аркуша. Тепер у вас є стеки по 2.
    2. Обійдіть, поклавши кожну 2-х стек поверх іншої 2-стопки. Тепер у вас є стеки 4.
    3. Обійдіть, поклавши кожен 4-х стек поверх іншого 4-стогового. Тепер у вас є стеки 8.
    4. ... знову і знову ...
    5. Тепер у вас є одна величезна стопка з 1024 аркушів!

Зауважте, що це досить інтуїтивно, окрім підрахунку всього (що не обов'язково). Насправді ви можете не спуститись до 1-х аркушів, але ви могли б і все одно працювати. Важлива частина - молоток: Зі своїми руками ви завжди можете покласти один стоп зверху на інший, щоб зробити більшу пачку, і не має значення (в межах причини), наскільки великим є будь-який стек.


6
Ви описуєте поділ і перемагайте. Хоча це приклад рекурсії, він аж ніяк не єдиний.
Конрад Рудольф

Добре. Я не намагаюся зафіксувати [світ рекурсії] [1] у реченні. Я хочу інтуїтивного пояснення. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Андрес Яан Так

1

Рекурсія - це процес, коли метод вимагає виконувати певне завдання. Це зменшує надмірність коду. Більшість рекурсивних функцій або методів повинні мати кондифітон, щоб перервати рекусивний виклик, тобто зупинити його від виклику, якщо виконується умова - це запобігає створенню нескінченного циклу. Не всі функції підходять для рекурсивного використання.


1

Ей, вибачте, якщо моя думка з кимсь погоджується, я просто намагаюся пояснити рекурсію простою англійською.

припустимо, у вас є троє менеджерів - Джек, Джон та Морган. Джек керує двома програмістами, Джон - 3 та Морган - 5. Ви збираєтесь дати кожному менеджеру по 300 доларів і хочете знати, що це коштуватиме. Відповідь очевидна - але що робити, якщо 2 співробітники Morgan-s також є менеджерами?

ТУТ приходить рекурсія. ви починаєте з вершини ієрархії. літня вартість 0 $. ви починаєте з Джека, а потім перевіряйте, чи є у нього менеджери як співробітники. якщо ви виявите, що хтось із них є, перевірте, чи є у них менеджери як співробітники тощо. Додайте 300 доларів до літніх витрат кожного разу, коли ви знайдете менеджера. коли ви закінчите з Джеком, ідіть до Джона, його співробітників, а потім до Моргана.

Ви ніколи не дізнаєтесь, скільки циклів пройдете, перш ніж отримати відповідь, хоча ви знаєте, скільки у вас є менеджерів і скільки бюджету ви можете витратити.

Рекурсія - це дерево, з гілками та листям, яке називають відповідно батьки та діти. Використовуючи алгоритм рекурсії, ви більш-менш свідомо будуєте дерево з даних.


1

Простий англійською мовою рекурсія означає повторювати щось знову і знову.

У програмуванні одним із прикладів є виклик функції всередині себе.

Подивіться на наступний приклад обчислення факторіалу числа:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
Простий англійською мовою повторювати щось знову і знову називається ітерацією.
toon81

1

Будь-який алгоритм має структурну форму рекурсію на типі даних, якщо він в основному складається з оператора перемикання з випадком для кожного випадку типу даних.

наприклад, коли ви працюєте над типом

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

структурний рекурсивний алгоритм мав би форму

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

це дійсно найбільш очевидний спосіб написання будь-якого алгоритму, який працює на структурі даних.

тепер, коли ви дивитеся на цілі числа (ну, натуральні числа), як визначено за допомогою аксіом Пеано

 integer = 0 | succ(integer)

ви бачите, що структурний рекурсивний алгоритм на цілі числа виглядає приблизно так

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

надто відома факторіальна функція - про найтривіальніший приклад цієї форми.


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