【问题标题】:Optimizing recursive calls over data structures优化对数据结构的递归调用
【发布时间】:2010-11-04 16:47:56
【问题描述】:

是否有一种算法可以将高度递归的函数优化为对数据结构的迭代?例如,给定以下函数...

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

(当然不同的是,调用栈不是填充调用栈,而是填充堆中的另一个栈。)


编辑:我的意思是可以由编译器执行的算法,所以我不必手动优化它。

【问题讨论】:

  • 当您问“是否有算法”时,您是在问是否有人类可以遵循的通用算法来执行转换,或者您是否在问是否有一些 STL 或其他功能要做这是自动的吗?
  • 一种可由任何语言的编译器执行的算法,不一定是 C++ 编译器。
  • 这不会是对大多数 CPU 架构和语言的优化,因为基于处理器的调用堆栈比您使用的自定义构建堆栈快得多(因为它是处理器内部的)在你的第二个例子中。您应该使用迭代的较短的递归变体,因为它可能更快且更易于阅读。如果可能有更快的实现,那么现在大多数编译器已经在一定程度上展开了这样的循环。您应该在这里依赖编译器本身。如果有疑问,请使用第一条优化规则:衡量。
  • 如果“任何语言”是你想要的,这里是 LISP 的一个:user.it.uu.se/~torer/publ/remrec.html——这里是 PL/I 的一个:portal.acm.org/citation.cfm?id=359344——这是一个长期存在的、仍然活跃的领域研究,但我不知道有任何流行语言的编译器实际上提供了此类工具。

标签: optimization data-structures recursion


【解决方案1】:

是的,你可以这样做。

但是,除了非常深的树可能会耗尽堆栈空间之外,这里没有“优化”。如果您需要提高速度,请考虑使用threading your trees

【讨论】:

  • 感谢您的解决方案,恕我直言,一个非常聪明的解决方案。然而,我写的有序遍历代码只是一个例子。我正在编写的实际程序中有一些非常大的数据结构。
  • 您的元问题的元答案可能类似:重新设计/增强您的结构以 (a) 支持非递归迭代或 (b) 首先不需要迭代。
【解决方案2】:

我遇到的唯一通用递归优化是优化尾递归。这通常在函数式语言中完成,基本上涉及编译器/解释器将最终操作是递归调用的函数更改为完全迭代的函数(因此堆栈没有问题等)。

对于其他形式的递归,我不知道已经找到/创建了任何用于将它们优化为迭代函数的通用算法。您当然可以始终以迭代的方式表达这些函数,但这种转换不是通用的。

【讨论】:

  • 尾递归相当简单:首先覆盖参数,然后转到第一行代码。我的实际问题并不那么简单:我正在编写的程序中有一个非常大而复杂的数据结构,我想以一种文明的方式(递归,而不是迭代)遍历它。但我得到的只是堆栈溢出。
【解决方案3】:

您要求continuation-passing-style 转换与defunctionalized 延续;它在Essentials of Programming Languages 的第 6 章中有介绍,代码在 Scheme 中。但是为 C++ 实现将是一个巨大的痛苦。但是,如果您有一个编译器前端可以将 C++ 转换为可合理访问的数据结构,并且您需要对大量代码执行此操作。这本书还解释了如何手动系统地进行这种转换,这在您的情况下更有可能是实用的。

【讨论】:

    【解决方案4】:

    从技术上讲,答案是“是”:任何可以递归表达(即使用隐式堆栈)的算法也可以重新制定以使用迭代(即使用显式堆栈或其他跟踪结构)。

    但是,我怀疑大多数编译器不能或不会尝试为您执行此操作。想出一个通用的自动算法来执行转换可能相当困难,虽然我从未尝试过,所以我不应该说它是棘手的。

    【讨论】:

      【解决方案5】:

      可以在不使用递归的情况下深度优先遍历树。

      一个很好的例子如下:http://code.activestate.com/recipes/461776/

      不过,编译器不会为您执行此优化。然而,这个概念并不难掌握。您正在做的是自己创建调用堆栈和节点列表,而不是使用函数调用来深入树。

      【讨论】:

        【解决方案6】:

        可以迭代地遍历一个可变的有序树,方法是在你选择的分支中记录节点的父节点,并知道你接近节点的方向(向下,或从左或右向上,其有序属性树让你测试):

        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 或您想要的任何其他迭代模式之间切换。

        【讨论】:

        • 这个主意不错。但是,在您的代码运行之后,树的深度会增加,因此插入和查找等普通操作在计算上变得更加复杂。但我想这就是在这种特殊情况下不使用堆栈所必须付出的代价。
        【解决方案7】:

        当然,不同的是, 而不是填充调用堆栈, 后者填充另一个堆栈 堆。

        你自己回答了。什么更便宜,堆栈还是堆? (除非你的堆栈空间用完了)

        【讨论】:

        • 我想要一个算法,给定第一个代码 sn-p,输出第二个代码,所以我不必手动编写优化但很长的代码! :)
        • 鉴于栈在大多数情况下比堆便宜得多,我不明白为什么:)
        • 我有一些非常大的数据结构。
        【解决方案8】:

        当然,您可以制作自己的堆栈。

        你想要速度吗?除非routine(root-&gt;data) 几乎什么都不做,否则您永远不会注意到差异。

        【讨论】:

        • 我想避免堆栈溢出。
        • @Eduardo:我明白——这就是为什么我说你可以自己制作堆栈。即使您自己的堆栈可能会比 CPU 堆栈效率低,但如果例程(根->数据)在每个节点上花费的周期比您在推送和弹出每个节点上花费的时间多得多,那么它可能不会产生明显的差异,即例程( ) 将在这种情况下占主导地位。
        猜你喜欢
        • 2018-03-08
        • 2016-09-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-08-27
        • 1970-01-01
        • 2011-11-30
        相关资源
        最近更新 更多