漫谈递归和迭代

先讲个故事吧。
算法

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”。数据库

这个故事永远也讲不完,由于没有递归结束条件。老师讲递归时老是说,递归很简单,一个递归结束条件,一个本身调用本身。若是递归没有结束条件,那么就会无限递归下去。在编程的时候,没有递归结束条件或者递归过深,通常会形成栈溢出。编程

下面这个函数,能够利用栈溢出来估测栈的大小:ruby

void stack_size()
{
    static int call_time = 0;
    char dummy[1024*1024];
    call_time++;
    printf("call time: %d\n",call_time);
    stack_size();
}

这个函数定义了1M的局部变量,而后调用本身。栈溢出时会崩溃,根据最后打印出的数字能够算一下栈的大小。app

递归算法通常用于解决三类问题:
编程语言

(1)数据的定义是按递归定义的。(Fibonacci函数)ide

(2)问题解法按递归算法实现。(回溯)wordpress

(3)数据的结构形式是按递归定义的。(树的遍历,图的搜索)函数式编程

对于求1+2+3+…+n这种问题,大部分人不会用递归方式求解:函数

int sum1(int n)
{
    if(n == 0)
        return 0;
    else
        return n+sum1(n-1);
}

而是使用迭代的方式:

int sum2(int n)
{
    int ret = 0;
    for(int i = 1;  i <= n; i++)
              ret += i;
    return ret;
}


迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快、适合作重复性操做的特色,让计算机对一组指令(或必定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。

为何使用迭代而不用递归呢?

很明显,使用递归时每调用一次,就须要在栈上开辟一块空间,而使用递归就不须要了,所以,不少时候设计出了递归算法,还要想法设法修改为迭代算法。

假如如今咱们不考虑编程,咱们仅仅看一下上面使用递归和迭代求1+2+3…+n的过程。

使用递归:

sum(5)
5+sum(4)
5+4+sum(3)
5+4+3+sum(2)
5+4+3+2+sum(1)
5+4+3+2+1+sum(0)
5+4+3+2+1+0
5+4+3+2+1
5+4+3+3
5+4+6
5+10
15

使用迭代

0+1=1
1+2=3
3+3=6
6+4=10
10+5=15

上面两个计算过程所需的步骤都是O(n)。可是两个计算过程的形状不同。

递归过程是一个先逐步展开然后收缩的形状,在展开阶段,这一计算过程构造起一个推迟进行的操做所造成的的链条(这里是+),在收缩阶段才会实际执行这些操做。这种类型的计算过程由一个推迟执行的运算链条刻画,称为一个递归计算过程。要执行这种计算过程,就须要维护之后将要执行的操做的轨迹。在计算1+2+3+…+n时,推迟执行的加法链条的长度就是为了保存其轨迹须要保存的信息量,这个长度随着n值而线性增加,这样的过程称为线性递归过程。

迭代过程的造成没有任何增加或收缩。对于任意一个n,在计算的每一步,咱们须要保存的就只有i,ret,这个过程就是一个迭代计算过程。通常来讲,迭代计算过程就是那种其状态能够用固定数目的状态变量描述的结算过程。在计算1+2+…+n时,所需的计算步骤与n成正比,这种过程称为线性迭代过程。

如今再回到编程语言中。

上面提到的推迟执行的运算链条就存在栈里,因为栈很小,若是链条太长,就会溢出了。

那咱们再来看下面的函数

int sum3(int n, int acc)
{
    if(n == 0)
        return acc;
    else
        return sum3(n-1,acc+n);
}

调用的时候acc=0,以sum(5,0)为例这是一个递归函数,咱们来看看它的计算过程。

sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)
15

这个计算过程是递归的仍是迭代的呢?

是迭代的!

可是命名函数sum又调用了本身。

咱们须要将递归计算过程与递归过程分隔开。

当咱们说一个过程(函数)是递归的时候,论述的是一个语法形式上的事实,说明这个过程的定义中(直接或间接的)调用了本身。咱们说一个计算过程具备某种模式时(例如线性递归),咱们说的是这一计算过程的进展方式,而不是过程说些上的语法形式。

一个递归过程,若是它的计算过程是迭代的,那么咱们称这种递归为尾递归。尾递归不须要保存递归的推迟计算链,那么是否是就意味着不会形成栈溢出了?

咱们来试一下

int sum3(int n, int acc)
{
    if(n == 0)
        return acc;
    else
        return sum3(n-1,acc+n);
}
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d\n",sum(n,0));
    return 0;
}


运行结果
2013062001.png

看来仍是会栈溢出。

为啥呢?由于c语言默认不会对尾递归进行优化,即便你的程序是尾递归的,它仍是按通常的递归进行编译。加上优化选项就能够对尾递归进行优化。

2013062002-300x130.png

下面哪些是尾递归呢?

int fib(int n)
{
    if(n == 0 || n == 1)
        return 1;
    else
        return fib(n-1) + fib(n-2);
}
void qsort(int A, int p, int q)
{
    r = partition(A,p,q);
    qsort(A,p,r-1);
    qsort(A,r+1,q);
}
int gcd(int a, int b)
{
    if(b == 0)
        return a;
    else
        gcd(b, a%b);
}

在函数式编程语言中,不存在变量,所以任何的循环都须要用递归实现。若是递归使用了尾递归,那么编译器或解释器能自动优化,若是不是尾递归,那么就存在栈溢出的风险。前面两个不是尾递归,第三个是尾递归。

任何递归均可以转化成迭代,那么任何递归均可以转化成尾递归。

斐波那契数列改为尾递归后以下

int fib(int n,int count, int a , int b)
{
    if(n == 0 || n == 1)
        return 1;
    else if (count > n)
        return b;
    else
        return fib(n,count+1,b,a+b);
}
int FIB(int n)
{
    return fib(n,2,1,1);
}

下面这段代码

i = 1, ret = 0
for(;i <= n; i++)
        ret += i;

对应的递归形式就是

int fun(int i, int ret) {
    if(i > n)
        return ret;
    else
        return fun(ret+i,i+1);
}

fun(1,0)至关于给i和ret赋初值。

若是将快速排序改为迭代的话,那么须要一个栈!它的变量个数是有限的吗?咱们能够把栈当作一个变量就能够了。

先修改为迭代形式

void qsort_iterate(int a[],int p,int q)
{
        stack s;
        s.push(p);
        s.push(q);
        while(!s.empty())
        {
                int high = s.top();
                s.pop();
                int low = s.top();
                s.pop();
                if(high > low)
                {
                        int r = partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r+1);
                        s.push(high);
                }
        }
}

上面的迭代形式能够很容易的改为尾递归:

void qsort_tail(int a[],stack s)
{
        if(!s.empty())
        {
                int high = s.top();
                s.pop();
                int low = s.top();
                s.pop();
                if(high > low)
                {
                        int r = partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r+1);
                        s.push(high);
                }
                qsort_tail(a,s);
        }
}

那么在函数式编程语言里,快排是否是就是这样实现的?答案是No。函数式编程为何不能用循环?就是由于没有变量,因此在函数式编程语言里不能进行原地排序的。

(define (qsort s)
  (cond ((null? s) s)
        ((null? (cdr s)) s)
        (else
         (let ((h (car s))
               (left (filter (lambda (x) (<= x (car s))) (cdr s)))
               (right (filter (lambda (x) (> x (car s))) (cdr s))))
           (append (qsort left) (list h) (qsort right))))))

咱们把这段代码翻译成Python(翻译成C或者C++挺啰嗦的)上面这段代码是用Lisp的方言Scheme实现的,不是尾递归的。

def qsort_lisp(A):
    if len(A) == 0 or len(A) == 1:
        return A
    left = []
    right = []
    pivot = A[0]
    for i in range(1,len(A)):
        if A[i]             left.append(A[i]);
        else:
            right.append(A[i]);
    return qsort_lisp(left) + [pivot] + qsort_lisp(right)
x = [3,4,5,6,2,34,6,2,2,5,7,2,7]
print qsort_lisp(x)

其实刚才我说谎了,大部分函数式编程语言,例如Scheme,Erlang,Clojure等都提供可变的变量,数据库里有上G的数据,不能把它拷贝一份在写回去,这时候就须要使用真正的变量了。函数式编程语言都是比较高级的语言,排序时通常使用自带的sort函数就好了。上面这段代码没有对变量作修改的操做,因此能够看作是函数式编程。这个函数能改为尾递归吗?应该是能够的,可是挺麻烦的,我是没找到好办法。到网上找了找也没找到好的方法。

总结一下尾递归:(1)计算过程是迭代的(2)在函数最后一步调用本身,并且是仅有调用语句,或者是一句fun(),或者是return fun(),不存在x = fun()这样的状况(3)函数执行最后一句调用本身的语句时,将状态变量以参数形式传递给下一次调用,本身的栈没用了,形象的说,它告诉下一次被调用的函数,我已经死了,你干完活后直接向个人上级报告就好了,不须要和我说了(4)gcc开启优化选项后能够对尾递归进行优化,大部分函数式编程语言会对尾递归进行优化

相关文章
相关标签/搜索