Исключение времени выполнения, слишком глубокая рекурсия
я преобразовал псевдо-код здесь в C#, и он рекурсивно повторяется 10,000 раз. Но я получаю ошибку времени выполнения C#,StackOverflow Exception
после 9217
раза. Как я могу предотвратить это?
редактировать если это кому-то поможет, вот код:
private double CalculatePi(int maxRecursion)
{
return 2 * CalculatePi(maxRecursion, 1);
}
private double CalculatePi(int maxRecursion, int i)
{
if (i >= maxRecursion)
return 1;
return 1 + i / (2.0 * i + 1) * CalculatePi(maxRecursion, i + 1);
}
double pi = CalculatePi(10000); // 10,000 recursions
EDIT2 поэтому все, похоже, согласны с тем, что мне нужно преобразовать это в итеративное... кто-нибудь может дать код? Кажется, я не могу написать итеративный код, который завод...
редактировать спасибо полу Рику за этот ответ, который я тестировал, и он работает:
private static double CalculatePi(int maxRecursion)
{
double result = 1;
for (int i = maxRecursion; i >= 1; i-- )
{
result = 1 + i / (2.0 * i + 1) * result;
}
return result * 2;
}
12 ответов
"компилятор c#" скомпилирует этот штраф. Однако во время выполнения вы можете получить исключение StackOverflow (на моей машине 10,000 работает нормально, но умирает позже).
Это не "бесконечное исключение рекурсии" - переполнение стека-это именно то, что он говорит; вызов метода означает размещение информации в стеке и удаление ее при возврате вызова. У вас есть 10 000 вызовов, и ни один не вернулся, поэтому стек заполнен, и возникает исключение.
вы можете исправить это довольно легко в данном конкретном случае, удалив рекурсию и запустив итеративно. В общем случае существуют способы удаления рекурсии через итерацию, оптимизацию хвостовой рекурсии и прохождение продолжения.
вот быстрый прямой перевод в итеративный стиль:
private static double CalculatePi(int maxRecursion)
{
double result = 1;
for (int i = maxRecursion -1; i >= 1; i-- )
{
result = 1 + i / (2.0 * i + 1) * result;
}
return result * 2;
}
поэтому все, похоже, согласны с тем, что мне нужно преобразовать это в итеративное... кто-нибудь может дать код? Кажется, я не могу написать итеративный код, который работает...
вы просите рыбу или просите научить вас ловить рыбу? Если цель этого упражнения состоит в том, чтобы научиться преобразовывать рекурсивный код в итеративный, то вы не очень хорошо обслуживаетесь, просто получая ответ.
для преобразования рекурсивного кода в итеративный код существует множество возможные способы сделать это. Самый простой способ в этом случае-просто разработать шаблон. Что делает код? Он вычисляет:
(1 + 1 / (2.0 * 1 + 1)) *
(1 + 2 / (2.0 * 2 + 1)) *
(1 + 3 / (2.0 * 3 + 1)) *
(1 + 4 / (2.0 * 4 + 1)) *
(1 + 5 / (2.0 * 5 + 1)) *
(1 + 6 / (2.0 * 6 + 1)) *
(1 + 7 / (2.0 * 7 + 1)) *
...
(1 + 9999/ (2.0 * 9999+ 1)) *
1
теперь вы можете написать цикл, который вычисляет это? Конечно.
double product = 1.0;
for (int i = 9999; i >= 0; --i)
product *= (1 + i / (2.0 * i + 1));
Это самый простой способ сделать это. Но есть много способов решить эту проблему.
вы могли бы использовать агрегатор. Рассмотрим операцию "total"; это пример агрегатора. У вас есть последовательность вещей, и вы продолжаете работает всего их, накапливая результат в аккумуляторе. Стандартные операторы запросов имеют операцию агрегирования, поставляемую для вас:
double seed = 1.0;
Enumerable.Range(0, 10000).Aggregate(seed,
(product, i) => product * (1 + i / (2.0 * i + 1))
или вы можете сохранить свой алгоритм рекурсивным, но устранить переполнение стека:
- создание собственной структуры стека на куче
- определение виртуальной машины, написание программы на языке виртуальной машины, а затем реализация виртуальной машины для сохранения ее стека на куча.
- переписывание программы в стиле продолжения передачи; каждый шаг по пути затем возвращает продолжение вызывающему объекту, который вызывает следующее продолжение; стек никогда не становится глубоким
объяснение этих методов занимает слишком много времени для одного ответа. У меня есть серия из шести частей блога о том, как использовать эти методы для превращения рекурсивных программ в программы, которые не потребляют много стека; начните читать здесь:
http://blogs.msdn.com/b/ericlippert/archive/2005/07/25/recursion-part-zero.aspx
Не используйте рекурсию. Это хороший пример того, когда вы не должны рекурсировать, так как вы вызовете переполнение стека вызовов.
вы, вероятно, можете легко преобразовать это в нерекурсивное.
вы не можете предотвратить это, функция вызывает себя слишком много раз, есть ограничение глубины стека вызова могут сделать, и ваша функция достигает его.
чтобы glom на то, что все остальные говорят здесь - это переполнение стека (woot! вопрос переполнения стека вопрос о StackOverflow. Это может привести к стеку . . .)
в любом случае, каждый раз, когда вызывается ваш рекурсивный метод, он толкает стек, то есть он помещает ссылку на себя в стек, так что, когда вызывается последний вызов CalculatePi, он может размотать все остальные вызовы.
стек не является бесконечным ресурсом. Каждый толчок чтобы стек съедал немного памяти, и когда вы используете все это, вы получаете переполнение стека.
рекурсивный вызов не является хвостовым рекурсивным, и даже если бы это было так, это не помогло бы, так как компилятор C# в настоящее время не оптимизирует хвостовые рекурсивные вызовы. EDIT: как указали Эрик Липперт и Гейб, среда CLR может генерировать хвостовые вызовы, даже если явные инструкции хвостового вызова не находятся в излучаемом IL.
- на лучшие как бы превратить этот рекурсивного решения в итеративном один.
- так как это почти завершает, быстрый взлом может заключаться в увеличении размера стека потока, на котором выполняется этот метод.
пожалуйста, не делай этого. только для удовольствия:
static void Main()
{
Console.WriteLine(SafeCalculatePi(10000));
}
// Calculates PI on a separate thread with enough stack-space
// to complete the computation
public static double SafeCalculatePi(int maxRecursion)
{
// We 'know' that you can reach a depth of 9217 for a 1MB stack.
// This lets us calculate the required stack-size, with a safety factor
double requiredStackSize = (maxRecursion / 9217D) * 1.5 * 1024 * 1024 ;
double pi = 0;
ThreadStart ts = delegate { pi = CalculatePi(maxRecursion); };
Thread t = new Thread(ts, (int)requiredStackSize);
t.Start();
t.Join();
return pi;
}
Это не бесконечный рекурсия, очевидно, так как она останавливается после 10 000 уровней, но это все еще не лучшая идея.
десять тысяч уровней стека-это ужасно много-стек не является массивным ресурсом. Вы должны преобразовать его в итеративное решение.
поскольку вы почти можете запустить программу, вы можете просто заставить ее использовать меньше пространства стека. Просто запустите в режиме выпуска вместо отладки, и он должен работать.
итеративный код для этого просто:
private double CalculatePi(int iterations) {
double pi = 1.0;
for (int i = iterations - 1; i >= 1; i--) {
pi = 1 + i / (2.0 * i + 1) * pi;
}
return pi * 2.0;
}
использование:
double pi = CalculatePi(51);
Это будет не рекурсивный вариант:
private double CalculatePi(int maxRecursion)
{
return 2 * CalculatePi2(maxRecursion, 1);
}
private double CalculatePi2(int maxRecursion, int i)
{
double product = 1;
while (i < maxRecursion)
{
product = product * (1f + i / (2.0f * i + 1f));
i++;
}
return product;
}
итерационный вариант:
public static double CalculatePi(int maxRecursion)
{
double result=1;
for(int i=maxRecursion-1;i>=1;i--)
{
result=1+i/(2.0*i+1)*result;
}
return result*2;
}
рекурсивные вызовы функций почти всегда являются действительно ужасным способом делать вещи.
Я бы просто получил значение pi из файла заголовка / математической библиотеки.