递归还是不递归,That‘s a problem!

今天我班的大神心血来潮,居然找了本算法的书看起来了,里面有很多经典的例子,大神在编程方面是一张白纸,所以看到讲解递归的部分时就傻了眼,比如书上讲解求Fibonacci数列时就用的是递归算法,相当的简洁明了,代码如下:

unsigned long fibonacci (int n)
{
if (n <= 2)
{
return 1;
}
else
{
return fibonacci(n-1) + fibonacci(n-2);
}
}

我对于递归的思想很是赞同的,有化繁为简,分而治之的想法在里面,就好像很多编程思想一样,如:动态规划等等。但我认为大部分的递归想法仅仅是在人的接受能力上进行了化简,这是后话。其实递归算法是很好理解的,只是大神还不习惯罢了。如上的程序是运用数列的递推公式非常明白的写出来的。但就如我所言,这个递归算法其实浪费了很多CPU资源,我们可以仔细分析一下,比如:

f(5) = f(4) + f(3)
= f(3) + f(2) + f(2) + f(1)
= f(2) + f(1) + f(2) + f(2) + f(1)
= 1 + 1 + 1 + 1 + 1

这里计算f(5)就用到了12个加法,其实我们可以发现我们把f(3)算出来后f(4)其实不用像我这样拆开来算的,直接用f(3)的结果就行了,换句话说即充分利用所求项等于前两项的和,而这两项之间又有关系,不难想到,可以写出一个不使用递归的算法如下:

unsigned long fibonacci (int n)
{
unsigned long f0, f1, temp;
int i;
if (n <= 2)
{
return 1;
}
else
{
for (f0 = f1 = 1, i = 3; i <= n; i++)
{
temp = f0 + f1;
f0 = f1;
f1 = temp;
}
return f1;
}
}

这个算法用一个变量temp保存两个前项之和,然后及时更新f0和f1,最后那个return返回temp也是可以的,因为我的temp保存的内容和f1一样。可以再看一下我们计算f(5)时做了多少个加法,仔细一数是仅仅只用了三个加法,大大减少了运算开销,而其上面那个递归程序因为在不停的调用函数,所以还会消耗一定的函数调用栈。为什么大牛们都说除非是实在太繁杂,否则别用递归,原因可见一二。我把我的想法向班上的大牛说了下,他马上告诉我一个在一本书上的更快的算法,这着实让我们班一群小草很是震惊,他说是这么想的,由我上面那个非递归的算法可以推出在算f(n)时最多不会超过n-2个加法,要加快程序只能把加法继续减少,这里才是思维的精髓,事实上一般减少加法的方法就是使用乘法,不过后来我想到乘法器比加法器要复杂,所以能够提高的效率我们并不可知,不过这种思想确实不一般。曾经在线性代数课上老师也介绍过一个伪Fibonacci矩阵,即一个2*2的矩阵,第一行元素为1,1,第二行为1,0。我们发现f(n)的值就是这个矩阵n-2次方后在第一行上的两个元素的和,如f(5)就等于把这个矩阵3次方后,此时第一行的两个元素分别为3和2,所以f(5) = 3 + 2。这个要推也很简单,算一两个就发现这个规律了,依据这个思想,他写下了如下的代码:

unsigned long fibonacci (int n)
{
unsigned long a, b, c, d;
int i;
if (n <= 2)
{
return 1;
}
else
{
matrix_power(1,1,1,1,n-2,&a,&b,&c,&d);
return f1;
}
}
//计算矩阵的n次方
matrix_power(unsigned long a, unsigned long b, unsigned long c, unsigned long d, int n,
unsigned long *aa, unsigned long *bb, unsigned long *cc, unsigned long *dd)
{
unsigned long xa, xb, xc, xd;
if (1 == n)
{
*aa = a, *bb = b, *cc = c, *dd = d;
}
else if (n & 0x01 == 1)
{
matrix_power(a, b, c, d, n-1, &xa, &xb, *xc, *xd);
*aa = a * xa + b * xc;
*bb = a * xb + b * xd;
*cc = c * xa + d * xc;
*dd = c * xb + d * xd;
}
else
{
matrix_power(a, b, c, d, n>>1, &xa, &xb, &xc, &xd);
*aa = xa * xa + xb * xc;
*bb = xa * xb + xb * xd;
*cc = xc * xa + xd * xc;
*dd = xc * xb + xd * xd;
}
}

我们还可以发现这段代码在求矩阵的n次方时用了一个分治的思想,判断n的奇偶,如果是偶数就递归求出n/2次方再平方,若是奇数就写成M*M的偶数次方,这里居然又用了递归,倒!原本是不想递归才绕了这么一大圈子,现在又回到原点了,实在不好意思去求大牛了, 只好自己动手丰衣足食,不就是求一个矩阵的n次方吗?看我也不用递归。矩阵弄在一起太复杂,我就假设是求一个数m的n次方吧!如果是按递归算法求m的n次方,然后把每次递归调用时传入的n值列出来我发现一个惊人的事实,那就是每次调用时n均为2的倍数,这时我似乎看到了些什么,于是我仔细研究了下,加上大胆的猜想,终于让我发现了一个规律,在求m的n次方时可以把n写成二进制数,找到二进制位为1的位,然后可以将n写成2的这些位所对应的权值之和,于是m的n次方就能写成m的这些权值次方的和了,比如求m的9次方,将9写成二进制数为1001,所以9 = 2的0次方 + 2的3次方,令a = 2的0次方,b = 2的3次方,则m的9次方 = m的a次方 + m的b次方。这样一来我只需要遍历n的二进制位就行了,于是我迅速修改了上面求矩阵的n次方的代码:

//改变出参的值
getTemp(unsigned long **aa, unsigned long **bb, unsigned long **cc, unsigned long **dd,
unsigned long a, unsigned long b, unsigned long c, unsigned long d)
{
unsigned long tempa = **aa, tempb = **bb, tempc = **cc, tempd = **dd;
tempa = **aa * a + (**bb) * c;
tempb = **aa * b + (**bb) * d;
tempc = **cc * a + (**dd) * c;
tempd = **cc * b + (**dd) * d;
**aa = tempa;
**bb = tempb;
**cc = tempc;
**dd = tempd;
}

//计算矩阵的n次方修正版
matrix_power(unsigned long a, unsigned long b, unsigned long c, unsigned long d, int n,
unsigned long *aa, unsigned long *bb, unsigned long *cc, unsigned long *dd)
{
unsigned long xa = a, xb = b, xc = c, xd = d;
unsigned long tempa = a, tempb = b, tempc = c, tempd = d;
int flageven = 0, flag = 1;
if (!(n & 0x01UL))
{
flageven = 1;
}
while (n > 0)
{
if (1 == (n & 0x01UL))
{
if (1 == flag)
{
*aa = a;
*bb = b;
*cc = c;
*dd = d;
flag = 0;
if (1 == flageven)
{
getTemp(&aa, &bb, &cc, &dd, xa, xb, xc, xd);
}
}
else
{
getTemp(&aa, &bb, &cc, &dd, xa, xb, xc, xd);
}
}
tempa = xa * a + xb * c;
tempb = xa * b + xb * d;
tempc = xc * a + xd * c;
tempd = xc * b + xd * d;
xa = tempa;
xb = tempb;
xc = tempc;
xd = tempd;
n >>= 1;
}
}

忙活了半个晚上终于结束了这场和递归的战斗,至此这个Fibonacci数列总算被阶段性拿下,一个我认为可以接受的非递归算法出炉。关于递归还有很多可以讨论一下,不过现在已经早上了,我还是先去休息一下,过段时间再来和大家分享一下自己的递归心得。同时也希望大家对我上面的算法有什么建议或是置疑都可以提出来,我们共同探讨,互联网最方便的应该是交流了,我期待您的关注。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值