二叉树的构造总篇(一篇解决全部构造思路)
本文将带领大家解决根据各种遍历方式构造二叉树的算法问题。
tips:博主不定期分享前端知识与算法。
公众号:FE Corner
wx小程序:FE Corner
首先,我们要清晰构造二叉树最基本的思路: 分解问题
构造完整的二叉树 =》分解为递归构造 根节点 + 左子树 + 右子树
1. 最大二叉树:中等
题目描述:
给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
创建一个根节点,其值为 nums 中的最大值。
递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
- [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
- 空数组,无子节点。
- [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
- 空数组,无子节点。
- 只有一个元素,所以子节点是一个值为 1 的节点。
- [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
- 只有一个元素,所以子节点是一个值为 0 的节点。
- 空数组,无子节点。
- [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
解法&思路
上文提到了要将问题分解为根节点+左子树+右子树,那么拿到这道题我们的思路就很清晰了
- 首先要解决的是找到每次递归传入数组的最大值做为根节点
- 该最大值下标左边的元素递归构造为左子树,右边的元素递归构造为右子树
详见下方代码:(注释)
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
return build(nums)
function build(nums){
if(nums.length === 0){
return null
}
// 找到最大值 和 最大索引值
let root = Math.max(...nums)
let index = nums.indexOf(root)
// 用最大值创建索引
let rootNode = new TreeNode(root)
// 最大值左侧为左子树
rootNode.left = build(nums.slice(0,index))
// 最大值右侧为左子树
rootNode.right = build(nums.slice(index+1,nums.length))
return rootNode
}
};
这样一来,这道题就迎刃而解了,思路仅仅是分解问题而已。
2. 从前序与中序遍历序列构造二叉树:中等
题目描述:
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
解法&思路
直奔主题,有了上一题的过渡,本题大家想必已经找到了第一思路:
- 找到根节点
- 从遍历数组中找到左子树,递归构造左子树
- 从遍历数组中找到右子树,递归构造右子树
下面一张图解决元素位置规律:
不难发现,根节点其实就是前序遍历数组的第一个元素
那么剩下的问题其实就是如何在每次递归数组中确认左右子树分别的范围,到这里不理解的小伙伴请见下方代码框架:(注意buid函数中的递归 ? 处)
// 函数签名分别 传入 前序遍历数组、中序遍历数组、前序遍历起始下标、中序遍历起始下表、尾标
function build(preorder,inorder,preStart,inStart,inEnd){
if(inStart > inEnd){
return null
}
// 从前序遍历中找到根节点
let root = preorder[preStart]
// 中序遍历中找到根节点位置
let rootIndex= inorder.indexOf(root)
//构造右子树时,前序遍历需要知道左子树长度
let leftSize = rootIndex - inStart
// 构造根节点
let rootNode = new TreeNode(root)
// 构造左右子树
rootNode.left = build(preorder,inorder,
?,
?,?)
rootNode.right = build(preorder,inorder,
?,
?,?)
return rootNode
}
这回应该明白了吧! 其实我们的问题就是找到每次build传入的下标而已。
而上图根据不同颜色已经找到了在两个遍历中,左右子树的分别下标:
首先明确定义:
- 前序遍历起点:preStart
- 中序遍历起点:inStart
- 中序遍历终点:inEnd
- 根节点索引(中序遍历中):rootIndex(这里是为了拿到左子树长度)
- 左子树长度:leftSize = rootIndex - inStart
- 根节点:前序遍历数组中的第一个,即preStart
- 前序遍历左子树起点:起始索引的下一个,即preStart+1
- 前序遍历右子树起点:起始索引+左子树长度+1
- 中序遍历左子树起点:起始索引,即inStart
- 中序遍历左子树终点:根节点索引-1,即rootIndex-1
- 中序遍历右子树起点:根节点索引+1,即rootIndex+1
- 中序遍历右子树终点:终点索引,即inEnd
至于为什么只需要中序遍历的终点,而不需要前序遍历的终点?因为在我们的思路中其实可以发现只需要前序遍历的起点确认根节点的值,并不需要终点值。我们可以随便选择一个遍历的终点值用来确认边界值,即这部分代码。
if(inStart > inEnd){
return null
}
明确了每个下标,我们只需要根据上面的结论填入 ? 处即可,详见全部代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} inorder
* @return {TreeNode}
*/
var buildTree = function(preorder, inorder) {
return build(preorder,inorder,0,0,inorder.length-1)
function build(preorder,inorder,preStart,inStart,inEnd){
if(inStart > inEnd){
return null
}
// 从前序遍历中找到根节点
let root = preorder[preStart]
// 中序遍历中找到根节点位置
let rootIndex= inorder.indexOf(root)
//构造右子树时,前序遍历需要知道左子树长度
let leftSize = rootIndex - inStart
// 构造根节点
let rootNode = new TreeNode(root)
// 构造左右子树
rootNode.left = build(preorder,inorder,
preStart+1,
inStart,rootIndex -1)
rootNode.right = build(preorder,inorder,
preStart + leftSize +1,
rootIndex+1,inEnd)
return rootNode
}
};
所以看似很难的题,只要根据树的构造思路分解问题,找到左右子树的范围,问题就迎刃而解了。
3. 从中序与后序遍历序列构造二叉树:中等
题目描述:
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
解法&思路
经历了上一道题,相信大家已经发现了两道题的区别,找根节点的过程变成了后序遍历的最后一个元素,和左右子树的索引稍有变动,剩下的思路可以说是一模一样。
只需要像上一道题一样画出索引图即可清晰找到边界:
首先明确定义:
- 后序遍历起点:postStart
- 后序遍历终点:postEnd
- 中序遍历起点:inStart
- 中序遍历终点:inEnd
- 根节点索引(中序遍历):rootIndex
- 左子树长度:leftSize = rootIndex - inStart
- 根节点:后序遍历数组中的最后,即postEnd
- 中序遍历左子树起点:inStart、终点:rootIndex-1
- 中序遍历右子树起点:rootIndex + 1、终点:inEnd
- 后序遍历左子树起点:postStart、终点:postStart + leftSize-1
- 后序遍历右子树起点:postStart + leftSize、终点:postEnd-1
至此边界已全部找到,补全代码即可,如下:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} inorder
* @param {number[]} postorder
* @return {TreeNode}
*/
var buildTree = function(inorder, postorder) {
return build(inorder,0,inorder.length-1,postorder,0,postorder.length-1)
function build(inorder,inStart,inEnd,postorder,postStart,postEnd){
if(inStart > inEnd){
return null
}
// 后续遍历的最后一个便是根节点
let root = postorder[postEnd]
// 找到根节点在中序遍历中的位置
let rootIndex = inorder.indexOf(root)
//后续遍历左子树起点需要左子树长度,左子树长度需要在前序遍历中获得
let leftSize = rootIndex - inStart
// 构造根节点
let rootNode = new TreeNode(root)
// 递归构造左右子树
rootNode.left = build(inorder,inStart,rootIndex-1,
postorder,postStart, postStart + leftSize-1 )
rootNode.right = build(inorder,rootIndex + 1,inEnd,
postorder,postStart + leftSize,postEnd-1)
return rootNode
}
};
有了前一题的铺垫,这道题无非就是 根节点 变成了后序遍历的最后一个元素,再改改递归函数的参数而已。
4. 根据前序和后序遍历构造二叉树:中等
题目描述:
给定两个整数数组,preorder 和 postorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。
如果存在多个答案,您可以返回其中 任何 一个。
解法&思路
本题最大的不同就在于只有根节点是确定的,而左右子树是不确定的,因此有多个解,题目也只要求返回任何一个即可。
为什么有多个解?举一个简单的例子:
preorder:a b c
postorder:c b a
这两棵树均符合上述定义。
虽然有这个多解的问题,但是基本思路其实并没有变化。
- 找到根节点:前序遍历第一个元素或者后序遍历最后一个元素均可以
- 前序遍历的第二个元素是左子树根节点
- 后序遍历中找到上面左子树的索引,从而确认边界,递归构造。
就不再赘述了,详见下方注释与代码:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} preorder
* @param {number[]} postorder
* @return {TreeNode}
*/
var constructFromPrePost = function(preorder, postorder) {
return build(preorder,0,preorder.length-1,postorder,0,postorder.length -1)
function build(preorder,preStart,preEnd,postorder,postStart,postEnd){
if(preStart > preEnd){
return null
}
//因为找左子树起点时使用了preorder[preStart+1],当preStart = preEnd时还剩下一个节点
if (preStart == preEnd) {
return new TreeNode(preorder[preStart]);
}
// 找到根节点
let rootVal = preorder[preStart]
// 找到左子树起点在后续遍历中的位置
let leftStartIndex = postorder.indexOf(preorder[preStart+1])
// 左子树长度
let size = leftStartIndex - postStart + 1
let root = new TreeNode(rootVal)
root.left = build(preorder,preStart +1,preStart+size,
postorder,postStart,leftStartIndex)
root.right = build(preorder,preStart+size+1,preEnd,
postorder,leftStartIndex+1,postEnd)
return root
}
};
至此本题就结束了,那么为什么会有很多解呢?问题就在这行代码
let leftStartIndex = postorder.indexOf (preorder[preStart+1])
我们代码中假设这个是左子树的根节点,但其实上面的图已经解释了:左子树可能为null,这时这个节点其实是右子树的根节点,因此存在多解。
小结
至此所有的构造二叉树问题已经解决了,我们callback一下:
清晰构造二叉树最基本的思路: 分解问题
构造完整的二叉树 =》分解为递归构造 根节点 + 左子树 + 右子树
找到根节点的索引,再根据此索引找到左右子树边界,递归构造,问题就迎刃而解了。
博主是前大厂程序猿,不定期分享前端知识与算法。
公众号:FE Corner
wx小程序:FE Corner
欢迎关注,一起探索知识~