-
Notifications
You must be signed in to change notification settings - Fork 0
JavaScript基础篇-递归 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Labels
Comments
我觉得 Tail calls 可以再开一篇 |
@joe223 我在补充下,写得有点简单 |
@Expelliarmus923 期待续集 |
给作者一个大大的赞 😀 ~ |
@mqyqingfeng 谢谢大佬! |
作者大大,几个图片都无法浏览了 |
@qianzhangsheng 图片备份丢了,我还在想办法 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
JavaScript基础篇-递归
递归或者叫递归算法是编程里面十分重要的概念,解决很多实际工作中很多编程问题。
什么是递归
什么是递归,搜索一波,发现Google非常皮。
简单的说,递归就是在方法里面直接或者间接调用自己,当然不能无限制地去调用,递归需要有出口,否则会形成死循环,导致内存溢出。
递归的原理
先来看下阶乘,阶乘是递归的一个经典例子。相信大家高中都学过阶乘,比如6! = 1 * 2 * 3 * 4 * 5 * 6,用程序描述这个问题,实现一个factorial函数,输入n,输出n*(n-1)…21。仔细观察下,factorial(6)是 6 * factorial(5),6 * 5 * factorial(4)… 6 * 5 * 4 * 3 * 2 * factorial(1)即6 * 5 * 4 * 3 * 2 * 1
上面代码的意思是当n不等于1的时候循环调用自己和传入的n相乘,n等于1,则返回1。
简单说下递归的原理,我们知道在JS中函数调用是一个叫的调用栈(Call Stack)实现的。比如传入n为5的时候,第一次执行factorial,当执行到factorial(n-1),factorial会被压到栈中。递归方法的出入栈和其他普通方法唯一的区别是前者调用了自己。我们把每次调用的factorial已调用顺序命名下。当运行到n===1的时候,栈里依次存放则factorial1, factorial2, factorial3, factorial4, factorial5。
当factorial5被执行的时候,n是1,返回1,推出栈。当factorial4被调用的时候,n是2,返回2 * 1。接下去factorial3, factorial2依次被调用出栈,返回3 * 2和4 * 6,最后一个factorial1的时候,计算5 * 24,结束运行。
斐波那契数列
斐波那契数列是递归另一个经典的例子,这是个叫斐波那契的意大利人在数兔子的时候观察归纳出的一个数列。
即如果n是a,n+1是b,那么n+2一定是a+b。比如运行Fibonacci(6),返回斐波那契数列第6个元素8。
递归使用技巧
根据上面的例子,总结下:
下面介绍几个实际应用场景。
递归应用
树状菜单
树状菜单是比较常见的组件,一般后端数据都是二维扁平存储的,前端拿到数据后需要解析展开一下
把上面的数组,展开成下面的格式。根据parentId放入对应id对象的children节点上。
我们先寻找一个基本的操作,叫find(), 这个函数用来遍历arr,如果发现数组里面的对象el的parentId等于当前对象的id,则把这个值放到当前对象result的children数组里面,这个时候,el也可能是别人的parent,所以我们需要继续寻找数组里面是否有parentId等于el的id,继续find下去。
这个递归函数的隐含的边界条件其实是el.parentId === result.id,如果相等则继续递归下去,一直到找不到相等为止。
优化下find函数,每次都遍历整个数组,会显得很浪费,把已经符合条件的对象从当前数组删除。数组为空则直接返回。
比较两个JS对象是否相等
首先我们来重新定义下相等。
前面两种情况可以是用ES6新增的Object.is处理,第三种情况需要遍历数组/对象的每个key对应的value是否相等。为了方便对比,我们抽象出两个函数,eq和deepEq。eq主要判断原始类型的相等,deepEq则是遍历引用对象进行对比。这里的递归并未直接调用原函数,而是间接调用,eq调用deepEq,deepEq又调用eq。
对于deepEq,我们需要做一下判断
代码还引用了一个Type.js,用来做类型判断。具体代码会贴在这里方便大家查看。
这里只是对递归思想进行解释,更多实现细节可以参考这篇JavaScript专题之如何判断两个对象相等 · Issue #41 · mqyqingfeng/Blog · GitHub
对象深拷贝
深拷贝具体细节可以参考我写得这篇浅析JS的拷贝。这里只对递归做解释。
深拷贝是值对js的基本类型做复制操作(=),遇到引用类型,需要继续递归下去。
1. 深拷贝的基本操作是复制原始类型(String,Number,Undefined,Null,Boolean,Symbol)。
2. 边界条件是值是否是引用类型,如果是,则继续递归下去。
尾调用优化
递归虽然能用比较简洁的方式解决问题,但递归的运行效率比较低,原因在用递归同时调用栈保存了成百上千个调用帧,容易发生堆栈溢出的错误。
上文提到的阶乘函数。通过debugger发现调用到n为1的时候,Call Stack[调用栈]会有5个factorial函数调用帧。
使用尾调用可以解决这个问题的,尾调用就是指某个函数的最后一步是调用另一个函数。
下面这三种情况都不属于尾调用。
当然尾调用不一样要在函数的尾部,只要是最后一步操作即可(return)。
改写下之前的factorial函数,
尾递归只在严格模式生效,class module也会生效,因为class module内部默认是严格模式。
尾部调用是函数最后一步操作,而不需要保留外部函数的调用帧,因此只存在一个调用帧。
在实际chrome上跑代码的时候,发现并没有生效,因为支持度非常感人,一片飘红。各大浏览器(除了safari)并未部署尾调用优化。
原因是在尾调用的实现中,许多调用帧被抛弃,导致很难在调试栈中调试代码。
T39又提出一种语义上的尾调用方案Syntactic Tail Calls。用明确的语义来标识需要尾调用优化的场景。比如
小结
递归算法的内容远远不止这些,如二叉树查找,汉诺塔问题,能力有限,只能暂时介绍到这里了。
参考
http://imweb.io/topic/5a244260a192c3b460fce275
http://imweb.io/topic/584d33049be501ba17b10aaf
mqyqingfeng/Blog#41
https://chenqx.github.io/2014/09/29/Algorithm-Recursive-Programming/
https://zhuanlan.zhihu.com/p/25644447
https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/
https://juejin.im/post/5a35e7c26fb9a0452341f70c#heading-6
The text was updated successfully, but these errors were encountered: