Рекурсия vs итерация в отношении использования памяти

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

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

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

vs

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);
    }
}

1 ответов


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

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

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

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

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

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