[算法实践] 二叉树的遍历类型题实践(一)

486 阅读11分钟

二叉树的遍历实践(一)

系列开篇

为进入前端的你建立清晰、准确、必要概念和这些概念的之间清晰、准确、必要关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。

算法拆解是带你分析算法,掌握算法规律,体会概念与关联的力量。更重要的是让你不害怕算法题,利用分治思维拆解它,你会发现,又是回到了 概念关联 上。下面来看看我们遇到问题,如何去拆解, 并举一反三的吧。

算法实践是带你用拆解的算法模型,或者说是思考套路来具体应用在例题中,让你体会到算法思维体系的重要,也算是带你思考,如何应用这些算法模型。

算法是逃不掉的,它是你最直接的实现程序能力的体现。你写的每一句代码,每一种架构思考,每一种优化方式,都是你编程路上的硬核能力,所幸这个能力下'功夫'就能得到。

二叉树的遍历的扩展问题

我们的思路框架是:

  1. 我们判断每个节点该做什么事
  2. 是该用前/中/后/序遍历的哪种来处理会方便

类型题

  • 翻转二叉树
  • 最大二叉树
  • 二叉树展开为链表
  • 从遍历序列构造二叉树

翻转二叉树

挂上leetcode链接,可以用来查看更多示例和测试,并最后通过它。翻转二叉树

很显然题意就是 镜像翻转一棵二叉树。

图例          

     4                                4
   /   \                            /   \  
  2     7            ->            7     2 
 / \   / \                        / \   / \  
1   3 6   9                      9   6 3   1

下面是解法思路:

再重复一遍框架是:

  1. 我们判断每个节点该做什么事。
  • 这个问题比较简单所以比较容易看出: 我们要镜像翻转二叉树,也就是每个节点它的左右子节点进行交换,就翻转了。
  1. 是该用前/中/后/序遍历的哪种来处理会方便
  • 这里思考,当我们遍历到这个节点,我们该做的是 先交换左右子树,再递归遍历左右子树。既然先做操作其实就是先对根进行操作,也就是先 根 -> 左 -> 右 是不是前序遍历

回顾上节[算法拆解] 二叉树的遍历我们知道

前序遍历实现

const preOrderTraversalRecursion = (root) => {
  let resList = []

  const preOrderTraversal = (node) => {
    if (node !== null) {
      // 先访问根节点
      resList.push(node.val)               // ---------> (根)
      // 然后遍历左子树
      preOrderTraversal(node.left)         // ---------> (左)
      // 再遍历右子树
      preOrderTraversal(node.right)        // ---------> (右)
    }
  }
  
  // 根节点作为第一个参数传入
  preOrderTraversal(root)
  return resList
};

那么这个翻转二叉树的算法我们也可以轻易写出来:

const invertTree = (root) => {
  if (root === null) {
    return null;
  }
  
  // 先访问的是根 就在这里进行交换左右子树操作就行
  // 这里用的是解构赋值 不知道可以查查 ES6解构赋值
  [root.left, root.right] = [root.right, root.left]
  
  // 然后遍历左子树
  invertTree(root.left)         // ---------> (左)
  // 再遍历右子树
  invertTree(root.right)        // ---------> (右)
  
  return root
}

最大二叉树

最大二叉树 leetcode

给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:

  • 二叉树的根是数组中的最大元素。
  • 左子树是通过数组中最大值左边部分构造出的最大二叉树。
  • 右子树是通过数组中最大值右边部分构造出的最大二叉树。
  • 通过给定的数组构建最大二叉树,并且输出这个树的根节点。

示例

  • 输入:[3, 2, 1, 6, 0, 5]
  • 输出:返回下面这棵树的根节点:
         6
      /    \
    3        5
     \      / 
      2    0   
       \
        1

这个问题我想你直接看实现也能明白

实现

var constructMaximumBinaryTree = function(nums) {
  // 递归出口
  if (nums.length === 0) {
    return null
  }

  // 找到数组的最大值
  let maxNum = Math.max(...nums)
  let maxIndex = nums.indexOf(maxNum)
  let root = new TreeNode(maxNum)

  // 递归调用构造左右子树
  root.left = constructMaximumBinaryTree(nums.slice(0, maxIndex));
  root.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1));

  return root
};

二叉树展开为链表

二叉树展开为链表

给定一个二叉树,原地将它展开为一个单链表。

例如,给定二叉树

    1
   / \
  2   5
 / \   \
3   4   6

将其展开为:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

还是思考这个问题的最小操作,分解它 !为第一次重点


    1                1              1 
   / \              / \              \
  2!  5     ->     2!  5      ->      2
 / \   \            \   \              \
3!  4!  6            3!  6              3
                      \                  \
                       4!                 4
                                           \
                                            5
                                             \          
                                              6        

这个规律还是很好发现的,那么程序怎么写?

我们看出来,一个节点的最小操作其实就是:拉平一个二叉树

  1. 左子树变成右子树
  2. 原右子树挂到当前右子树的末尾

那么如何按题目要求把一棵树拉平成一条链表

  1. 我们首先要把 左子树 和 右子树 拉平才行,这样才能继续把这个节点进行拉平,所以第一步是将 root 的左子树和右子树拉平。也就是说这波递归操作在前面。(思考下应该是个后序遍历的框架)

  2. 当前节点进行最小操作: 1.左子树变成右子树 2.原右子树挂到当前右子树的末尾

实现

var flatten = function(root) {
  if (root === null) {
    return null
  }
  // 先进行递归操作,把子树都给拉平了,再操作当前节点(拉平当前节点)
  flatten(root.left)
  flatten(root.right)
  // 此时左右子树已经拉平 (很像图例的第二幅图)
  // 进行最小操作 
  let origin_left = root.left
  let origin_right = root.right

  // 1.左子树变成右子树
  root.left = null
  root.right = origin_left

  // 2. 原右子树挂到当前右子树的末尾
  // 先获取拉平后的最后的节点作为最后的挂载点
  let lastRightNode = root
  while (lastRightNode.right !== null) {
    lastRightNode = lastRightNode.right
  }
  lastRightNode.right = origin_right
};

从遍历序列构造二叉树

挂上leetcode链接,可以用来查看更多示例和测试,并最后通过它。

1. 根据一棵树的 前序遍历中序遍历 构造二叉树。

注意: 你可以假设树中没有重复的元素

例如,给出

  • 前序遍历 preorder = [3,9,20,15,7]
  • 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:
    3
   / \
  9  20
    /  \
   15   7

首先回顾上节[算法拆解] 二叉树的遍历我们知道

对于任意一颗树而言,

  • 前序遍历的形式总是 [ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
  • 中序遍历的形式总是 [ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

解法思路

  • 构建一个二叉树需要构建三部分:root、左子树、右子树
  • 左子树、右子树的构建,又包括:root、左子树、右子树
  • 根据根节点的定位,划分出左/右子树的 [前序,中序] 数组,其实就是准备递归的参数
  • 解题关键在于定位出根节点,划分出左右子树,然后递归构建左右子树

具体做法

  1. preorder 的第一项肯定是根节点, 因为前序遍历的顺序是 [根 左 右]。
  2. 由根节点,在 inorder [左 根 右] 中划分出属于左、右子树的 inorder 序列。
  3. 顺便我们得到了左右子树的节点个数,在 preorder 中划分出左、右子树的 preorder 序列。
  4. 这样,我们得到左、右子树的 preorderinorder 序列,就可以递归去构建左、右子树,最终形成完整的一棵二叉树

实现

var buildTree = function(preorder, inorder) {
   if (preorder.length === 0) {
     return null
   }
   // 根据前序数组的第一个元素,就可以确定根节点 
   let root = new TreeNode(preorder[0])
   // 定位根节点在中序遍历的midIndex
   let midIndex = inorder.indexOf(root.val)
   // 那么左边就是左子树节点个数,右子树节点个数都知道了, 左右子树的 (前序,中序)数组就都能搞出来了
   root.left = buildTree(
       preorder.slice(1, midIndex + 1),      // 左子树的前序遍历
       inorder.slice(0, midIndex)	     // 左子树的中序遍历
   );
   root.right = buildTree(
       preorder.slice(midIndex + 1), 
       inorder.slice(midIndex + 1)
   );
   return root
};

相关api简介(slice 用法):

前闭后开 [start_index, end_index)

console.log([0,1,2,3].slice(1, 2))   // [1] 
console.log([0,1,2,3].slice(0, 3))   // [0, 1, 2]
// 只有一个参数就是 start_index 一直截到最后
console.log([0,1,2,3].slice(1))      // [1, 2, 3]
// end 是负数 就是往前数index
console.log([0,1,2,3].slice(0, -1))  // [0, 1, 2]

思考问题千万别钻细节,比如递归参数为啥 midIndex + 1 不是 midIndex 等, 多关注整体思想,细节靠调试也能调出来,api用法等都是能查到的,但是思想是要靠积累和深度思考的。

我们再次回顾 思考这棵树如何被创建的最小过程,以及如何使用递归

  1. 题意是构建一个二叉树,已知条件是 这个二叉树的 前序/中序 遍历
  2. 构建二叉树 需要
    1. 构建 根
    2. 构建左子树 => 递归构建
    3. 构建右子树 => 递归构建 左右子树可看成一个完整的树,所以递归实现。
  3. 思考如何构建根呢,已知条件前序遍历,那么第一个元素就是根了。
  4. 下面的问题就是 如何获得左右子树分别的 前序,中序 遍历 来递归构建
  5. 可以用各种手法来做第四步,也有很多优化方式 slice只是一种较低效的方式。

2. 从中序与后序遍历序列构造二叉树

注意: 你可以假设树中没有重复的元素。

例如,给出

  • 中序遍历 inorder = [9,3,15,20,7]
  • 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:
    3
   / \
  9  20
    /  \
   15   7

这个思路和例一几乎一模一样,不赘述。直接给实现结果,你可以先自己在leetcode上写下,然后再看下面的实现。

实现

var buildTree = function(inorder, postorder) {
  let length = postorder.length
  if (length === 0) {
    return null
  }
  let root = new TreeNode(postorder[length - 1])
  let midIndex = inorder.indexOf(root.val)
  root.left = buildTree(
  	inorder.slice(0, midIndex), 
    	postorder.slice(0, midIndex));
  root.right = buildTree(
  	inorder.slice(midIndex + 1), 
    	postorder.slice(midIndex, length - 1));
  return root
};

3. 从前序和后序遍历序列构造二叉树

输入:

  • pre = [1, 2, 4, 5, 3, 6, 7],
  • post = [4, 5, 2, 6, 7, 3, 1] 输出:[1, 2, 3, 4, 5, 6, 7]
      1
    /   \
   2     3
  / \   / \
 4   5 6   7

我们思考这个递归函数对每个节点的操作是:

  1. 有2个参数,前序list,后序list
  2. 通过这个函数,构建 root 以及它的左子树,右子树,出口是 list.length === 0
  3. 那么想构建左子树,右子树 其实也是构建树,递归此函数,前提是知道左右子树的 那两个序列就行

实现

var constructFromPrePost = function(pre, post) {
  // 递归出口 前序列为空 直接返回
  if (pre.length === 0) {
    return null
  }
  // 前序遍历的第一个节点就是根
  let root = new TreeNode(pre[0])

  // 优化递归出口 前序列长度为1 这表示它没有左右子树了 可以直接返回 当然没这段都没事 这是优化
  if (pre.length === 1) {
    return root
  }

  // 左子树的length
  //    !pre[1]
  // 1 (2, 4, 5) (3, 6, 7)     => pre
  //       ! index = 2 => length === 3 左子树长度
  //(4, 5, 2)(6,  7, 3) 1      => post
  let left_list_length = post.indexOf(pre[1]) + 1
  // 下面就是递归了,主要就是思考如何获取左右子树的 前序,后序遍历
  root.left = constructFromPrePost(
    pre.slice(1, left_list_length + 1), 
    post.slice(0, left_list_length));
  root.right = constructFromPrePost(
    pre.slice(left_list_length + 1), 
    post.slice(left_list_length, -1));

  return root
};

其实我的注释基本看了就非常清晰了,但是还是重复下重点: 再次重复这类型的问题思路:

  1. 根据题意: 构建树,参数 为 前序list, 后序list
  2. 构建根,核心是 如何把左右子树的 前序list, 后序list 表示出来
  3. 递归构建左右子树,注意不要钻细节

这波我们先看这几个问题,发现了吗,其实思想都是一样,运用我们上篇的二叉树的遍历

继续下去,你总会有收获。

上面这句话给你们,同样也给我自己前进的动力。


我是摩尔,数学专业,做过互联网研发,测试,产品
致力用技术改变别人的生活,用梦想改变自己的生活
关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系

参考