递归算法转换为非递归算法的技巧

递归函数具备很好的可读性和可维护性,可是大部分状况下程序效率不如非递归函数,因此在程序设计中通常喜欢先用递归解决问题,在保证方法正确的前提下再转换为非递归函数以提升效率。html

函数调用时,须要在栈中分配新的帧,将返回地址,调用参数和局部变量入栈。因此递归调用越深,占用的栈空间越多。若是层数过深,确定会致使栈溢出,这也是消除递归的必要性之一。递归函数又能够分为尾递归和非尾递归函数,前者每每具备很好的优化效率,下面咱们分别加以讨论。web

尾递归函数算法

尾递归函数是指函数的最后一个动做是调用函数自己的递归函数,是递归的一种特殊情形。尾递归具备两个主要的特征:数组

  1. 调用自身函数(Self-called)函数

  2. 计算仅占用常量栈空间(Stack Space)oop

为何尾递归能够作到常量栈空间,咱们用著名的fibonacci数列做为例子来讲明。性能

fibonacci数列实现方法通常是这样的,优化

int FibonacciRecur(int n) {
  if (0==n) return 0;
  if (1==n) return 1;
  return FibonacciRecur(n-1)+FibonacciRecur(n-2);
}

不过须要注意的是这种实现方法并非尾递归,由于尾递归的最后一个动做必须是调用自身,这里最后的动做是加法运算,因此咱们要修改一下,ui

int FibonacciTailRecur(int n, int acc1, int acc2) {
  if (0==n) return acc1;
  return FibonacciTailRecur(n-1, acc2, acc1+acc2);
}

好了,如今符合尾递归的定义了,用gcc分别加-O-O2选项编译,下面是部分汇编代码,lua

-O2汇编代码

FibonacciTailRecur:
.LFB12:
        testl   %edi, %edi
        movl    %esi, %eax
        movl    %edx, %esi
        je      .L4
        .p2align 4,,7
.L7:
        leal    (%rax,%rsi), %edx
        decl    %edi
        movl    %esi, %eax
        testl   %edi, %edi
        movl    %edx, %esi
        jne     .L7   // use jne 
.L4:
        rep ; ret

-O汇编代码

FibonacciTailRecur:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        subq    $16, %rsp
.LCFI2:
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    %edx, -12(%rbp)
        cmpl    $0, -4(%rbp)
        jne     .L2
        movl    -8(%rbp), %eax
        movl    %eax, -16(%rbp)
        jmp     .L1
.L2:
        movl    -12(%rbp), %eax
        movl    -8(%rbp), %edx
        addl    %eax, %edx
        movl    -12(%rbp), %esi
        movl    -4(%rbp), %edi
        decl    %edi
        call     FibonacciTailRecur  //use call
        movl    %eax, -16(%rbp)
.L1:
        movl    -16(%rbp), %eax
        leave
        ret

能够看到-O2时用了jne命令,每次调用下层递归并无申请新的栈空间,而是更新当前帧的局部数据,重复使用当前帧,因此无论有多少层尾递归调用都不会栈溢出,这也是使用尾递归的意义所在。

-O使用的是call命令,这会申请新的栈空间,也就是说gcc默认状态下并无优化尾递归,这么作的一个主要缘由是有时候咱们须要保留帧信息用于调试,而加-O2优化后,无论多少层尾递归调用,使用的都是第一层帧,是得不到当前帧的信息的,你们能够用gdb调试下就知道了。

除了尾递归,Fibonacci数列很容易推导出循环实现方式,

int fibonacciNonRecur(int n) {
  int acc1 = 0, acc2 = 1;
  for(int i=0; i<n; i++){
    int t = acc1;
    acc1 = acc2;
    acc2 += t;
  }
  return acc1;
}

 在个人机器上,所有加-O2选项优化编译,运行时间以下(单位微秒)

n

fibonacciNonRecur

FibonacciTailRecur

FibonacciRecur

20

1

1

123

30

1

1

14144

fibonacci函数的迭代,尾递归和递归函数性能比较,能够发现迭代和尾递归时间几乎一致,n的大小对迭代和尾递归运行时间影响很小,由于只是多执行On)条机器指令而已。可是n对递归函数影响很是大,这是因为递归须要频繁分配回收栈空间所致。正是因为尾递归的高效率,在一些语言如lua中就明确建议使用尾递归(参照《lua程序设计第二版》第6章)。

非尾递归函数

编译器没法自动优化通常的递归函数,不过经过模拟递归函数的过程,咱们能够借助于栈将任何递归函数转换为迭代函数。直观点,递归的过程实际上是编译器帮咱们处理了压栈和出栈的操做,转换为迭代函数就须要手动地处理压栈和出栈。

下面咱们以经典的快速排序为例子。

int partition(int *array, int low, int high) {
    int val = array[low];
    while(low < high) {
      while(low<high && array[high]>=val) --high;
     swap(&array[low], &array[high]);
     while(low<high && array[low]<=val) ++low;
     swap(&array[low], &array[high]);
   }
   return low;
 }
  void Quicksort(int *array, int b, int e) {
    if (b >= e) return;
   int p = partition(array, b, e);
    Quicksort(array, b, p-1);
    Quicksort(array, p+1, e);
 }

其实不难看出快速排序的递归算法就是一个二叉树的先序遍历过程,先处理当前根节点,而后依次处理左子树和右子树。将快速排序递归算法转换为非递归至关于将二叉树先序遍历递归算法转为非递归算法。

二叉树先序遍历递归算法伪码

void PreorderRecursive(Bitree root){
  if (root) {
    visit(root);
    PreorderRecursive(root->lchild); 
    PreorderRecursive(root->rchild); 
  }
}

二叉树先序遍历非递归伪码

void PreorderNonRecursive(Bitree root){
  stack stk;
  stk.push(root);
  while(!stk.empty()){
    p = stk.top();
    visit(p);
    stk.pop();
    if(p.rchild) stk.push(stk.rchild);
    if(p.lchild) stk.push(stk.lchild);
  }
}

每次处理完当前节点后将右子树和左子树分别入栈,相似地,咱们也很容易获得快速排序的非递归算法实现。partition将数组分为左右两部分,至关与处理当前节点,接下来要作的就是将左右子树入栈,那么左右子树须要保存什么信息呢?这个是处理非递归函数的关键,由于被调用函数信息须要压入栈中。快速排序只须要保存子数组的边界便可。

void QuicksortNonRecur(int *array, int b, int e) {
  if (b >= e) return;
  std::stack< std::pair<int, int> > stk;
  stk.push(std::make_pair(b, e));
  while(!stk.empty()) {
    std::pair<int, int> pair = stk.top();
    stk.pop();
    if(pair.first >= pair.second) continue;
    int p = partition(array, pair.first, pair.second);
    if(p < pair.second) stk.push(std::make_pair(p+1, e));
    if(p > pair.first) stk.push(std::make_pair(b, p-1));
  }
}

 

总结

虽然将递归函数转换为迭代函数能够提升程序效率,可是转换后的迭代函数每每可读性差,难以理解,不易维护。因此只有在特殊状况下,好比对栈空间有严格要求的嵌入式系统,才须要转换递归函数。大部分状况下,递归并不会成为系统的性能瓶颈,一个代码简单易读的递归函数经常比迭代函数更易维护。

Reference

https://secweb.cs.odu.edu/~zeil/cs361/web/website/Lectures/recursionConversion/page/recursionConversion.html

http://en.wikipedia.org/wiki/Tail_Recursion

http://c2.com/cgi/wiki?TailRecursion

http://c2.com/cgi/wiki?RecursionVsLoop

相关文章
相关标签/搜索