Общие вопросы по рекурсии - PullRequest
5 голосов
/ 02 августа 2011

Как я понимаю, хорошие рекурсивные решения могут облегчить сложные проблемы.Они могут быть более эффективными с точки зрения времени или пространства.

Мой вопрос: это не бесплатно, и стек вызовов будет очень глубоким.Это будет занимать много памяти.Я прав?

Ответы [ 6 ]

12 голосов
/ 02 августа 2011

Трудно точно определить компромиссы, связанные с рекурсией.

На математически абстрактном уровне рекурсия дает мощную основу для описания явного поведения функции.Например, мы можем математически определить факториал как

x! = 1             if x == 0
x! = x * (x - 1)!  else

Или мы можем рекурсивно определить более сложную функцию, например, как мы можем вычислить «N выбрать K»:

C(n, k) = 1                             if k == 0
C(n, k) = 0                             if k < 0 or if n > k
C(n, k) = C(n - 1, k) + C(n - 1, k - 1) else

Когдаиспользуя рекурсию в качестве метода реализации, нет никакой гарантии, что вы в конечном итоге будете использовать больше памяти или создавать код, который работает более эффективно.Часто рекурсия использует больше места из-за памяти, необходимой для хранения кадров стека, но в некоторых языках это не проблема, поскольку компилятор может попытаться оптимизировать вызовы функций (см., Например, исключение хвостовых вызовов).В других случаях рекурсия может израсходовать огромные ресурсы до такой степени, что рекурсивный код может не завершиться при простых задачах.

Что касается вопросов эффективности, часто рекурсивный код существенно менее эффективен, чем итеративный код.Вызовы функций дороги, а наивный перевод из рекурсии в код приводит к ненужному дублированию работы.Например, наивная реализация Фибоначчи

int Fibonacci(int n) {
    if (n <= 1) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

ужасно неэффективна и настолько медленна, что никогда не использовалась на практике.Хотя код чище, неэффективность поглощает любые потенциальные преимущества рекурсии.

В других случаях, однако, рекурсия может удивительно сэкономить время.Например, mergesort - это очень быстрый алгоритм сортировки, определяемый красивой рекурсией:

Mergesort(array, low, high) {
    if (low >= high - 1) return;
    Mergesort(array, low, low + (high - low) / 2);
    Mergesort(array, low + (high - low) / 2, high);
    Merge(array, low, low + (high - low) / 2, high);
}

Этот код очень быстрый, и соответствующий итерационный код, вероятно, будет медленнее, труднее для чтения и труднее для понимания.

Короче говоря, рекурсия не является ни магическим лекарством, ни силой, которой следует избегать.Это помогает осветить структуру многих проблем, которые в противном случае могут показаться трудными или почти невозможными.Хотя это часто приводит к более четкому коду, оно часто делает это за счет времени и памяти (хотя это не обязательно автоматически менее эффективно; во многих случаях это может быть более эффективно).Определенно стоит учиться совершенствовать свои навыки алгоритмического мышления и решения проблем, даже если вы никогда не пишете в своей жизни еще одну рекурсивную функцию.

Надеюсь, это поможет!

1 голос
/ 02 августа 2011

Это зависит.Проблемы, для которых рекурсия лучше всего подходит, будут устойчивы к этой проблеме.Типичным примером будет Mergesort, в котором для сортировки списка из N элементов будет около log2 (N) стековых фреймов.Таким образом, если ваш предел кадров стека равен 200, и к моменту вызова Mergesort вы использовали 50, этого все же достаточно, чтобы отсортировать около 2 ^ 150 элементов без переполнения стека.Кроме того, Mergesort не создает много памяти для каждого кадра стека, поэтому общее использование памяти для Mergesort никогда не должно быть значительно больше, чем удвоение размера исходного списка.

Кроме того, некоторые языки (схема хорошапример) использовать исключение хвостовых вызовов , чтобы код мог быть элегантно написан с использованием рекурсии, но затем оптимизирован или скомпилирован в итерационный цикл.Это один из способов, с помощью которого LISP, будучи функциональным языком, все еще может конкурировать с C и C ++ по скорости исполнения.использоваться для выполнения, казалось бы, рекурсивных операций без создания глубокого стека вызовов.Но если он не встроен в библиотеку или даже в конструкцию уровня языка, этот метод имеет менее очевидное преимущество в производительности (на мой взгляд).

Так что пока трудно спорить против старого доброго for x in xrange(10)во многих ситуациях рекурсия имеет свое место.

1 голос
/ 02 августа 2011

Практически стек вызовов не будет очень глубоким.Возьмем, к примеру, алгоритм «разделяй и властвуй», такой как быстрая сортировка, который делит задачу на две половины.С помощью стека вызовов глубиной 32 вы можете сортировать элементы 4G, которые, вероятно, даже не поместятся в память обычного компьютера.Потребление памяти на самом деле не проблема, это стек, и он бесплатный, если у вас его не заканчивается ... (и с 32 уровнями у вас есть много данных для хранения для каждого уровня).

Вы можете переписать практически все ресурсоемкие процессы в итеративные, если вы поддерживаете состояние кучи 1004 * в структуре стека, но это только усложняет код.Основной сценарий, в котором вы можете получить реальную выгоду от переписывания, это когда у вас есть хвостовой рекурсивный код, которому не нужно поддерживать состояние для каждого рекурсивного вызова.Обратите внимание, что для некоторых языков (большинство функциональных языков программирования и C / C ++, возможно, также и Java) хороший компилятор может сделать это за вас.

0 голосов
/ 03 августа 2011

Зависит от проблемы.

Если проблема требует рекурсии, как обход дерева в глубину, единственный способ избежать рекурсии - это смоделировать ее, написав свой собственный стек. Это ничего не спасет.

Если проблема не требует рекурсии, как обычная факториальная функция или функция Фибоначчи, какой смысл? Вы ничего не получаете, используя это.

Это довольно маленький класс проблем, где у вас может быть даже разумный выбор.

0 голосов
/ 02 августа 2011

Это дорого, только если ваша рекурсия не является хвостовой рекурсией и ваш язык не поддерживает хвостовую рекурсию. См. Следующую статью в Википедии о призывах к хвосту для обсуждения этой темы:

http://en.wikipedia.org/wiki/Tail_call

В противном случае код может быть намного проще для чтения и тестирования.

0 голосов
/ 02 августа 2011
Cons:
 It is hard (especially for inexperienced programmers) to think recursively
 There also exists the problem of stack overflow when using some forms of recursion (head
recursion).
 It is usually less efficient because of having to push and pop recursions on and off the
run-time stack, so it can be slower to run than simple iteration.

Но почему мы пытаемся использовать рекурсии?

Pros:
 It is easier to code a recursive solution once one is able to identify that solution. The
recursive code is usually smaller, more concise, more elegant, and possibly even easier to
understand, though that depends on one’s thinking style☺
 There are some problems that are very difficult to solve without recursion. Those problems
that require backtracking such as searching a maze for a path to an exit or tree based
operations are best solved recursively.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...