zoukankan      html  css  js  c++  java
  • 将递归转化成迭代的通用技术

    http://blog.csdn.net/whinah/article/details/6419680

    从理论上讲,只要允许使用栈,所有的递归程序都可以转化成迭代。

    但是并非所有递归都必须用栈,不用堆栈也可以转化成迭代的,大致有两类

    1. 尾递归:可以通过简单的变换,让递归作为最后一条语句,并且仅此一个递归调用。
    // recursive
    int fac1(int n) {
        if (n <= 0) return 1;
        return n * fac1(n-1);
    }
    // iterative
    int fac2(int n) {
        int i = 0, y = 1;
        for (; i <= n; ++i) y *= i;
        return y;
    }
    

      自顶向下->自底向上:对程序的结构有深刻理解后,自底向上计算,比如 fibnacci 数列的递归->迭代转化。

    // recursive, top-down
    int fib1(int n) {
        if (n <= 1) return 1;
        return fib1(n-1) + fib1(n-2);
    }
    // iterative, down-top
    int fib2(int n) {
        int f0 = 1, f1 = 1, i;
        for (i = 2; i <= n; ++i) {
            int f2 = f1 + f0;
            f0 = f1; f1 = f2;
        }
        return f1;
    }
    

      

    对于非尾递归,就必须使用堆栈。可以简单生硬地使用堆栈进行转化:把函数调用和返回的地方翻译成汇编代码,然后把对硬件 stack 的  push, pop 操作转化成对私有 stack 的 push, pop ,这其中需要特别注意的是对返回地址的 push/pop,对应的硬件指令一般是 call/ret。使用私有 stack 有两个好处:

    1. 可以省去公用局部变量,也就是在任何一次递归调用中都完全相同的函数参数,再加上从这些参数计算出来的局部变量。
    2. 如果需要得到当前的递归深度,可以从私有 stack 直接拿到,而用递归一般需要一个单独的 depth 变量,然后每次递归调用加 1。

    我们把私有 stack 元素称为 Frame,那么 Frame 中必须包含以下信息:

    1. 返回地址(对应于每个递归调用的下一条语句的地址)
    2. 对每次递归调用都不同的参数

    通过实际操作,我发现,有一类递归的 Frame 可以省去返回地址!所以,这里又分为两种情况:

    • Frame 中可以省去返回地址的递归:仅有两个递归调用,并且其中有一个是尾递归。
    // here used a function 'partition', but don't implement it
    tempalte<class RandIter>
    void QuickSort1(RandIter beg, RandIter end) {
        if (end - beg <= 1) return;
        RandIter pos = partition(beg, end);
        QuickSort1(beg, pos);
        QuickSort1(pos + 1, end);
    }
    tempalte<class RandIter>
    void QuickSort2(RandIter beg, RandIter end) {
        std::stack<std::pair<RandIter> > stk;
        stk.push({beg, end});
        while (!stk.empty()) {
            std::pair<RandIter, RandIter> ii = stk.top(); stk.pop();
            if (ii.second - ii.first) > 1) {
                RandIter pos = partition(beg, end);
                stk.push({ii.first, pos});
                stk.push({pos + 1, ii.second});
            }
        }
    }
    

      Frame 中必须包含返回地址的递归,这个比较复杂,所以我写了个完整的示例:

    • 以MergeSort为例,因为 MergeSort 是个后序过程,两个递归调用中没有任何一个是尾递归
    • MergeSort3 使用了 GCC 的 Label As Value 特性,只能在 GCC 兼容的编译器中使用
    • 单纯对于这个实例来说,返回地址其实只有两种,返回地址为 0 的情况可以通过判断私有栈(varname=stk)是否为空,stk为空时等效于 retaddr == 0。如果要精益求精,一般情况下指针的最低位总是0,可以把这个标志保存在指针的最低位,当然,如此的话就无法对 sizeof(T)==1 的对象如 char 进行排序了。
    #include <stdio.h>
    #include <string.h>
    # if 1
    #include <stack>
    #include <vector>
    template<class T>
    class MyStack : public std::stack<T, std::vector<T> >
    {
    };
    #else
    template<class T>
    class MyStack {
    	union {
    		char*  a;
    	   	T* p;
       	};
    	int n, t;
    public:
    	explicit MyStack(int n=128) {
    		this->n = n;
    		this->t = 0;
    		a = new char[n*sizeof(T)];
    	}
    	~MyStack() {
    		while (t > 0)
    			pop();
    		delete[] a;
    	}
    	void swap(MyStack<T>& y) {
    		char* q = y.a; y.a = a; a = q;
    		int z;
    		z = y.n; y.n = n; n = z;
    		z = y.t; y.t = t; t = z;
    	}
    	T& top() const { 
    		return p[t-1];
       	}
    	void pop() {
    		--t;
    		p[t].~T();
    	}
    	void push(const T& x) {
    		x.print(); // debug
    		p[t] = x;
    		++t;
    	}
    	int size() const { return t; }
    	bool empty() const { return 0 == t; }
    	bool full() const { return n == t; }
    };
    #endif
    template<class T>
    struct Frame {
    	static T* base;
    	T *beg, *tmp;
    	int len;
    	int retaddr;
    	Frame(T* beg, T* tmp, int len, int retaddr)
    		: beg(beg), tmp(tmp), len(len), retaddr(retaddr)
    	{}
    	void print() const { // for debug
    		printf("%4d %4d %d/n", int(beg-base), len, retaddr);
    	}
    };
    template<class T> T* Frame<T>::base;
    #define TOP(field) stk.top().field
    template<class T>
    bool issorted(const T* a, int n)
    {
    	for (int i = 1; i < n; ++i) {
    		if (a[i-1] > a[i]) return false;
    	}
    	return true;
    }
    template<class T>
    void mymerge(const T* a, int la, const T* b, int lb, T* c) {
    	int i = 0, j = 0, k = 0;
    	for (; i < la && j < lb; ++k) {
    		if (b[j] < a[i])
    			c[k] = b[j], ++j;
    		else
    			c[k] = a[i], ++i;
    	}
    	for (; i < la; ++i, ++k) c[k] = a[i];
    	for (; j < lb; ++j, ++k) c[k] = b[j];
    }
    template<class T>
    void MergeSort1(T* beg, T* tmp, int len) {
    	if (len > 1) {
    		int mid = len / 2;
    		MergeSort1(beg    , tmp    , mid);
    		MergeSort1(beg+mid, tmp+mid, len-mid);
    		mymerge(tmp, mid, tmp+mid, len-mid, beg);
    		memcpy(tmp, beg, sizeof(T)*len);
    	}
    	else
    		*tmp = *beg;
    }
    template<class T>
    void MergeSort2(T* beg0, T* tmp0, int len0) {
    	int mid;
    	int cnt = 0;
    	Frame<T>::base = beg0;
    	MyStack<Frame<T> > stk;
    	stk.push(Frame<T>(beg0, tmp0, len0, 0));
    	while (true) {
    		++cnt;
    		if (TOP(len) > 1) {
    			mid = TOP(len) / 2;
    			stk.push(Frame<T>(TOP(beg), TOP(tmp), mid, 1));
    			continue;
    L1:
    			mid = TOP(len) / 2;
    			stk.push(Frame<T>(TOP(beg)+mid, TOP(tmp)+mid, TOP(len)-mid, 2));
    			continue;
    L2:
    			mid = TOP(len) / 2;
    			mymerge(TOP(tmp), mid, TOP(tmp)+mid, TOP(len)-mid, TOP(beg));
    			memcpy(TOP(tmp), TOP(beg), sizeof(T)*TOP(len));
    		} else
    			*TOP(tmp) = *TOP(beg); 
    		int retaddr0 = TOP(retaddr);
    		stk.pop();
    		switch (retaddr0) {
    		case 0: return;
    		case 1: goto L1;
    		case 2: goto L2;
    		}
    	}
    }
    // This Implementation Use GCC's goto saved label value
    // Very similiar with recursive version
    template<class T>
    void MergeSort3(T* beg0, T* tmp0, int len0) {
    MyEntry:
    	int mid;
    	int retaddr;
    	Frame<T>::base = beg0;
    	MyStack<Frame<T> > stk;
    	stk.push(Frame<T>(beg0, tmp0, len0, 0));
    #define Cat1(a,b) a##b
    #define Cat(a,b) Cat1(a,b)
    #define HereLabel() Cat(HereLable_, __LINE__)
    #define RecursiveCall(beg, tmp, len) /
    	stk.push(Frame<T>(beg, tmp, len, (char*)&&HereLabel() - (char*)&&MyEntry)); /
    	continue; /
    	HereLabel():;
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // retaddr == 0 是最外层的递归调用,
    // 只要到达这一层时 retaddr 才为 0,
    // 此时就可以返回了
    #define MyReturn /
    	retaddr = TOP(retaddr); /
    	stk.pop(); /
    	if (0 == retaddr) { /
    		return; /
       	} /
    	goto *((char*)&&MyEntry + retaddr);
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    	while (true) {
    		if (TOP(len) > 1) {
    			mid = TOP(len) / 2;
    			RecursiveCall(TOP(beg), TOP(tmp), mid);
    			mid = TOP(len) / 2;
    			RecursiveCall(TOP(beg)+mid, TOP(tmp)+mid, TOP(len)-mid);
    			mid = TOP(len) / 2;
    			mymerge(TOP(tmp), mid, TOP(tmp)+mid, TOP(len)-mid, TOP(beg));
    			memcpy(TOP(tmp), TOP(beg), sizeof(T)*TOP(len));
    		} else
    			*TOP(tmp) = *TOP(beg); 
    		MyReturn;
    	}
    }
    template<class T>
    void MergeSortDriver(T* beg, int len, void (*mf)(T* beg_, T* tmp_, int len_))
    {
    	T* tmp = new T[len];
    	(*mf)(beg, tmp, len);
    	delete[] tmp;
    }
    #define test(a,n,mf) /
    	memcpy(a, b, sizeof(a[0])*n); /
    	MergeSortDriver(a, n, &mf); /
    	printf("sort by %s:", #mf); /
    	for (i = 0; i < n; ++i) printf("% ld", a[i]); /
    	printf("/n");
    int main(int argc, char* argv[])
    {
    	int n = argc - 1;
    	int i;
    	long* a = new long[n];
    	long* b = new long[n];
    	for (i = 0; i < n; ++i)
    		b[i] = strtol(argv[i+1], NULL, 10);
    	test(a, n, MergeSort1);
    	test(a, n, MergeSort2);
    	test(a, n, MergeSort3);
    	printf("All Successed/n");
    	delete[] a;
    	delete[] b;
    	return 0;
    }
    

      

    http://itjingyingjida.blog.sohu.com/161173529.html

    一个算法设计技巧:递归转化成迭代。


    在某些特定的情况下,递归的效率是非常低的,必须使用迭代来实现。 下面,将通过两个经典的递归例子,来感悟这两种算法设计方法的效率问题。
     
    1.计算n的阶乘。 
    学过最基本程序设计知识的同学都知道,递归处理。
    #include <stdio.h>
    #include<stdlib.h>
    long fact(int);

    int main(void)
    {   int n;
        printf("请输入一个非负整数n:\n");
        scanf("%d",&n);
        if(n<0)
           printf("error:n must>0.");
           else
        printf("%d的阶乘为%d.\n",n,fact(n));
        system("pause");
        return 0;
    }   
       
    //计算递归的函数        
    long fact(int n)
    {
        if(n<=0)
            return -1;
        else
            return n*fact(n-1);
    }   


    2.计算Fabonacci数列。

    基本定义:

    f(n)=0,n=0;

    f(n)=1,n=1;

    f(n)=1,n=2;

    f(n)=f(n-1)+f(n-2),(n>2);递归定义。

    递归实现:

    #include <stdio.h> 

    //计算数列第n项  
    long fib(int n)

    if (n == 1 || n == 2)  
               return 1;
      return fib(n - 1) + fib(n - 2);
    }  

    int main()
    {   
       int n;
      scanf("%d", &n);
      printf("%ld\n", fib(n));
      return 0;  
    }     


    一个程序,或者一个算法写好之后,我们自然而然地想到:好有没有更好的实现??这个算法好么??能改进么?? 

    对于这两个程序,效率确实很低下,必须使用更优秀的方式来实现。 

    一.先分析Fibonacci数列的计算。  
    1.感性的认知。  
      
     
      可以看到,f(1)和f(2)被重复计算了很多次,因此效率显得很低下。 

       

    2.理性分析。     

    这里的工作即将集中在程序运行时间的分析上面。 

    假设,计算第n项需要的时间为T(n)。 

    由递归关系式知道:T(n) = T(n - 1) + T(n - 2),n >= 3;T(n) = 1。这里的1代表1个CPU单位时间,即CPU计算一个基本语句所需要的时间。 例如,赋值语句,比较大小等等。 

    先定义一个函数,叫做生成函数,也叫母函数。

    (1)Stirling公式的证明过程如下(来自维基百科):

     

    这个公式,以及误差的估计,可以推导如下。我们不直接估计n!,而是考虑它的自然对数

    这个方程的右面是积分的近似值(利用梯形法则),而它的误差由欧拉-麦克劳林公式给出:

    其中Bk伯努利数Rm,n是欧拉-麦克劳林公式中的余项。取极限,可得:

    我们把这个极限记为y。由于欧拉-麦克劳林公式中的余项Rm,n满足:

    其中我们用到了大O符号,与以上的方程结合,便得出对数形式的近似公式:

    两边取指数,并选择任何正整数m,我们便得到了一个含有未知数ey的公式。当m=1时,公式为:

    n趋于无穷大时,两边取极限,并利用沃利斯乘积,便可以得出ey()。因此,我们便得出斯特灵公式:

    这个公式也可以反复使用分部积分法来得出,首项可以通过最速下降法得到。把以下的和

    用积分近似代替,可以得出不含的因子的斯特灵公式(这个因子通常在实际应用中无关):

    [编辑]收敛速率和误差估计

     
    y轴表示截断的斯特灵级数的相对误差,x轴表示所使用的项数。

    更加精确的近似公式为:

    其中:

    斯特灵公式实际上是以下级数(现在称为斯特灵级数)的第一个近似值:

    当时,截断级数的误差等于第一个省略掉的项。这是渐近展开式的一个例子。它不是一个收敛级数;对于任何特殊值n,级数的准确性只在取有限个项时达到最大,如果再取更多的项,则准确性将变得越来越差。

    阶乘的对数的渐近展开式也称为斯特灵级数:

    在这种情况下,级数的误差总是与第一个省略掉的项同号,且最多同大小。

    (3)数据分析

    至此,数学分析已经臻于完美。

    终于知道,自己为什么学数学了,呵呵 。 微笑  偷笑 大笑   

     

    两个算法的迭代版本:
    1.斐波那契数列的第n项 

    int Fibo(int n){ 
     
     int a1=1,a2=1; //前两项 
     int an ; //第n项 

     if(n==1||n==2)  //n <= 2返回1  
      return 1 ;

     for(int i=3;i<n;i++) 
     { 
      an=a1+a2; 
      a1=a2; 
      a2=an; 
     }

        return an; 
    }   

     

    2.计算n的阶乘

    #include <stdio.h>
    int main()
    {
     int i,n,s=1;

      scanf("%d",&n);
     
      for(i=1;i<=n;i++)
         s*=i;
     
      printf("%d!=%d\n",n,s);
     
     return 0; 
    }

        

     
    这两个版本都用了一个for循环来实现迭代,只有n次循环就可以完成,效率有了
    很大的改善。运行时间都是O(n)。  
     
    通过两个例子的分析,递归在某些场合下的效率是很低下的,因此必须使用迭代来实现。
    但是,递归并不是不好,关于递归和迭代的转化问题,以下的观点来自这篇博客:
     
     

    ****不需要消解的递归
    那种盲目的消解递归,不惜一切代价躲避递归,认为“递归的速度慢,为了提高速度,必须用栈或者其他的方法来消解”的说法是很片面的。如果一个递归过程用非递归的方法实现后,速度提高了,那只是因为递归做了一些无用功。假使一个递归过程必须要用栈才能消解,那么完全模拟后的结果根本就不会对速度有任何提升,只会减慢;如果你改完后速度提升了,那只证明你的递归函数写的有问题,如多了许多重复操作——打开关闭文件、连接断开数据库,而这些完全可以放到递归外面。可以在本质上是非递归的机器上实现递归过程这一事实本身就证明:为着实际目的,每一个递归程序都可以翻译成纯粹迭代的形式,但这包含着对递归栈的显式处理,而这些运算常常模糊了程序的本质,以致使它非常难以理解。 
    因此,是递归的而不是迭代的算法应当表述成递归过程。如汉诺塔问题等。汉诺塔问题的递归算法中有两处递归调用,并且其中一处递归调用语句后还有其他语句,因此该递归算法不是尾递归或单向递归。要把这样的递归算法转化为非递归算法,并没有提高程序运行的速度,反而会使程序变得复杂难懂,这是不可取的。也就是说,很多递归算法并不容易改写成迭代程序:它们本质上是递归的,没有简单的迭代形式。这样的递归算法不宜转化为非递归算法。

    说到底,在我们选择算法时应该全面分析算法的可行性、效率、代码优化。在综合了算法的各个因素后,选择合适的算法来编写程序,这样的程序才会达到优化的效果。

     

    四.结束语

    数学,是一门艺术

  • 相关阅读:
    快速排序
    优先队列
    堆排序
    树、二叉树基础
    分治法
    递归算法详细分析
    算法基础
    Linux文件系统详解
    fs/ext2/inode.c相关函数注释
    块设备的读流程分析
  • 原文地址:https://www.cnblogs.com/anorthwolf/p/3134307.html
Copyright © 2011-2022 走看看