将递归转化成迭代的通用技术

 

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)。   
 
通过两个例子的分析,递归在某些场合下的效率是很低下的,因此必须使用迭代来实现。
但是,递归并不是不好,关于递归和迭代的转化问题,以下的观点来自这篇博客:
 
 

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

 

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

 

四.结束语

数学,是一门艺术

 

你可能感兴趣的:(递归)