最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 精读《算法 - 二叉树》

    正文概述 掘金(黄子毅)   2021-07-05   540

    二叉树是一种数据结构,并且拥有种类复杂的分支,本文作为入门篇,只介绍一些基本二叉树的题型,像二叉搜索树等等不在此篇介绍。

    二叉树其实是链表的升级版,即链表同时拥有两个 Next 指针,就变成了二叉树。

    二叉树可以根据一些特性,比如搜索二叉树,将查找的时间复杂度降低为 logn,而且堆这种数据结构,也是一种特殊的二叉树,可以以 O(1) 的时间复杂度查找最大值或者最小值。所以二叉树的变种很多,都可以很好的解决具体场景的问题。

    精读

    要入门二叉树,就必须理解二叉树的三种遍历策略,分别是:前序遍历、中序遍历、后序遍历,这些都属于深度优先遍历。

    所谓前中后,就是访问节点值在什么时机,其余时机按先左后右访问子节点。比如前序遍历,就是先访问值,再访问左右;后续遍历就是先访问左右,再访问值;中序遍历就是左,值,右。

    用递归方式遍历树非常简单:

    function visitTree(node: TreeNode) {
      // 三选一:前序遍历
      // console.log(node.val)
      visitTree(node.left)
      // 三选一:中序遍历
      // console.log(node.val)
      visitTree(node.right)
      // 三选一:后序遍历
      // console.log(node.val)
    }
    

    当然题目需要我们巧妙利用二叉树三种遍历的特性来解题,比如重建二叉树。

    重建二叉树

    重建二叉树是一道中等题,题目如下:

    先给你二叉树前序与中序遍历结果,让你重建二叉树,这种逆向思维的题目就难了不少。

    仔细观察遍历特性可以看出,我们也许能推测出一些关键节点的位置,再通过数组切割递归一下就能解题。

    前序遍历第一个访问的一定是根节点,因此 3 一定是根节点,然后我们在中序遍历找到 3,这样 左边就是所有左子树的中序遍历结果,右边就是所有右子树的中序遍历结果,我们只要再找到 左子树的前序遍历结果与右子树的前序遍历结果,就可以递归了,终止条件是左或右子树只有一个值,那样就代表叶子节点。

    那么怎么找左右子树的前序遍历呢?上面例子中,我们找到了 3 的左右子树的中序遍历结果,由于前序遍历优先访问左子树,因此我们数一下中序遍历中,3 左边的数量,只有一个 9,那么我们从前序遍历的 3,9,20,15,73 之后推一位,那么 9 就是左子树前序遍历结果,9 后面的 20,15,7 就是柚子树的前序遍历结果。

    最后只要递归一下就能解题了,我们将输入不断拆解为左右子树的的输入,直到达到终止条件。

    解决此题的关键是,不仅要直到如何写前中后序遍历,还要知道前序遍历第一个节点是根节点,后序遍历最后一个节点是根节点,中序遍历以根节点为中心,左右分别是其左右子树,这几个重要延伸特征。

    说完了反向,我们说正向,即递归一颗二叉树。

    其实二叉树除了递归,还有一种常见的遍历方法是利用栈进行广度优先遍历,典型题目有从上到下打印二叉树。

    从上到下打印二叉树

    从上到下打印二叉树是一道简单题,题目如下:

    这道题要求从左到右顺序打印,完全遵循广度优先遍历,我们可以在二叉树递归时,先不要急着读取值,而是按照左、中、右,遇到左右子树节点,就推入栈的末尾,利用 while 语句不断循环,直到栈空为止。

    利用展开时追加到栈尾,并不断循环处理栈元素的方式非常优雅,而且符合栈的特性。

    当然如果题目要求倒序打印,你就可以以 右、中、左 的顺序进行处理。

    接下来看看深度优先遍历,典型题目是二叉树的深度。

    二叉树的深度

    二叉树的深度是一道简单题,题目如下:

    由于二叉树有多种分支,在遍历前,我们并不知道哪条路线是最深的,所以必须利用递归尝试。

    我们可以转换一下思路,用函数式语义方式来理解。假设我们有了这样一个函数 deep 来求二叉树深度,那么这个函数内容是什么呢?二叉树只可能存在左右子树,所以 deep 必然是左右子树的最大深度的最大值 +1(它自己)。

    而求左右子树深度可以复用 deep 函数形成递归,我们只需要考虑边界情况,即访问节点不存在时,返回深度 0 即可,因此代码如下:

    function deep(node: TreeNode) {
      if (!node) return 0
      return Math.max(deep(node.left), deep(node.right)) + 1
    }
    

    从这可以看出,二叉树一般能用比较优雅的递归函数解决,如果你的解题思路不包含递归,往往就不是最优雅的解法。

    类似优雅的题目还有,平衡二叉树。

    平衡二叉树

    平衡二叉树是一道简单题,题目如下:

    同理,我们设函数 isBalance 就是答案函数,那么一个平衡二叉树的特征,必然是其左右子树也是平衡的,所以可以写成:

    function isBalance(node: TreeNode) {
      if (root == null) return true
      return isBalance(node.left) && isBalance(node.right)
    }
    

    但是哪里不对,左右子树平衡还不够啊,万一左右子树之间深度相差超过 1 就坏了,所以还要求一下左右子树的深度,我们复用上题的函数 deep,整理一下如下:

    function isBalance(node: TreeNode) {
      if (root == null) return true
      return isBalance(root.left) && isBalance(root.right) &&
        Math.abs(deep(root.left) - deep(root.right)) < 2
    }
    

    这道题提醒我们,不是所有递归都能完美写成仅自己调用自己的模式,不同题目要辅以其他函数,要敏锐的察觉到还缺少哪些条件。

    还有一种递归,不是简单的函数自身递归自身,而是要构造出另一个函数进行递归,原因是递归参数不同。典型的题目有对称的二叉树。

    对称的二叉树

    对称的二叉树是一道简单题,题目如下:

    我们要注意,一颗二叉树的镜像比较特殊,比如最左节点与最右节点互为镜像,但它们的父节点并不相同,因此 isSymmetric(tree) 这样的参数是无法子递归的,我们必须拆解为左右子树作为参数,让它们进行相等判断,在传参时,将父级不同,但互为镜像的左右节点传入即可。

    所以我们必须起一个新函数 isSymmetricNew(left, right),将 left.leftright.right 对比,将 left.rightright.left 对比即可。

    具体代码就不写了,然后注意一下边界情况即可。

    这道题的重点是,由于镜像的关系,并不拥有相同的父节点,因此必须用一个新参数的函数进行递归。

    那如果这道题反过来呢?要求构造一个二叉树镜像呢?

    二叉树的镜像

    二叉树的镜像是一道简单题,题目如下:

    判断镜像比较容易,但构造镜像就要想一想了:

    例如输入:
         4
       /   \
      2     7
     / \   / \
    1   3 6   9
    
    镜像输出:
         4
       /   \
      7     2
     / \   / \
    9   6 3   1
    

    观察发现,其实镜像可以理解为左右子树互换,同时 其各子树的左右子树再递归互换,这就构成了一个递归:

    function mirrorTree(node: TreeNode) {
      if (node === null) return null
    
      const left = mirrorTree(node.left)
      const right = mirrorTree(node.right)
      node.left = right
      node.right = left
      return node
    }
    

    我们要从下到上,因此先生成递归好的左右子树,再进行当前节点的互换,最后返回根节点即可。

    接下来介绍一些有一定难度的经典题。

    二叉树的最近公共祖先

    二叉树的最近公共祖先是一道中等题,题目如下:

    题目很简短,也很明确,就是寻找最近的公共祖先。显然,根节点是所有节点的公共祖先,但不一定是最近的。

    我们还是用递归,先考虑特殊情况:如果任意节点等于当前节点,那么当前节点一定就是最近公共祖先,因为另一个节点一定在其子节点中。

    然后,利用递归思想思考,假设我们利用 lowestCommonAncestor 函数分别找到左右子节点的最近公共祖先会怎样?

    function lowestCommonAncestor(node, a, b) {
      const left = lowestCommonAncestor(node.left)
      const right = lowestCommonAncestor(node.right)
    }
    

    如果左右节点都找不到,说明只可能当前节点是最近公共子节点:

    if (!left && !right) return node
    

    如果左节点找不到,则右节点就是答案,否则相反:

    if (!left) return right
    return left
    

    这里巧妙利用了函数语义进行结果判断。

    二叉树的右视图

    二叉树的右视图是一道中等题,题目如下:

    想象一束光照,从二叉树右侧向左照射,自上而下读取即是答案。

    其实这道题可以认为是一道融合题。右侧的光束可以认为是分层照射的,那么当我们用广度优先算法遍历时,对于每一层,都找到最后一个节点打印,并且按顺序打印就是最终答案。

    有一道二叉树的题目,是根据树的深度,按照广度优先遍历打印成二维数组,记录树的深度其实也有巧妙办法,即在栈尾追加元素时,增加一个深度 key,那么访问时自然就可以读到深度值。

    完全二叉树的节点个数

    完全二叉树的节点个数是一道中等题,题目如下:

    用递归解决这道题的话,关键要分几种情况探讨完全二叉树。

    由于最底层可能没有填满,但最底层一定有节点,而且是按照从左到右填的,那么递归遍历左节点就可以获取树的最大深度,通过最大深度我们可以快速计算出节点个树,前提是二叉树必须是满的。

    但最底层节点可能不满,那怎么办呢?分情况即可,首先,如果一直按照 node.right....right 递归获得右侧节点深度,发现和最大深度相同,那么就是一个满二叉树,直接计算出结果即可。

    我们再看 node.right...left 的深度如果等于最大深度,说明 node.left 也就是左子树是个满二叉树,可以通过数学公式 2^n-1 快速算出节点个树。

    如果不等于最大深度呢?则说明右子树深度减 1 是满二叉树,也可以通过数学公式快速计算节点个数,再通过递归计算另一边即可。

    总结

    从题目中可以感受到,二叉树的解题魅力在于递归,二叉树问题中,我们可以同时追求优雅与答案。

    如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

    精读《算法 - 二叉树》

    起源地下载网 » 精读《算法 - 二叉树》

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元