问题描述
上一篇为大家介绍了利用递归算法求解1到100求和问题,其基本思想为将100依次递减到0为止,然后将每一次递减的整数累加到求和变量sum上,进而完成求和。
在每一次整数递减时,使用的代码是foo(n-1),学会Java或C之类的编程语言的同学都会了解,自减一的操作除了n-1外,还可以是n--和--n,那么我们将提出如下问题:
foo(n-1)
foo(n--)
foo(--n)
三者是完全一样的效果吗?还是有所不同?这三种也是大家在编写递归算法时比较容易混淆和出错的地方,而且很多时候出错了也很难短时间内找到原因。
问题分析
首先编写代码观察三种实验的结果,
foo(n-1)就是上一篇的结果5050,答案是正确的。
foo(n--)出现栈溢出错误,表示递归并未终止。
foo(--n)的运行结果是4950,与正确的答案相差100,貌似少加了整数100。
为什么--n和n--二者的结果会相差如此之大?
(1)n-- 错误原因分析
n-- 表达式的执行顺序为先赋值然后再自减。因此foo(n--)等价于
foo(n);
n--;
递归函数虽然有比较明确的递归终止条件代码,但是由于每一次递归调用自身的函数foo(n)的形参整数n,并没有做到每次都减1,因而整数n并没有实现每次递减的目的,所以最终还是陷入了死循环状态。
至此,可以总结递归算法陷入死循环状态的原因有二:
其一是递归函数没有终止条件。
其二是递归函数的形参没有实现实质性的递减。
以上这两点原因是非常重要的,今后在自己编写递归算法出现栈溢出时,就可以从这两方面查找出错原因。
(2)--n 错误原因分析
foo(--n)等价于:
n = n - 1;
foo(n);
接下来重点来看,第一次调用即n=100时执行情况:
n = 100 -1 = 99;
foo(99);
而foo(n-1)的情况是:
n = 100;
foo(100-1); // 即foo(99);
不知大家是否能够发现这两者细微的差别。foo(n-1)中n并没有发生变化,变化的是foo函数的形参,而foo(--n)是变量n的值发生了变化引起了形参的变化。
可以看到foo(--n)得出其实n并没有从100开始,而是从99开始递减的,故而得出的和是1+2+···+99=4950;因此只需要将初始的值从101开始即可,代码如下:
结语
本文在上一篇的基础上进一步的探讨了递归算法设计在形参递减时非常容易出错、易混淆的三个地方,而一旦在算法设计中遇到了这些错误如果没有前期的积累则非常难以发现。另外我们也给出了递归算法出现栈溢出的两种情形,对于排除错误非常重要。
另外,在设计自增或自减的算法时,尽量不要图方便而使用foo(n--)和foo(--n)这样的写法,这两种写法在出现问题,比较难以排查错误,建议统一写成foo(n-1)的方式。