У чому полягають реальні проблеми, коли рекурсивний підхід є природним рішенням, крім глибокого пошуку (DFS)?
(Я не розглядаю Ханойську вежу , число Фібоначчі чи факторіальні реальні проблеми. Вони трохи надумані в моїй свідомості.)
У чому полягають реальні проблеми, коли рекурсивний підхід є природним рішенням, крім глибокого пошуку (DFS)?
(Я не розглядаю Ханойську вежу , число Фібоначчі чи факторіальні реальні проблеми. Вони трохи надумані в моїй свідомості.)
Відповіді:
Тут є безліч прикладів математики, але ви хотіли приклад із реального світу , тому, трохи подумавши, це, мабуть, найкраще, що я можу запропонувати:
Ви знайдете людину, яка перехворіла даною заразною інфекцією, яка не є смертельною, і швидко виправляється (Тип А), за винятком кожного п’ятого (ми будемо називати це типу В), який назавжди інфікується нею і не показує симптоми і просто діє як розповсюджувач.
Це створює досить надокучливі хвилі хаосу, коли тип В завжди заражає безліч типів А.
Ваше завдання - відстежити всі типи В і імунізувати їх, щоб зупинити хребет хвороби. На жаль, ти не можеш ввести загальнонаціональне ліки для всіх, тому що люди типу А також мають смертельну алергію на ліки, які діють на тип В.
Як би ви це зробили, це було б соціальним відкриттям, якщо зараженому (тип А) вибрати останні контакти за останній тиждень, позначивши кожен контакт у купі. Коли ви перевіряєте, чи заражена людина, додайте її до черги "подальшого спостереження". Коли людина є типом В, додайте її до "подальших дій" на чолі (бо ви хочете зупинити це швидко).
Після обробки даної особи виберіть людину з черги та застосуйте імунізацію, якщо це необхідно. Отримайте всі їхні контакти, які раніше не відвідували, а потім протестуйте, чи не заражені вони.
Повторюйте, поки черга заражених людей не стане 0, а потім почекайте чергового спалаху.
(Гаразд, це трохи ітеративно, але це ітераційний спосіб вирішення рекурсивної проблеми, в даному випадку - це перше обхід ширини бази населення, що намагається виявити ймовірні шляхи до проблем, і крім того, ітеративні рішення часто швидші та ефективніші , і я компульсивно усуваю рекурсію скрізь, настільки, що це стає інстинктивним ... блін!)
Як щодо будь-чого, що стосується структури каталогів у файловій системі. Рекурсивний пошук файлів, видалення файлів, створення каталогів тощо.
Ось реалізація Java, яка рекурсивно друкує вміст каталогу та його підкаталогів.
import java.io.File;
public class DirectoryContentAnalyserOne implements DirectoryContentAnalyser {
private static StringBuilder indentation = new StringBuilder();
public static void main (String args [] ){
// Here you pass the path to the directory to be scanned
getDirectoryContent("C:\\DirOne\\DirTwo\\AndSoOn");
}
private static void getDirectoryContent(String filePath) {
File currentDirOrFile = new File(filePath);
if ( !currentDirOrFile.exists() ){
return;
}
else if ( currentDirOrFile.isFile() ){
System.out.println(indentation + currentDirOrFile.getName());
return;
}
else{
System.out.println("\n" + indentation + "|_" +currentDirOrFile.getName());
indentation.append(" ");
for ( String currentFileOrDirName : currentDirOrFile.list()){
getPrivateDirectoryContent(currentDirOrFile + "\\" + currentFileOrDirName);
}
if (indentation.length() - 3 > 3 ){
indentation.delete(indentation.length() - 3, indentation.length());
}
}
}
}
Quicksort , сортування злиттям , і більшість інших N сортів N-лог.
Приклад Метта Ділларда хороший. Взагалі кажучи, будь-яка ходьба по дереву, як правило, може бути дуже легко рекурсивна. Наприклад, складання дерев синтаксичного аналізу, перегляд XML чи HTML тощо.
Рекурсія часто використовується в реалізаціях алгоритму зворотного відстеження . Для "реального" застосування цього, як щодо розв'язувача судоку ?
Рекурсія доцільна, коли проблему можна вирішити, розділивши її на підзадачі, які можуть використовувати той самий алгоритм їх вирішення. Алгоритми на деревах та відсортовані списки - це природна відповідність. Багато проблем в обчислювальній геометрії (та 3D-іграх) можна вирішити рекурсивно, використовуючи дерева двійкового простору (BSP), жирові підрозділи або інші способи поділу світу на підчастини.
Рекурсія також доречна, коли ви намагаєтесь гарантувати правильність алгоритму. Враховуючи функцію, яка приймає незмінні входи та повертає результат, який є комбінацією рекурсивних та нерекурсивних викликів на входах, як правило, легко довести, що функція правильна (чи ні), використовуючи математичну індукцію. Це часто важко зробити за допомогою ітеративної функції або за допомогою входів, які можуть мутувати. Це може бути корисно при роботі з фінансовими розрахунками та іншими програмами, де коректність дуже важлива.
Безумовно, що багато компіляторів активно використовують рекурсію. Комп’ютерні мови самі по собі є рекурсивними (тобто ви можете вбудовувати оператори if якщо в інші оператори if) тощо).
Вимкнення / налаштування лише для читання для всіх дочірніх елементів керування в елементі керування контейнером. Мені потрібно було це зробити, оскільки деякі елементи контролю дітей були самими контейнерами.
public static void SetReadOnly(Control ctrl, bool readOnly)
{
//set the control read only
SetControlReadOnly(ctrl, readOnly);
if (ctrl.Controls != null && ctrl.Controls.Count > 0)
{
//recursively loop through all child controls
foreach (Control c in ctrl.Controls)
SetReadOnly(c, readOnly);
}
}
(джерело: mit.edu )
Ось визначення eval:
(define (eval exp env)
(cond ((self-evaluating? exp) exp)
((variable? exp) (lookup-variable-value exp env))
((quoted? exp) (text-of-quotation exp))
((assignment? exp) (eval-assignment exp env))
((definition? exp) (eval-definition exp env))
((if? exp) (eval-if exp env))
((lambda? exp)
(make-procedure (lambda-parameters exp)
(lambda-body exp)
env))
((begin? exp)
(eval-sequence (begin-actions exp) env))
((cond? exp) (eval (cond->if exp) env))
((application? exp)
(apply (eval (operator exp) env)
(list-of-values (operands exp) env)))
(else
(error "Unknown expression type - EVAL" exp))))
Ось визначення застосувати:
(define (apply procedure arguments)
(cond ((primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments))
((compound-procedure? procedure)
(eval-sequence
(procedure-body procedure)
(extend-environment
(procedure-parameters procedure)
arguments
(procedure-environment procedure))))
(else
(error
"Unknown procedure type - APPLY" procedure))))
Ось визначення послідовності eval:
(define (eval-sequence exps env)
(cond ((last-exp? exps) (eval (first-exp exps) env))
(else (eval (first-exp exps) env)
(eval-sequence (rest-exps exps) env))))
eval
-> apply
-> eval-sequence
->eval
Люди часто сортують стоси документів за допомогою рекурсивного методу. Наприклад, уявіть, що ви сортуєте 100 документів, на яких є імена. Спочатку покладіть документи в купи за першою літерою, потім сортуйте кожну купу.
Пошук слів у словнику часто виконується методом бінарного пошуку, який є рекурсивним.
В організаціях начальники часто віддають команди керівникам підрозділів, а ті, в свою чергу, командам керівників тощо.
Синтаксичні аналізатори та компілятори можуть бути записані методом рекурсивного спуску. Не найкращий спосіб це зробити, оскільки такі інструменти, як lex / yacc, генерують швидші та ефективніші парсери, але концептуально прості та легкі у реалізації, тому вони залишаються поширеними.
Рекурсія застосовується до проблем (ситуацій), коли ви можете розбити її (зменшити) на менші частини, і кожна частина (частини) виглядає схожою на вихідну проблему.
Хороші приклади того, де речі, які містять менші частини, схожі на нього самі:
Рекурсія - це техніка, яка дозволяє продовжувати розбивати проблему на дедалі менші шматки, поки один із цих шматочків не стане достатньо дрібним, щоб стати шматочком пирога. Звичайно, після того, як ви їх розіб’єте, вам доведеться «зшити» результати разом у правильному порядку, щоб сформувати загальне рішення вашої початкової проблеми.
Деякі рекурсивні алгоритми сортування, алгоритми ходьби по дереву, алгоритми картографування / зменшення, поділ і володар - все це приклади цієї техніки.
У комп'ютерному програмуванні більшість мов типу зворотного виклику на основі стеку вже мають вбудовані можливості для рекурсії: тобто
У мене є система, яка використовує чисту рекурсію хвоста в декількох місцях для імітації автомата.
Деякі чудові приклади рекурсії можна знайти у функціональних мовах програмування . У функціональних мовах програмування ( Erlang , Haskell , ML / OCaml / F # та ін.) Дуже часто, коли будь-яка обробка списку використовує рекурсію.
Маючи справу зі списками типовими імперативними мовами стилю ООП, дуже часто можна побачити списки, реалізовані як пов'язані списки ([item1 -> item2 -> item3 -> item4]). Однак у деяких функціональних мовах програмування ви виявляєте, що самі списки реалізуються рекурсивно, де "заголовок" списку вказує на перший елемент у списку, а "хвіст" вказує на список, що містить решту елементів ( [item1 -> [item2 -> [item3 -> [item4 -> []]]]]). На мій погляд, це досить креативно.
Ця обробка списків у поєднанні із зіставленням шаблонів ДУЖЕ потужна. Скажімо, я хочу підсумувати список чисел:
let rec Sum numbers =
match numbers with
| [] -> 0
| head::tail -> head + Sum tail
По суті, це говорить "якщо нас викликали з порожнім списком, поверніть 0" (що дозволяє нам розбити рекурсію), інакше повертає значення head + значення Sum, що викликається з рештою елементів (отже, наша рекурсія).
Наприклад, у мене може бути список URL-адрес , я думаю розбити всі URL-адреси, на які посилається кожна URL-адреса, а потім зменшую загальну кількість посилань на / з усіх URL-адрес, щоб генерувати "значення" для сторінки (підхід, який Google бере разом із PageRank і що ви можете знайти, визначені в оригінальній статті MapReduce ). Ви можете зробити це, щоб також генерувати кількість слів у документі. І багато, багато, багато іншого.
Ви можете поширити цей функціональний шаблон на будь-який тип коду MapReduce, де ви можете взяти список чогось, перетворити його і повернути щось інше (будь-який інший список або якусь zip-команду зі списку).
XML, або обхід будь-чого, що є деревом. Хоча, чесно кажучи, я майже ніколи не використовую рекурсію у своїй роботі.
Цикли зворотного зв'язку в ієрархічній організації.
Найкращий начальник каже керівникам керівництва збирати відгуки від усіх у компанії.
Кожен керівник збирає свої / її прямі звіти та каже їм збирати відгуки від своїх прямих звітів.
І далі по лінії.
Люди, які не мають прямих звітів - листкові вузли на дереві - дають свої відгуки.
Зворотній зв'язок рухається назад по дереву, кожен менеджер додає свій власний відгук.
Зрештою всі відгуки повертаються до головного начальника.
Це природне рішення, оскільки рекурсивний метод дозволяє фільтрувати на кожному рівні - зіставлення дублікатів та видалення образливих відгуків. Найвищий начальник міг би надіслати глобальний електронний лист і попросити кожного співробітника повідомити зворотний зв'язок безпосередньо з ним, але є проблеми "ти не можеш впоратися з правдою" і "ти звільнений", тому рекурсія тут працює найкраще.
Припустимо, ви створюєте систему управління вмістом для веб-сайту, де ваші сторінки мають деревну структуру, припустимо, що корінь - це домашня сторінка.
Припустимо, також ваш {користувач | клієнт | клієнт | бос} просить вас розмістити на всіх сторінках стежку, щоб показати, де ви знаходитесь у дереві.
Для будь-якої даної сторінки n ви, можливо, захочете підійти до батьківського елемента n та його батьківського об'єкта тощо, щоб рекурсивно створити список вузлів, що створюються до кореня дерева сторінок.
Звичайно, ви натискаєте db кілька разів на сторінку в цьому прикладі, тому, можливо, ви захочете використати якийсь псевдонім SQL, де ви шукаєте таблицю сторінок як a, а таблицю сторінок знову як b, і приєднаєте a.id за допомогою b.parent, тому ви змушуєте базу даних виконувати рекурсивні об'єднання. Минув якийсь час, тому мій синтаксис, мабуть, не корисний.
Знову ж таки, ви можете просто захотіти обчислити це лише один раз і зберегти його разом із записом сторінки, лише оновивши, якщо перемістите сторінку. Це, мабуть, було б ефективніше.
У всякому разі, це мої $ .02
На моїй роботі у нас є система із загальною структурою даних, яку можна описати як дерево. Це означає, що рекурсія є дуже ефективною технікою роботи з даними.
Її вирішення без рекурсії потребує багато непотрібного коду. Проблема рекурсії полягає в тому, що непросто простежити, що відбувається. Ви дійсно повинні зосередитися, дотримуючись потоку виконання. Але коли він працює, код елегантний та ефективний.
Розбір дерева елементів керування у Windows Forms або WebForms (.NET Windows Forms / ASP.NET ).
Найкращий приклад, який я знаю, - це швидке сортування , воно набагато простіше з рекурсією. Подивись на:
shop.oreilly.com/product/9780596510046.do
www.amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/0596510047
(Клацніть на перший підзаголовок під главою 3: "Найкрасивіший код, який я коли-небудь писав").
Телефонні та кабельні компанії підтримують модель своєї топології проводки, яка насправді є великою мережею або графіком. Рекурсія - це один із способів обходу цієї моделі, коли ви хочете знайти всі батьківські або всі дочірні елементи.
Оскільки рекурсія є дорогою з точки зору обробки та пам'яті, цей крок зазвичай виконується лише тоді, коли топологія змінена, а результат зберігається у зміненому попередньо замовленому форматі списку.
Так само, як і коментар про компілятори. Вузли абстрактного дерева синтаксису, природно, піддаються рекурсії. Усі рекурсивні структури даних (зв’язані списки, дерева, графіки тощо) також легше обробляти за допомогою рекурсії. Я вважаю, що більшість з нас не часто користуються рекурсією, коли ми не навчаємось, через типи реальних проблем, але добре усвідомлювати це як варіант.