Рекурсія без факторіалів, числа Фібоначчі тощо


48

Майже кожна стаття, яку я можу знайти про рекурсію, включає приклади факторіальних чи цифр Фібоначчі, які є:

  1. Математика
  2. Марно в реальному житті

Є деякі цікаві без математики коди прикладів , щоб навчити рекурсії?

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


26
Хоча ваше питання цілком справедливе, я б вагався називати цифри Фібоначчі марними в реальному житті . Те саме стосується і факторських .
Zach L

2
Маленький Схемер - це ціла книга про рекурсії, яка ніколи не використовує Факт чи Фіб. junix-linux-config.googlecode.com/files/…
Ерік Вілсон

5
@Zach: Незважаючи на це, рекурсія - це жахливий спосіб реалізації чисел Фібоначчі через експоненціальний час роботи.
dan04

2
@ dan04: Рекурсія - це жахливий спосіб реалізувати майже все, що пояснюється можливістю переповнення стека у більшості ланагуг.
Р ..

5
@ dan04, якщо ваша мова не є достатньо розумною, щоб здійснити оптимізацію хвостових викликів, як і більшість функціональних мов, і в цьому випадку вона працює чудово
Zachary K

Відповіді:


107

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

void GetAllFilePaths(Directory dir, List<string> paths)
{
    foreach(File file in dir.Files)
    {
        paths.Add(file.Path);
    }

    foreach(Directory subdir in dir.Directories)
    {
        GetAllFilePaths(subdir, paths)
    }
}

List<string> GetAllFilePaths(Directory dir)
{
    List<string> paths = new List<string>();
    GetAllFilePaths(dir, paths);
    return paths;
}

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

9
Примітка: команда unix часто відключає параметр -r (cp або rm для primeple). -r підставка для рекурсивної.
deadalnix

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

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

1
У деяких файлових системах для каталогів є й інші особливості, наприклад, точки повторного розбору NTFS. моя думка полягає в тому, що код, який цього не спеціально знає, може мати несподівані результати в реальній файловій системі
jk.

51

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

Що варто подумати:

  • файлові системи - це в основному дерева; Приємним завданням програмування було б отримати всі .jpg зображення під певним каталогом та всіма його підкаталогами
  • предка - зважаючи на генеалогічне дерево, знайдіть кількість поколінь, до яких потрібно пройти, щоб знайти спільного предка; або перевірити, чи належать двоє людей на дереві до одного покоління; або перевірте, чи можуть дві людини на дереві законно одружитися (залежить від юрисдикції :)
  • HTML-подібні документи - конвертують між послідовним (текстовим) поданням документа та деревом DOM; виконувати операції над підмножинами DOM (можливо, навіть реалізувати підмножину xpath?); ...

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


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

1
Що стосуються цього вказівники? Навіть коли у вас є вказівники на першу дитину, на наступний брат та сестру та батьків, вам все одно доведеться якось ходити по вашому дереву, а в деяких випадках рекурсія є найбільш можливим способом.
tdammers

41

https://stackoverflow.com/questions/105838/real-world-examples-of-recursion

  • моделювання заразної інфекції
  • генерування геометрії
  • управління каталогами
  • сортування

https://stackoverflow.com/questions/2085834/how-did-you-practically-use-recursion

  • проміння
  • шахи
  • аналіз вихідного коду (граматика мови)

https://stackoverflow.com/questions/4945128/what-is-a-good-example-of-recursion-other-than-generating-a-fibach-sequence

  • BST пошук
  • Вежі Ханої
  • пошук паліндром

https://stackoverflow.com/questions/126756/examples-of-recursive-functions

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

10
Хоча це теоретично може відповісти на питання, бажано було б сюди включити істотні частини цих питань та відповіді та надати посилання для довідок. Якщо питання коли-небудь видалять із SO, ваша відповідь буде абсолютно марною.
Адам Лір

@Anna Ну що ж, користувачі не можуть видалити свої запитання, наскільки це ймовірно?
vemv

4
@vemv Видаліть голоси, модератори, правила про те, що змінюється на темі ... це може статися. У будь-якому випадку, більш повною відповіддю тут було б краще, ніж надсилати відвідувача на чотири різні сторінки прямо з місця.
Адам Лір

@Anna: Дотримуючись цього роздуму, кожне запитання, що закривається як "точний дублікат", має відповісти з оригіналу, який було вставлено. Це питання є точним дублікатом (із запитань на SO), чому він повинен отримувати інше лікування, ніж точне копії запитань про програмістів?
СФ.

1
@SF Якби ми могли закрити його як дублікат, ми б це зробили, але дублікати між веб-сайтами не підтримуються. Програмісти - це окремий сайт, тому в ідеалі відповіді тут використовуватимуть SO як будь-яку іншу посилання, а не делегувати її повністю. Це не інакше, ніж просто сказати "ваша відповідь у цій книжці" - технічно правдива, але її неможливо використати відразу, не звернувшись до посилань.
Адам Лір

23

Ось ще кілька практичних проблем, які мені спадають на думку:

  • Об’єднати сортування
  • Двійковий пошук
  • Обхід, введення та видалення на деревах (в основному використовується в додатках бази даних)
  • Генератор перестановок
  • Рішення для судоку (із зворотним треком)
  • Перевірка орфографії (знову із зворотним відстеженням)
  • Аналіз синтаксису (.eg, програма, яка перетворює префікс у нотацію постфікса)

11

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


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

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

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

8

Сортування, визначене рекурсивно в Python.

def sort( a ):
    if len(a) == 1: return a
    part1= sort( a[:len(a)//2] )
    part2= sort( a[len(a)//2:] )
    return merge( part1, part2 )

Злиття, визначене рекурсивно.

def merge( a, b ):
    if len(b) == 0: return a
    if len(a) == 0: return b
    if a[0] < b[0]:
        return [ a[0] ] + merge(a[1:], b)
    else:
        return [ b[0] ] + merge(a, b[1:]) 

Лінійний пошук, визначений рекурсивно.

def find( element, sequence ):
    if len(sequence) == 0: return False
    if element == sequence[0]: return True
    return find( element, sequence[1:] )

Двійковий пошук, визначений рекурсивно.

def binsearch( element, sequence ):
    if len(sequence) == 0: return False
    mid = len(sequence)//2
    if element < mid: 
        return binsearch( element, sequence[:mid] )
    else:
        return binsearch( element, sequence[mid:] )

6

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

Деякі приклади, які не включають математику для навчання рекурсії (принаймні, ті проблеми, які я пам’ятаю з моїх університетських років):

Ось приклади використання Backtracking для вирішення проблеми.

Інші проблеми - класика домену штучного інтелекту: використання глибинного першого пошуку, пошук маршрутів, планування.

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

0 = порожній список 1 = список з одним вузлом. 2 = список з 2 вузлами. ...

то визначте суму двох чисел у такій структурі даних, як це: Порожній + N = N Вузол (X) + N = Вузол (X + N)


5

Вежі Ханої - це гарна допомога для вивчення рекурсії.

В Інтернеті існує багато рішень для цього на багатьох різних мовах.


3
Це, на мій погляд, ще один поганий приклад. По-перше, це нереально; це насправді не проблема людей. По-друге, є легкі нерекурсивні рішення. (Одне: числення дисків. Ніколи не переміщуйте диск на диск того ж паритету і ніколи не скасовуйте останній крок, який ви зробили. Якщо ви будете дотримуватися цих двох правил, ви вирішите головоломку з оптимальним рішенням. Рекурсії не потрібно. )
Ерік Ліпперт

5

Детектор паліндром:

Почніть із рядка: "ABCDEEDCBA" Якщо початкові та кінцеві символи рівні, то повторіть повтор і поставте прапорець "BCDEEDCB" тощо.


6
Це також банально вирішити без рекурсії і, ІМХО, краще вирішити без нього.
Blrfl

3
Домовились, але ОП спеціально попросили приклади викладання з мінімальним використанням структур даних.
NWS

5
Це не гарний приклад навчання, якщо ваші студенти можуть відразу ж придумати нерекурсивне рішення. Чому б хтось звернув увагу, коли ваш приклад: "Ось щось тривіальне робити з петлею. Тепер я збираюся показати вам більш важкий шлях без видимих ​​причин".
Відновіть Моніку


4

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

F # - це нечиста функціональна мова, яка дозволяє обидва стилі, тому я порівняю обидва. Наступна сума всіх чисел у списку.

Імперативна петля зі змінною змінною

let xlist = [1;2;3;4;5;6;7;8;9;10]
let mutable sum = 0
for x in xlist do
    sum <- sum + x

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

let xlist = [1;2;3;4;5;6;7;8;9;10]
let rec loop sum xlist = 
    match xlist with
    | [] -> sum
    | x::xlist -> loop (sum + x) xlist
let sum = loop 0 xlist

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


ти заслужив би більше балів, ніж просто +1 від мене. F # без рекурсії було б дуже нудно, це ще більше стосується Haskell, який не має циклічних конструкцій ТОЛЬКО рекурсії!
Шетбі

3

Я активно використовував рекурсії в іграх AI. Пишучи на C ++, я використав серію з близько 7 функцій, які дзвонять одна одній по порядку (при цьому перша функція має можливість обійти всі ці функції і замість цього викличе ланцюжок з 2-х функцій). Кінцева функція в будь-якому ланцюжку знову викликала першу функцію до тих пір, поки залишилася глибина, яку я хотів шукати, не перейшла до 0, і тоді остаточна функція викликає мою оціночну функцію та повертає бал позиції.

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

Ви можете побачити дуже основні контури подібної програми, дивіться https://secure.wikimedia.org/wikipedia/en/wiki/Minimax#Pseudocode


3

По-справжньому хороший приклад реального життя в бізнесі - це щось, що називається "Білль про матеріали". Це дані, які представляють усі компоненти, що складають готовий продукт. Наприклад, давайте використовувати велосипед. У велосипеда є руль, колеса, рама тощо. І кожен з цих компонентів може мати підкомпоненти. наприклад, колесо може мати спиці, шток клапана тощо. Отже, вони зазвичай представлені у структурі дерева.

Тепер запитуйте будь-яку сукупну інформацію про BOM або змінюйте елементи в BOM часто, коли ви вдаєтесь до рекурсії.

    class BomPart
    {
        public string PartNumber { get; set; }
        public string Desription { get; set; }
        public int Quantity { get; set; }
        public bool Plastic { get; set; }
        public List<BomPart> Components = new List<BomPart>();
    }

І зразок рекурсивного дзвінка ...

    static int ComponentCount(BomPart part)
    {
        int subCount = 0;
        foreach(BomPart p in part.Components)
            subCount += ComponentCount(p);
        return part.Quantity * Math.Max(1,subCount);

    }

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


Білл Матеріали може бути спрямований грат, а не дерево, наприклад, однакові характеристики гвинта можуть використовуватися більш ніж одним компонентом.
Ян

-1

Сімейні стосунки служать хорошими прикладами, тому що всі вони інтуїтивно розуміють:

ancestor(joe, me) = (joe == me) 
                    OR ancestor(joe, me.father) 
                    OR ancestor(joe, me.mother);

на якій мові написаний цей код?
törzsmókus

@ törzsmókus Немає конкретної мови. Синтаксис досить близький до C, Obj-C, C ++, Java та багатьох інших мов, але якщо ви хочете реального коду, вам може знадобитися замінити відповідного оператора, наприклад ||для OR.
Калеб

-1

Рекурсивний досить марний, але показовий внутрішній робочий колодязь strlen():

size_t strlen( const char* str )
{
    if( *str == 0 ) {
       return 0;
    }
    return 1 + strlen( str + 1 );
}

Без математики - дуже проста функція. Звичайно, ви не реалізуєте це рекурсивно в реальному житті, але це хороша демонстрація рекурсії.


-2

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


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

-2

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

Ви просто:

mark your "Current" square
gather a list of free squares that are valid moves
are there no valid moves?
    are all squares marked?
        you win!
for each free square
    recurse!
clear the mark on your current square.
return.    

Багато видів сценаріїв "наперед" можна обробити, перевіривши майбутні можливості у такому дереві.

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