2016-10-09 3 views
9

Предположим, что у меня есть рекурсивное, а также итеративное решение (с использованием стека) с некоторой проблемой, например. предварительный обход бинарного дерева. С текущими компьютерами с памятью есть ли преимущество использования рекурсивного решения над итеративной версией или наоборот для очень больших деревьев?Рекурсия против итерации в отношении использования памяти

Я знаю, что для некоторых рекурсивных решений, в которых повторяются подзапросы, при использовании рекурсии есть дополнительные затраты времени и памяти. Предположим, что это не так. Например,

preOrder(Node n){ 
    if (n == null) return; 
    print(n); 
    preOrder(n.left); 
    preOrder(n.right); 
} 

против

preOrder(Node n){ 
    stack s; 
    s.push(n); 
    while(!s.empty()){ 
     Node node = s.pop(); 
     print(node); 
     s.push(node.right); 
     s.push(node.left); 
    } 
} 
+0

Предполагается, что 'посещение' должно быть' preOrder'? –

+1

Спасибо, что заметили это. –

+2

Можно подумать, что при использовании рекурсии память всегда будет в стеке. Хотя с итерацией вы можете решить выделить ее в кучу. –

ответ

8

Если существует опасность переполнения стека (в данном случае, потому что деревья не гарантированно даже полу-сбалансированный), то надежная программа будет избежать рекурсии и использовать явный стек.

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

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

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

void preOrder(Node n) { 
    for (; n; n = n.right) { 
     print(n); 
     preOrder(n.left); 
     n = n.right; 
} 

Подобный метод часто (и всегда должен быть) применяется для сортировки: после разделения, функция рекурсивен на меньшем разделе, а затем петли для обработки большего раздела. Поскольку меньший раздел должен быть меньше половины размера исходного массива, это гарантирует, что глубина рекурсии будет меньше, чем log исходного размера массива, который, конечно, меньше 50 кадров стека и, вероятно, намного меньше ,

+1

Хороший ответ, но вы также можете указать, что в примере OP хороший компилятор оптимизирует второй рекурсивный (хвостовой) вызов в цикле, поэтому только оставшиеся дети нуждаются в кадрах стека. В этом случае правые искаженные деревья будут искать в постоянном пространстве рекурсивный код и линейное пространство с помощью явного кода стека. Мораль состоит в том, что детали реализации и системы компиляции имеют значение. – Gene

+0

@gene: Первоначально у меня была дискуссия о правильном алгоритме быстрой сортировки, оптимизированном с помощью хвоста, но я удалил его, потому что он вышел из темы. ТСО трудно с C++, и, хотя я думаю, что gcc найдет оптимизацию в этом простом случае, никаких гарантий нет. Поскольку вы не можете знать, что компилятор будет TCO заданной функцией, надежный код не должен полагаться на нее. Вы можете написать хвостовой вызов как цикл и оставить рекурсию без TC как есть, что будет хорошо работать для искаженных деревьев или q-s; если я стану амбициозным, я добавлю гибридный вариант. – rici

+0

Не могли бы вы показать мне, как выглядит оптимизированная по хвосту версия? –

Смежные вопросы