我知道递归有时比循环要简洁得多,我不是在问什么时候应该用递归而不是迭代,我知道已经有很多关于这个的问题了。

我问的是,递归比循环快吗?在我看来,你总是能够细化一个循环,让它比递归函数执行得更快,因为循环是不存在的,不断地建立新的堆栈帧。

我特别在寻找在递归是正确处理数据的方法的应用程序中递归是否更快,例如在一些排序函数中,在二叉树中等等。


当前回答

这里大部分答案都是错误的。正确答案是视情况而定。例如,这里有两个遍历树的C函数。首先是递归的:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

下面是使用迭代实现的相同函数:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

理解代码的细节并不重要。p是节点,P_FOR_EACH_CHILD执行遍历。在迭代版本中,我们需要一个显式的堆栈st,其中节点被推入,然后弹出和操作。

递归函数比迭代函数运行得快得多。原因是在后者中,对于每一项,都需要调用函数st_push,然后调用函数st_pop。

在前者中,每个节点只有递归的CALL。

另外,访问调用堆栈上的变量非常快。这意味着您正在从内存中读取,而内存可能总是在最里面的缓存中。另一方面,显式堆栈必须由来自堆的malloc:ed内存支持,而这要慢得多。

通过仔细的优化,例如内联st_push和st_pop,我可以大致达到与递归方法相同的效果。但至少在我的计算机上,访问堆内存的开销要大于递归调用的开销。

但是这个讨论基本上是没有意义的,因为递归树遍历是不正确的。如果你有一个足够大的树,你会用完调用堆栈空间,这就是为什么必须使用迭代算法。

其他回答

这只是猜测。一般来说,如果两者都使用了非常好的算法(不考虑实现难度),那么在规模相当大的问题上,递归可能不会经常击败循环,如果使用带有尾部调用递归的语言(以及带有循环的尾部递归算法,也是语言的一部分),情况可能会有所不同——它们可能非常相似,甚至有时更喜欢递归。

从理论上讲,两者是一样的。 具有相同O()复杂度的递归和循环将以相同的理论速度工作,但当然,实际速度取决于语言、编译器和处理器。 数幂例可用O(ln(n))迭代编码:

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }

这里有一个例子,在Java中递归比循环运行得快。这是一个对两个数组执行冒泡排序的程序。recBubbleSort(....)方法使用递归对数组arr排序,bbSort(....)方法仅使用循环对数组narr排序。两个数组中的数据是相同的。

public class BBSort_App {
    public static void main(String args[]) {
        int[] arr = {231,414235,23,543,245,6,324,-32552,-4};

        long time = System.nanoTime();
        recBubbleSort(arr, arr.length-1, 0);
        time = System.nanoTime() - time;

        System.out.println("Time Elapsed: "+time+"nanos");
        disp(arr);

        int[] narr = {231,414235,23,543,245,6,324,-32552,-4};
    
        time = System.nanoTime();
        bbSort(narr);
        time = System.nanoTime()-time;
        System.out.println("Time Elapsed: "+time+"nanos");
        disp(narr);
    }

    static void disp(int[] origin) {
        System.out.print("[");
        for(int b: origin)
            System.out.print(b+", ");
        System.out.println("\b\b \b]");
    }
    static void recBubbleSort(int[] origin, int i, int j) {
        if(i>0)
            if(j!=i) {
                if(origin[i]<origin[j]) {
                    int temp = origin[i];
                    origin[i] = origin[j];
                    origin[j] = temp;
                }
                recBubbleSort(origin, i, j+1);
            }
            else
                recBubbleSort(origin, i-1, 0);
    }

    static void bbSort(int[] origin) {
        for(int out=origin.length-1;out>0;out--)
            for(int in=0;in<out;in++)
                if(origin[out]<origin[in]) {
                    int temp = origin[out];
                    origin[out] = origin[in];
                    origin[in] = temp;
                }
    }
}

即使运行该测试50次,也会得到几乎相同的结果:

对这个问题的回答是令人满意的,但没有简单的例子。有人能给出为什么这个递归更快的原因吗?

Most answers here forget the obvious culprit why recursion is often slower than iterative solutions. It's linked with the build up and tear down of stack frames but is not exactly that. It's generally a big difference in the storage of the auto variable for each recursion. In an iterative algorithm with a loop, the variables are often held in registers and even if they spill, they will reside in the Level 1 cache. In a recursive algorithm, all intermediary states of the variable are stored on the stack, meaning they will generate many more spills to memory. This means that even if it makes the same amount of operations, it will have a lot memory accesses in the hot loop and what makes it worse, these memory operations have a lousy reuse rate making the caches less effective.

递归算法的缓存行为通常比迭代算法差。

一般来说,不,在任何具有两种形式的可行实现的实际使用中,递归不会比循环快。我的意思是,当然,你可以编写出花费很长时间的循环,但是有更好的方法来实现相同的循环,可以通过递归来实现相同的问题。

关于原因,你一针见血;创建和销毁堆栈帧比简单的跳转代价更大。

但是,请注意我所说的“在两种形式中都有可行的实现”。对于像许多排序算法这样的事情,往往没有一种非常可行的方法来实现它们,因为它不能有效地建立自己的堆栈版本,因为生成子“任务”是这个过程固有的一部分。因此,递归可能和试图通过循环实现算法一样快。

编辑:这个答案是假设非函数式语言,其中大多数基本数据类型是可变的。它不适用于函数式语言。