Jakie są rozważania, aby ustalić, czy można użyć rekurencji do rozwiązania problemu?


10

Czasami w wywiadach mogę użyć rekurencji, aby rozwiązać problem (na przykład dodanie 1do nieskończonej liczby całkowitej precyzji) lub gdy problem wydaje się odpowiedni do użycia rekurencji. Czasami może to wynikać z częstego używania rekurencji do rozwiązywania problemów, więc bez większego zastanowienia rekursja służy do rozwiązania problemu.

Jakie są jednak uwagi, zanim zdecydujesz, że do rozwiązania problemu należy użyć rekurencji?


Kilka myśli, które miałem:

Jeśli użyjemy rekurencji na danych, które są zmniejszane o połowę za każdym razem, wydaje się, że nie ma problemu z użyciem rekurencji, ponieważ wszystkie dane, które mogą zmieścić się w 16 GB pamięci RAM, a nawet na dysku twardym o pojemności 8 TB, mogą być obsługiwane przez rekurencję na głębokości zaledwie 42 poziomów. (więc nie ma przepełnienia stosu (myślę, że w niektórych środowiskach stos może mieć głębokość 4000 poziomów, znacznie więcej niż 42, ale jednocześnie zależy to również od tego, ile zmiennych lokalnych, jak każdy stos wywołań, zajmuje więcej pamięci jeśli istnieje wiele zmiennych lokalnych i to rozmiar pamięci, a nie poziom, determinuje przepełnienie stosu)).

Jeśli obliczasz liczby Fibonacciego za pomocą czystej rekurencji, naprawdę musisz martwić się złożonością czasu, chyba że buforujesz wyniki pośrednie.

A co powiesz na dodanie 1liczby całkowitej nieskończonej precyzji? Może jest to dyskusyjne, ponieważ czy będziesz pracować z liczbami o długości 3000 cyfr lub długości 4000 cyfr, tak dużej, że może to spowodować przepełnienie stosu? Nie myślałem o tym, ale może odpowiedź brzmi nie, nie powinniśmy używać rekurencji, ale po prostu używać zwykłej pętli, ponieważ co, jeśli w niektórych aplikacjach liczba naprawdę musi mieć długość 4000 cyfr, aby sprawdzić niektóre właściwości liczby, takie jak to, czy liczba jest liczbą pierwszą, czy nie.

Ostateczne pytanie brzmi: jakie są względy, zanim zdecydujesz się użyć rekurencji do rozwiązania problemu?


7
To właściwie dość proste: „Czy rozwiązanie jest trywialne, jeśli mogę założyć, że znane jest rozwiązanie nieco mniejszego problemu?”
Kilian Foth

ale co z liczbą Fibonacciego lub dodaniem 1do nieskończonej liczby całkowitej precyzji? Można powiedzieć, że tak, redukują się one do mniejszych problemów, ale czysta rekurencja nie jest do tego odpowiednia
nonpolarity

Może ci się to
przydać

Odpowiedzi:


15

Jednym z rozważań jest to, czy algorytm ma być rozwiązaniem abstrakcyjnym, czy praktycznym rozwiązaniem wykonywalnym. W pierwszym przypadku poszukiwanymi atrybutami są poprawność i łatwość zrozumienia dla grupy docelowej 1 . W tym drugim przypadku problemem jest także wydajność. Te uwagi mogą wpłynąć na twój wybór.

Drugim zagadnieniem (dla praktycznego rozwiązania) jest to, czy język programowania (a ściślej jego implementacja), którego używasz, eliminuje ogon? Bez eliminacji wywołania ogonowego rekurencja jest wolniejsza niż iteracja, a głęboka rekurencja może prowadzić do problemów z przepełnieniem stosu.

Zauważ, że (poprawne) rozwiązanie rekurencyjne można przekształcić w równoważne rozwiązanie nierekurencyjne, więc niekoniecznie musisz dokonać trudnego wyboru między tymi dwoma podejściami.

Wreszcie, czasami wybór między formami rekurencyjnymi i nierekurencyjnymi jest motywowany potrzebą udowodnienia (w sensie formalnym) właściwości algorytmu. Formuły rekurencyjne bardziej bezpośrednio pozwalają na korektę przez indukcję.


1 - Obejmuje to takie kwestie, jak to, czy docelowi odbiorcy ... a może to obejmować programistów czytających praktyczny kod ... postrzegaliby jeden styl rozwiązania jako „bardziej naturalny” niż drugi. Pojęcie „naturalny” będzie się różnić w zależności od osoby, w zależności od tego, jak nauczyli się programowania lub algorytmiki. (Rzucam wyzwanie każdemu, kto zaproponuje „naturalność” jako podstawowe kryterium przy podejmowaniu decyzji o użyciu rekurencji (lub jej braku) do zdefiniowania „naturalności” w kategoriach obiektywnych, tj. Jak byś ją zmierzył.)


2
Niektóre problemy są po prostu bardziej naturalnie wyrażane za pomocą rekurencji. Na przykład przechodzenie przez drzewo.
Frank Hileman

Zaktualizowałem moją odpowiedź, aby odnieść się do tego punktu,
Stephen C

1
Jeśli chodzi o „naturalność”: na przykład przechodzenie przez drzewo bez rekurencji powoduje na ogół większy, mniej ogólny kod. Rozważ na przykład użycie wywołań polimorficznych do przechodzenia przez drzewo, z różnymi zachowaniami dla węzłów liścia i węzłów złożonych. Nie jest to możliwe bez rekurencji.
Frank Hileman,

1) Czy podjąłeś już wyzwanie zdefiniowania „naturalnego”? 2) Ponieważ możliwe jest symulowanie rekurencji przy użyciu struktury danych stosu, możliwe jest również zaimplementowanie przejścia drzewa. To może nie być najskuteczniejszy sposób ... i nie da ci najbardziej czytelnego kodu ... ale jest to zdecydowanie możliwe i praktyczne.
Stephen C,

Nawiasem mówiąc, pierwszy język programowania, którego się nauczyłem (FORTRAN 4), w ogóle nie obsługiwał rekurencji.
Stephen C

1

Jako programista C / C ++, uważam przede wszystkim za wydajność. Mój proces decyzyjny przypomina:

  1. Jaka jest maksymalna głębokość stosu wywołań? Jeśli jest zbyt głęboko, pozbądź się rekurencji. Jeśli jest płytki, przejdź do 2.

  2. Czy ta funkcja może stanowić wąskie gardło mojego programu? Jeśli tak, przejdź do 3. Jeśli nie, zachowaj rekurencję. W razie wątpliwości uruchom profiler.

  3. Jaki ułamek czasu procesora spędzonego na wywołaniach funkcji rekurencyjnych? Jeśli wywołania funkcji zajmują znacznie mniej czasu niż reszta treści funkcji, można użyć rekurencji.


0

Jakie są jednak uwagi, zanim zdecydujesz, że do rozwiązania problemu należy użyć rekurencji?

Pisząc funkcje w schemacie, uważam za naturalne pisanie funkcji rekurencyjnych bez myślenia za dużo.

Pisząc funkcje w C ++, zastanawiam się, zanim użyję funkcji rekurencyjnej. Pytania, które sobie zadaję to:

  • Czy obliczenia można wykonać przy użyciu algorytmu iteracyjnego? Jeśli tak, zastosuj podejście iteracyjne.

  • Czy głębokość rekurencji może wzrosnąć o rozmiar modelu? Niedawno wpadłem na przypadek, w którym głębokość rekurencji wzrosła do prawie 13000 z powodu wielkości modelu. Musiałem przekonwertować funkcję, aby użyć algorytmu iteracyjnego po pośpiechu.

    Z tego powodu nie polecałbym pisania algorytmu przechodzenia przez drzewo przy użyciu funkcji rekurencyjnych. Nigdy nie wiadomo, kiedy drzewo stanie się zbyt głębokie dla środowiska wykonawczego.

  • Czy funkcja może stać się zbyt skomplikowana przy użyciu algorytmu iteracyjnego? Jeśli tak, użyj funkcji rekurencyjnej. Nie próbowałem pisać qsortmetodą iteracyjną, ale mam wrażenie, że użycie funkcji rekurencyjnej jest dla niej bardziej naturalne.


0

W przypadku liczb Fibonacciego naiwna „rekurencja” jest po prostu całkowicie głupia. To dlatego, że prowadzi to do tego samego podproblemu, który jest rozwiązywany w kółko.

W rzeczywistości istnieje trywialna zmienność liczb Fibonacciego, w których rekurencja jest bardzo skuteczna: Biorąc pod uwagę liczbę n ≥ 1, oblicz zarówno fib (n), jak i fib (n-1). Potrzebujesz więc funkcji, która zwraca dwa wyniki, nazwijmy tę funkcję fib2.

Implementacja jest dość prosta:

function fib2 (n) -> (fibn, fibnm1) {
    if n ≤ 1 { return (1, 1) }
    let (fibn, fibnm1) = fib2 (n-1)
    return (fibn + fibnm1, fibn)
}

myślisz, że możesz napisać program we wspólnym języku? a twoje fib2zwraca parę liczb, a twój fib2()nie pasuje do interfejsu fib(), który, biorąc pod uwagę liczbę, zwraca liczbę. Wygląda na fib(n)to, że powrócisz, fib2(n)[0]ale bądź konkretny
nonpolarity
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.