递归

254 阅读4分钟

最近在leetcode上刷题,遇到binary tree 问题,基本上都可以用递归解决,用过递归很多次,在这里只谈谈一些个人感受,基础知识可以阅读其他文档。

问题:给定一棵二叉树root,求该树上所有节点的和。节点数据结构如下:

    Node {
        val: xx,
        left: Node,
        right: Node
    }

如:

   1
 /   \
2     3
  
Sum:1 + 2 + 3 = 6
   1
 /   \
2     3
 \   /  \
  5 8    12
 /         \ 
-10         8

Sum:1 + 2 + 3 + 5 + 8 + 12 + (-10) + 8 =  29

思考过程:遍历所有节点,将其值加起来。

那么如何遍历呢?

1、从根部开始,左、右,然后左边的左右,右边的左右,然后左边的左边的左右,左边的右边的左右...(前两步很简单啊,后面脑阔疼!)

2、好像一直在左、右、左、右(自行脑补电影《青蛇》里两姐妹扭臀部的片段)

我们来理一理,如果只是下面这种情况,就很简单 root.val+left.val+right.val

   1
 /   \
2     3

那如果是如下的情况,可以引入递归的思想:root.val + 左子树的和 + 右子树的和

   1
 /   \
2     3
 \   /  \
  5 8    12
 /         \ 
-10         8

function getBTSum(root) {
    if (!root) return 0;
    return root.val + getBTSum(root.left) + getBTSum(root.right);
}

那是如何知道要用递归做呢? 这就需要一些经验,或者是自发探索出来这种方法(也许你不知道递归是什么,但是你已经在用了。)

一、定义

递归(Recursion)又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。

二、使用递归

1、何时使用递归

举个例子,老板要个材料,让总监去做,总监让部门经理去做,部门经理让你去做,你做完了给部门经理,部门经理给总监,总监给老板。

语言描述为:让直属下属去做

具有以下特征

  • 语言描述的时候,有大量的重复,或者可以抽象为一句话总结。比如: “加完当前节点加左边加右边,再加左边的左边,左边的右边”或“x的阶乘等于x(x>=1)乘以(x-1)的阶乘”

  • 看似很简单,但是脑阔却越来越疼,感觉自己掉进了一个螺旋下落的深坑。

  • 当刷过大量的题目后,当读完一个新题后,在没有思路前,大脑会冒出“用递归”的时候

2、确定使用递归后的注意事项

  • 考虑边界条件/停止条件,最简单的情况。

(要有停止条件,否则会陷入死循环的。比如老板要材料的事情一直在往下传递,但是没人做,老板一直收不到材料可能会会影响整个公司运转,最终公司倒闭了,你失业了。)。

  • 递归部分具有普遍性,抽象性。

比如树上的任意一个节点作为树根,求该树所有节点和的描述root.val + 左子树的和 + 右子树的和都成立。

三、尾递归

递归很耗内存,因为需要同时保存很多个调用帧(老板-总监-部门经理-你,这个线不能断了啊)。

ES6中引入了尾调用优化(Tail Call Optimization, TCO) exploringjs.com/es6/ch_tail…

尾调用(Tail call)

尾调用,在计算机学里,尾调用是指一个函数里的最后一个动作是返回一个函数的调用结果的情形,即最后一步新调用的返回值直接被当前函数的返回结果。此时,该尾部调用位置被称为尾位置。尾调用中有一种重要而特殊的情形叫做尾递归。

意义:节省内存

“调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧。”

“如果支持TCO的引擎能够意识到调用在尾部,不需要创建新的栈帧,而是可以重用已有的栈帧,这不仅速度更快,也更节省内存。”

摘自《你不知道的Javascript中卷》

看过很多资料视频,也用过很多次,在此过程中拓宽了知识面,也能搞定大部分相关问题。但是仍然感觉某块还有个小秘密我不知道,以致于无法说出最本质的那个点。实践出真知。后续再更新。