在解决约瑟夫问题时,我比较推荐使用递归,因为递归实现的算法代码更短,逻辑也更清晰,然而很多人有一个疑问,那就是他们知道递归层数是有极限的,这就意味着当需要很大层数的递归时,递归算法是不可行的,会导致段错误。
对于这个问题我有三个回答:第一,只要你使用的是计算机而不是你的大脑,你就不要指望什么是无限制的,计算机不是神,计算机里别谈无限。第二,虽然你的c代码是递归实现的,但是编译器生成的二进制文件却不一定会递归调用一个函数,这就涉及了编译器的优化。第三,因为我们使用的大多数是x86体系的冯诺依曼机器,这种机器的编译器一般对于函数调用是通过栈实现的,而我们知道栈的预备空间和最大扩展空间一般都不会太大,然而如果有一种机器,根本就没有栈的概念,或者说即使在x86机器上实现一个变态的编译器,将函数调用退化成简单的jump而不使用栈操作,那么栈的限制将不再存在。
在基于栈实现函数调用的机器上,我们看一下在什么情况下必须层层嵌套用递归实现。其中一个条件,那就是在递归的过程中,我们无法获知任何计算的中间结果,必须等到最深入一层的函数满足非递归条件而返回时,整个过程才会一层一层的返回,但是如果每一步的中间结果都被保存或者说每一步的递归过程并不是要计算什么值,而仅仅是为了实现一个副作用,那么最终的二进制文件大可不必非要用递归。而且我们知道,递归和迭代是等价的,这一点非常重要。
编译器不是傻子-其实是实现编译器的人很聪明,编译器一定不会傻到非要用递归生成最终的二进制执行文件,以gcc为例,即使你在c函数中明确使用了递归,在-O3的优化编译选项下,编译器还是会尝试将递归化为迭代的,所生成的二进制objdump文件中你将看不出任何递归调用,这种工作并不是你自己实现的,而是编译器帮你实现的。
有一种递归被叫做“尾递归”,对于这种递归的理解其实很简单,那就是每一步递归调用的中间结果被保存,因此你可以只使用一个栈帧实现所有的递归过程,因为中间结果随着递归的深入而持续改变,因此你可以将其理解为一种隐含的迭代。对于约瑟夫问题,这种尾递归是很显然的,因为每一步的递归过程的效果其实都是一种副作用,那就是在链表上删除一个节点。但是你还是要告诉编译器这一切,否则编译器仍然使用递归过程生成二进制代码。
最终的结果,你用-O3选项编译这个c文件,使用objdump查看其目标码,你将看不到任何递归过程,这就突破了栈的限制,但是记住,限制仍然是有的,因为很抱歉,你用的是计算机这种奴隶式的工具,计算机说的最多的话,那就是:Sorry, it is beyond my ablity!