递归

本文探讨了递归与非递归的概念及其在编程中的应用。通过对比斐波那契数列的不同实现方式,展示了递归与非递归方法的时间复杂度差异,并通过实例解释了递归输出数组元素时的注意事项。

递归

若一个对象部分的包含自己,或用他自己给自己定义,被称为递归。
简而言之,就是自己调用自己。

问大家一个问题,你认为可以无限递归吗?

首先,递归调用会进行入栈和栈帧回退,消耗栈空间;其次,我们栈空间有限,Win下的VS2008编译器默认堆栈分配大小是1M,当栈空间不足时,会产生栈溢出错误。
最后,它不同于死循环,比如while(1),会一直占用CPU,直至操作系统时间片用完,会由执行状态转换到就绪态。


接下来先看一段代码

struct Node
{
    int m;
    Node node;
    //Node* node1;
};

int main()
{
    Node n;
    return 0;
}

在你看来,这段代码可以运行吗?
答案是否定的,Node node;这行代码会造成程序无限递归下去,编译都无法通过。
编译器会提示“不允许使用不完整的类型”,从根本上杜绝了你的错误,因为我们在设计时,类中不能包含自己的实体。一般都会用注释那段代码,Node* node1。

斐波拉契数列在c语言中算是比较经典的,有递归和非递归实现方法,
斐波拉契数列,例1, 1, 2, 3, 5, 8, 13, 21…

 //递归实现
int Fac(int n)
{
    if(n == 1||n == 2)
        return 1;
    else
        return Fac(n-1)+Fac(n-2);
}

这里写图片描述

这里写图片描述
递归下来,经测试,n=10时,Fac(10) = 55,需要函数调用109次;
n=20时,Fac(20) = 6765,需要函数调用13529次。

 //非递归
int Fac1(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    if(n <= 2)
        return a;
    for(int i = 3;i <= n;++i)
    {
        a = b + c;
        b = c;
        c = a;
    }
    return a;
}

非递归下来,相当于迭代,时间复杂度是O(n),效率高。
两种方法对比一看,递归实现效率太低,很多步重复计算,n越大,重复计算量呈指数增长,所以一般斐波拉契数列不建议使用这种递归方法。
这里写图片描述

如果我们这里把非递归实现中的循环改为递归,情况会怎么样呢?

//循环改为递归
int fac(int a,int b,int n)
{
    if(n<=2)
        return a;
    else
        return fac(a+b,a,n-1);
}

int fun(int n)
{
    int a=1,b=1;
    return fac(a,b,n);
}

这里写图片描述
测试数据后,效率和非递归实现相同,时间复杂度是O(n)。


还有一个问题是,我们一般输出数组元素,简单地循环输出就可以,如果改成递归输出,会是怎么样呢?

//n是数组br长度
void Print(int *br,int n)
{
    if(n > 0)
    {
        cout<<br[n-1]<<" ";
        Print_Array(br,n-1);
    }
}//34,23,12

void Print_Array1(int *br,int n)
{
    if(br == NULL || n < 1)//需进行参数检查
        return;
    Print(br,n);
}

int main()
{
    int arr[]={12,23,34};
    int n = sizeof(arr)/sizeof(arr[0]);

    Print_Array(arr,n);

    return 0;
}

这里写图片描述
这样输出后,数组元素是逆序的,递归调用是先输出数组元素,然后函数压入栈,递归条件结束,最后栈回退。我们这里可以将输出和函数调用顺序替换,这样就会顺序输出数组元素。

void Print1(int *br,int n)
{
    if(n > 0)
    {
        Print1(br,n-1);
        cout<<br[n-1]<<" ";
    }
}

将上边这段代码稍加修改,n-1处改为–n,结果会是什么?

void Print2(int *br,int n)
{
    if(n > 0)
    {
        Print2(br,--n);
        cout<<br[n-1]<<" ";
    }
}
看到--n,不由地就想到了前置操作及特性,递归进行到结束条件,输出br[n-1]会造成数组越界,输出br[-1],是个随机数,接着是数组前两个元素。

这里写图片描述

再加修改,把–n改成n–,又会怎么样呢?相信这会的已经被各种–整懵了,那就猜一下结果。

void Print3(int *br,int n)
{
    if(n > 0)
    {
        Print3(br,n--);
        cout<<br[n-1]<<" ";
    }
}
编译器在处理后置时:是将值放入栈中,在输出时直接出栈就行;
     在处理前置时:是等运算完成后,直接从n的地址中取值。

这段代码运行会奔溃,出现栈溢出错误。后置–会在函数调用完后执行–,n一直等于3,会无限递归下去,直至栈空间不足,出现栈溢出,程序奔溃。

引用是经常会使用到的,这里对代码再加修改,参数列表中进行引用n。

void Print4(int *br,int &n)
{
    if(n > 0)
    {
        Print4(br,--n);
        cout<<br[n]<<" ";
    }
}
结果是12 12 12,先是会进行--操作,然后递归调用,递归条件结束时,n=0,这里是引用n,是n的地址,输出br[n]会连续输出br[0]。

如果这里是后置–,程序会编译无法通过。因为内置类型产生的临时量放在寄存器中,具有常性,所以是不能改变的,不能使用普通的引用。

(只是平时学习的一些总结,如果有什么错误或者问题,大佬们可以指点留言,互相交流学习嘛,谢谢支持!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值