Исключение времени выполнения, слишком глубокая рекурсия

я преобразовал псевдо-код здесь в 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.

  1. на лучшие как бы превратить этот рекурсивного решения в итеративном один.
  2. так как это почти завершает, быстрый взлом может заключаться в увеличении размера стека потока, на котором выполняется этот метод.

пожалуйста, не делай этого. только для удовольствия:

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 из файла заголовка / математической библиотеки.