Оптимизация рекурсивных вызовов через структуры данных - PullRequest
2 голосов
/ 21 июня 2009

Существует ли алгоритм для оптимизации высокорекурсивной функции в итерацию по структуре данных? Например, учитывая следующую функцию ...

// template <typename T> class BSTNode is defined somewhere else
// It's a generic binary search tree.
template <typename T, typename R>
void in_order(BSTNode<T>* root, R (*routine)(T))
{
    if (root)
    {
        in_order(root->leftChild);
        routine(root->data);
        in_order(root->rightChild);
    }
}

... можно ли оптимизировать его в ...

// template <typename> class Stack is defined somewhere else
// It's a generic LIFO array (aka stack).

template <typename T, typename R>
void in_order(BSTNode<T>* root, R (*routine)(T))
{
    if (!root) return;

    Stack<BSTNode*> stack;
    stack.push(NULL);

    line1:
    while (root->leftChild)
    {
        stack.push(root);
        root = root->leftChild;
    }

    line2:
    routine(root->data);
    if (root->rightChild)
    {
        root = root->rightChild;
        goto line1;
    }

    else if (root = stack.pop())
        goto line2;
}

(Конечно, разница в том, что вместо заполнения стека вызовов последний заполняет другой стек в куче.)


РЕДАКТИРОВАТЬ: Я имел в виду алгоритм, который может выполняться компилятором , поэтому мне не нужно оптимизировать его вручную.

Ответы [ 8 ]

4 голосов
/ 21 июня 2009

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

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

4 голосов
/ 21 июня 2009

Да, вы можете сделать это.

Однако, кроме возможного исчерпания стекового пространства с очень глубокими деревьями, здесь нет "оптимизации". Если вам нужно прибавить в скорости, рассмотрите заправляя свои деревья .

2 голосов
/ 28 октября 2009

Вы запрашиваете преобразование в стиле продолжения с нефункционализированными продолжениями; он описан в главе 6 Основы языков программирования с кодом на схеме. Но это было бы огромной болью для C ++. Возможно, если у вас есть интерфейс компилятора, который преобразует C ++ в достаточно доступные структуры данных, и вам нужно сделать это с lot кода. В книге также объясняется, как систематически выполнять это преобразование вручную, что более вероятно в вашей ситуации.

1 голос
/ 22 июня 2009

Можно итеративно пройти по изменяемому упорядоченному дереву, записав родителя узла в выбранных вами ветвях и зная направление, в котором вы приближаетесь к узлу (вниз или вверх слева или справа, что является упорядоченным свойством дерево позволяет вам тестировать):

template <typename T, typename R>
void in_order ( BSTNode<T>* root, R (*routine)(T) ) {
    typedef BSTNode<T>* Node;

    Node current = root;
    Node parent  = 0;

    bool going_down = true;

    while ( current ) {
        Node next = 0;

        if ( going_down ) {
            if ( current -> leftChild ) {
                // navigate down the left, swapping prev with the path taken
                Node next_child = current -> leftChild;
                current -> leftChild = parent;
                parent = current;
                current = next_child;
            } else if ( current -> rightChild ) {
                // navigate down the right, swapping prev with the path taken
                Node next_child = current -> rightChild;
                current -> rightChild = parent;
                parent = current;
                current = next_child;
            } else {
                // leaf
                routine ( current -> data );
                going_down = false;
            }
        } else {
            // moving up to parent
            if ( parent ) {
                Node next_parent = 0;

                // came from the left branch
                if ( parent -> data > current -> data ) {
                    // visit parent after left branch
                    routine ( parent -> data );

                    // repair parent
                    next_parent = parent -> leftChild;
                    parent -> leftChild = current;

                    // traverse right if possible
                    if ( parent -> rightChild ) {
                        going_down = true;
                        // navigate down the right, swapping prev with the path taken
                        Node next_child = parent -> rightChild;
                        parent -> rightChild = next_parent;
                        //parent = current;
                        current = next_child;
                        continue;
                    }
                } else {
                    // came from the right branch
                    next_parent = parent -> rightChild;
                    parent -> rightChild = current;
                }

                current = parent;
                parent = next_parent;
            } else {
                break;
            }
        }
    }
}

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

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

Поскольку вы используете шаблоны, вам нужно всего лишь один раз выполнить код обхода и объединить его с шаблонами стратегий, чтобы переключаться между режимами pre, post и inorder или любым другим итерационным режимом, который вы хотите.

1 голос
/ 21 июня 2009

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

Хорошим примером является следующее: http://code.activestate.com/recipes/461776/.

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

1 голос
/ 21 июня 2009

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

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

0 голосов
/ 22 июня 2009

Конечно, вы можете сделать свой собственный стек.

Вы хотите скорость? Если routine(root->data) почти ничего не сделает, вы никогда не заметите разницу.

0 голосов
/ 21 июня 2009

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

Вы ответили сами. Что дешевле, стек или куча? (если вам не хватает места в стеке)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...