二叉树的遍历实践(一)
系列开篇
为进入前端的你建立清晰、准确、必要的概念和这些概念的之间清晰、准确、必要的关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。
算法拆解是带你分析算法,掌握算法规律,体会概念与关联的力量。更重要的是让你不害怕算法题,利用分治思维拆解它,你会发现,又是回到了 概念 和 关联 上。下面来看看我们遇到问题,如何去拆解, 并举一反三的吧。
算法实践是带你用拆解的算法模型,或者说是思考套路来具体应用在例题中,让你体会到算法思维体系的重要,也算是带你思考,如何应用这些算法模型。
算法是逃不掉的,它是你最直接的实现程序能力的体现。你写的每一句代码,每一种架构思考,每一种优化方式,都是你编程路上的硬核能力,所幸这个能力下'功夫'就能得到。
二叉树的遍历的扩展问题
我们的思路框架是:
- 我们判断每个节点该做什么事
- 是该用前/中/后/序遍历的哪种来处理会方便
类型题
- 翻转二叉树
- 最大二叉树
- 二叉树展开为链表
- 从遍历序列构造二叉树
翻转二叉树
挂上leetcode链接,可以用来查看更多示例和测试,并最后通过它。翻转二叉树
很显然题意就是 镜像翻转一棵二叉树。
图例
4 4
/ \ / \
2 7 -> 7 2
/ \ / \ / \ / \
1 3 6 9 9 6 3 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
}
最大二叉树
给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
- 二叉树的根是数组中的最大元素。
- 左子树是通过数组中最大值左边部分构造出的最大二叉树。
- 右子树是通过数组中最大值右边部分构造出的最大二叉树。
- 通过给定的数组构建最大二叉树,并且输出这个树的根节点。
示例
- 输入:[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
这个规律还是很好发现的,那么程序怎么写?
我们看出来,一个节点的最小操作其实就是:拉平一个二叉树
- 左子树变成右子树
- 原右子树挂到当前右子树的末尾
那么如何按题目要求把一棵树拉平成一条链表
-
我们首先要把 左子树 和 右子树 拉平才行,这样才能继续把这个节点进行拉平,所以第一步是将 root 的左子树和右子树拉平。也就是说这波递归操作在前面。(思考下应该是个后序遍历的框架)
-
当前节点进行最小操作: 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、左子树、右子树
- 根据根节点的定位,划分出左/右子树的 [前序,中序] 数组,其实就是准备递归的参数
- 解题关键在于定位出根节点,划分出左右子树,然后递归构建左右子树
具体做法
preorder
的第一项肯定是根节点, 因为前序遍历的顺序是 [根 左 右]。- 由根节点,在
inorder
[左 根 右] 中划分出属于左、右子树的inorder
序列。 - 顺便我们得到了左右子树的节点个数,在
preorder
中划分出左、右子树的preorder
序列。 - 这样,我们得到左、右子树的
preorder
和inorder
序列,就可以递归去构建左、右子树,最终形成完整的一棵二叉树
实现
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用法等都是能查到的,但是思想是要靠积累和深度思考的。
我们再次回顾 思考这棵树如何被创建的最小过程,以及如何使用递归
- 题意是构建一个二叉树,已知条件是 这个二叉树的 前序/中序 遍历
- 构建二叉树 需要
- 构建 根
- 构建左子树 => 递归构建
- 构建右子树 => 递归构建 左右子树可看成一个完整的树,所以递归实现。
- 思考如何构建根呢,已知条件前序遍历,那么第一个元素就是根了。
- 下面的问题就是 如何获得左右子树分别的 前序,中序 遍历 来递归构建
- 可以用各种手法来做第四步,也有很多优化方式 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
我们思考这个递归函数对每个节点的操作是:
- 有2个参数,前序list,后序list
- 通过这个函数,构建 root 以及它的左子树,右子树,出口是 list.length === 0
- 那么想构建左子树,右子树 其实也是构建树,递归此函数,前提是知道左右子树的 那两个序列就行
实现
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
};
其实我的注释基本看了就非常清晰了,但是还是重复下重点: 再次重复这类型的问题思路:
- 根据题意: 构建树,参数 为 前序list, 后序list
- 构建根,核心是 如何把左右子树的 前序list, 后序list 表示出来
- 递归构建左右子树,注意不要钻细节
这波我们先看这几个问题,发现了吗,其实思想都是一样,运用我们上篇的二叉树的遍历
继续下去,你总会有收获。
上面这句话给你们,同样也给我自己前进的动力。
我是摩尔,数学专业,做过互联网研发,测试,产品
致力用技术改变别人的生活,用梦想改变自己的生活
关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系