为什么要写这篇
前段时间看到一篇公众号文章:写给小白看的递归(硬核),讲解的是递归,认为是功能逻辑落实到代码实现上的一种基本知识,就像C语言的指针一样,缺少这部分认识,实现上捉襟见肘。回过头来翻收藏夹时重点看了一下,颇有感触,想记录下来。
初问:递归是什么
递归,就是函数自己调用自己。 只是听到这句话的话,应该会不明所以,有什么用?
我的初步理解
变量不传值而改为传地址,就分化出了指针的概念。
函数不调用别的函数而改为调用自己,就分化出了递归的概念。
换种熟悉的问法
你觉得指针有什么用?我的理解它就是指向物理地址的一个变量。
什么情况下才有的指针
由于程序正常工作时会从外部采集数据(变量),或者接收到数据(变量),以及函数为实现功能会设计几个辅助变量,函数与函数之间的对接交互也是变量,变量太多太零散不好管理(命名就是个大麻烦,零散的全局变量对于理解代码也是个大麻烦)变量能少用就少用,就有了指针。
指针能干什么
通过传地址代替传值 减少了不少中间变量(数组属于指针的应用,指针却不是数组,不知道注意到没有:C语言函数返回值可以是指针,但不能是数组)。
不仅如此,也能灵活地运用在函数上(好像也带来了理解上的困难),
还有就是有了链表这种数据结构,进而可以实现抽象数据类型 堆栈,队列,二叉树等,
还有的 就是用其实现面向对象的思想,封装继承和多态等,进而可以理解设计模式。
指针在工程师心中的地位
有人将其认为是C语言的精髓,大概是觉得一个挺大的知识面最后就落实到代码实现上最核心的就是这么一个概念吧,其它的词法,语法,语义,关键字,数据类型,条件判断,逻辑跳转以及循环语句等等都太固定写在了书里,指针的概念和本质也可以写在书里,但灵活的运用却在实践中。当然太过灵活也带来了理解和掌握上的困难。
顺便说一下:变量太零散就有了作用域、数据结构等,前者限制变量的活动空间,避免零散带来更大的影响,后者可以将变量按照某种用途组织起来。
再问:递归是什么
递归就像是重复操作,跟循环类似。循环不是自己调用自己,也就没有递归调用栈。逆序打印这种就适合用递归,而阶乘这种简单的也可以用循环来替代。
在《算法图解》中讲解递归时,提到了两者的区别:盒子堆里有一个盒子里放着钥匙,循环方式查找的话会创建一个待查找的盒子堆,因此你始终知道还有哪些未查找。而递归不需要创建就可以查找到,而且还因此可以记录钥匙的存放路径。
怎么做到的
这就需要了解递归 与 Call Stack之间的关系(Call Stack 叫作调用栈 或 函数调用栈,是一种追踪函数执行流的机制,简单说就是保存调用的返回地址,函数嵌套时保存上下文环境用于返回)
那文章中对于递归函数的执行流程有一定介绍:逆序输出的实例 ,同时也说明了递归 与 Call Stack之间的关系。
能干什么
快速排序听说过没,它被评为二十世纪最伟大的十大算法之一。虽然C标准库中的看不到源码,但在提到快速排序实现的书里,都是用到了递归来实现,
不止快速排序算法,还有很多其它算法也用到了递归,以及数据结构的遍历搜索。
有这么一道题:链表的逆序打印,而考察最关键的两个知识点:组成链表所需的指针,链表逆序所需的递归调用栈。 假如说是字符串的逆序打印,字符串的长度应该是可以通过strlen函数得出的,然后根据数组下标的方式由高到低访问即可。(可以说是一种实现方式,可能巧妙避过了出题人的意图)
如果你见过二叉树的前序,中序,后序遍历,图的搜索,就知道类似指针那样贯穿始终了。
我看到有这么一句话:很多复杂问题,很难找到相应的递归式。实际上,一旦找到递归式,那问题就解决了99%
怎么用
一开始就像理解指针一样难理解递归,看上去简单,但若要真不理解就怎么看都觉得简单,但自己就是不会用。
根据提供的几个例子去品味 初始条件,结束条件以及上下文的变化关系,去理解自己调用自己是怎么做到的(递归与调用栈),去站在问题的角度去考虑使用这种方式的妙处。
学到了什么
很多算法上都使用了递归,因此理解这种概念对于理解那些用到递归的算法有帮助。
递归的初始条件就是输入,终止条件就是输出,不能只有输入没有输出(无限循环)。如何实现想要的功能就是上下文的变化关系。
递归只是让解决方案更清晰,并没有性能上的优势,而且滥用会导致无限循环。
入没有输出(无限循环)。如何实现想要的功能就是上下文的变化关系。
递归只是让解决方案更清晰,并没有性能上的优势,而且滥用会导致无限循环。
递归拆分成两个及以上子问题效率就不太行了,因为有了重复计算,需要使用动态规划来优化它。(一种表格处理法,把每一步得到的子问题结果存储在表格里,每次遇到该子问题时查表即可)