节选自:https://mp.weixin.qq.com/s/1byx-MPeT0IfrBKwHFsZLg
二叉树前中后序遍历
前序遍历题目如下:
root节点是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)
* }
*
**/
function preorderTraversal (root) {
const arr = [] // 作为外部变量,存储结果
function preorder (root, arr) {
if (!root || !root.val) return []
arr.push(root.val)
// 递归调用root.left, root.right。
// 无需判断root.left 存在与否,因为上面已经做了异常判定
preorder(root.left, arr)
preorder(root.right, arr)
}
preorder(root, arr)
return arr
}
首先要明白什么事二叉树的前序遍历:根节点
-> 左节点
-> 右节点
的方式遍历这棵树,而在访问左子树或右子树的时候,按照同样的方式遍历,直至遍历完成。因此整个遍历过程天然具有递归的性质。
:::info
前序遍历: 根节点 -> 左子树 -> 右子树
中序遍历: 左子树 -> 根节点 -> 右子树
后序遍历:左子树 -> 右子树 -> 根节点
层级遍历: 只需要按层级遍历即可
:::
一些大厂会要求不使用递归,那么我们需要一套如何遍历一颗二叉树,并且先左子树,后右子树的通用模板。
前序遍历
通用模板
// 迭代法
function traversal (root) {
const res = []
const stack = [] // 维护一个栈,记录按顺序进入的节点
while (root || stack.length) { // 只要栈不为空,就继续执行
while(root) { // 只要节点不为空,就继续执行
res.push(root.val) // 存储遍历的值
stack.push(root) // 入栈
root = root.left // 左侧的节点也要不断经历入栈的过程。赋值后继续执行while
}
// 执行到这里,说明上一个while里的root为false了,即最左边的一条线上的节点已经入栈完毕了
// 这时候将最后一个入栈的节点拿出来,看它的右子节点的情况
root = stack.pop()
// 右子节点也会经历同样的过程
root = root.right
}
return res
}
结合图片分析,整个入栈过程是
- A B D 入栈
- D出栈
- B出栈
- E入栈
- E出栈
- A出栈
- C入栈
- C出栈
- F入栈
- F出栈
这里面的元素按入栈顺序排列,就是A -> B -> D -> E -> C -> F, 即前序遍历顺序。
中序遍历
中序遍历的递归法
// 中序遍历,每个节点内都是按照 左 -> 中 -> 右 的顺序
var inorderTraversal = function(root) {
const res = []
function temp (root) {
if (root && root.left) temp(root.left) // 递归调用,只要是root.left存在,就会一直往左边走,一直做到左边没有了,才会进入下一行
if (root && root.val) res.push(root.val)
if (root && root.right) temp(root.right)
}
temp(root)
return res
};
通用模板
function preorderTraversal (root) {
const stack = []
const res = []
while (root || stack.length) {
while (root) {
stack.push(root)
root = root.left
}
res.push(root.val)
root = stack.pop()
root = root.right
}
return res
}
遍历的入栈出栈顺序是一致的,还是按照从上到下,从左到右的顺序入栈,只不过在记录的时候,记录出栈的顺序。出栈顺序就是中序遍历。
后序遍历
后序遍历 左子树
-> 右子树
-> 根节点
后序遍历递归调用法
/**
* @param {TreeNode} root
* @return {number[]}
*/
// 后序遍历: 左 -> 右 -> 中
var postorderTraversal = function(root) {
const res = []
function temp(root) {
if (root && root.left) temp(root.left)
if (root && root.right) temp(root.right)
if (root && root.val) res.push(root.val)
}
temp(root)
return res
};
通用模板
function preorderTraversal (root) {
const stack = []
const res = []
while (root || stack.length) {
while(root) {
res.push(root.val) // 或者 res.unshift(root.val) 插入第一位
stack.push(root)
root = root.right
}
root = stack.pop()
root = root.left
}
return res.reverse()
}
// 这里后序遍历,左 -> 右 -> 中,其实可以看成是 中 -> 右 -> 左的倒序。
// 那么我们在遍历的时候,按照先右后左的顺序,只不过每次遍历的节点值,插在后面
var postorderTraversal = function(root) {
const res = []
const stack = []
while(root || stack.length) {
while(root) {
stack.push(root)
res.unshift(root.val) // 插在队尾
root = root.right
}
root = stack.pop()
root = root.left
}
return res
}
// 或者这种解法也是这个思路
const postorderTraversal = root => {
if (!root) return [];
const arr = [root];
const res = [];
while (arr.length) {
const n = arr.pop();
res.unshift(n.val); // 插入队尾
n.left && arr.push(n.left);
n.right && arr.push(n.right); // 那每次就先pop出来right
}
return res;
};
后序遍历和前序遍历的区别就在于,先入栈的元素,放在后面。从图中 654321的顺序可以观察出来,其实就是从右向左的遍历过程。
总结:
从三种递归调用的方法中可以看出,递归都是在调用 temp(root.left) 和 temp(root.right), 无非是res.push(root.val)的时机不同而已。
通过前中后序遍历二叉树可以看出,无论哪种顺序,二叉树都是从上到下进行遍历的,无非是是记录入栈的顺序还是出栈的顺序,从先左边还是先右边而已, 没有魔法。
对称二叉树
待续
判断一棵树是否是对称的,例如 二叉树 [1,2,2,3,4,4,3] 是对称的
1
/ \
2 2
/ \ / \
3 4 4 3
但是 [1,2,2,null,3, null,3] 就不是对称的
1
/ \
2 2
\ \
3 3
思路:递归解决
- 判断两个指针当前节点值是否相等
- 判断A的右子树与B的左子树是否对称
- 判断A的左子树和B的右子树是否对称
关键在于找到一个合适的递归时机
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function(root) {
if (!root) return root
function isMirrorEqual(left, right) {
if (left === null && right === null) return true
if (left === null || right === null) return false
// 注意,这里比较的是 左节点的left和右节点的right, 左节点的right和右节点的left。 而不是同一个节点内的左右比较
return left.val === right.val && isMirrorEqual(left.left, right.right) && isMirrorEqual(left.right, right.left)
}
return isMirrorEqual(root.left, root.right)
};
二叉树的最大深度
掌握遍历的套路
- 只要遍历到这个节点既没有左子树,也没有右子树,说明就到底了,这时候如果之前记录了深度,就可以比较是否比之前记录的深度大,大就更新深度。
- 依次类推,一直到最大深度。
精炼版function maxDepth (root) {
if (!root) return root
let ret = 1
function compareDepth (root, depth) {
// 左右节点都没有,比较曾今的记录和传入的depth的较大者,不断更改外部变量ret,最后才能返回ret
if (!root.left && !root.right) ret = Math.max(ret, depth)
if (root.left) compareDepth(root.left, depth + 1)
if (root.right) compareDepth(root.right, depth + 1)
}
compareDepth(root, ret)
// 被多次递归调用更新后, 返回的ret是最大的深度了
return ret
}
function maxDepth (root) {
if (!root) return 0
const left = root.left
const right = root.right
return Math.max(maxDepth(left), maxDepth(right)) + 1 // +1是因为每调用一次maxDepth,深度就会增加一层
}
将有序数组转化为高度平衡二叉搜索树
给一个整数数组nums,其中元素已经按升序排序,请将其转换为一颗高度平衡的二叉搜索数。
高度平衡的二叉搜索数是一颗满足 「每个节点的左右两个子树的高度差的绝对值不超过1」的二叉树
注意是,高度差不超过1
示例1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案
示例2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,3] 和 [3,1] 都是高度平衡二叉搜索树。
思路:
- 构建一棵树包括: 构建root,构建root.left , root.right
- 题目要求 ‘高度平衡’, 构建root的时候,选择数组的中间元素作为 root 节点值,即可保持平衡。
- 递归函数可以传递数组,也可以传递指针,传递指针的时候l r 分别代表参与构建的二叉搜索树的数组的首尾索引
// [-10,-3,0,5,9, 10, 13,15,17,18,20,33,34,45,66,77,88,99,100]
// 我这种方法比较常规,每次对数组进行取出中间点,然后树的值为这个中间点的值。
// 然后分割成两个数组再次进行上述操作
function TreeNode (val) {
this.val = val
}
function createNode (arr) {
if (!arr.length || arr.length < 2) return arr[0] || null
const midIndex = Math.floor(arr.length / 2)
const mid = arr[midIndex]
const root = new TreeNode(mid)
root.left = createNode(arr.slice(0, midIndex))
root.right = createNode(arr.slice(midIndex + 1, arr.length))
return root
}
最后可以得到
18
/ \
9 66
/ \ / \
0 15 34 99
/ \ / \ / \ / \
-3 5 13 17 33 45 88 100
/ / / /
-10 10 20 77
// 另一种算法
const sortedArrayToBST = function (nums) {
return toBST(nums, 0, nums.length - 1)
}
// l, r是传入的左右索引
const toBST = function (nums, l, r) {
if (l > r) return null
// 移位操作,先转成二进制,然后整体向右移动一位。是取中间值的快捷操作
// js 二进制十进制互转 5.toString(2) <=> parseInt('0101', 2)
const mid = l + r >> 1
const root = new TreeNode(arr[mid])
root.left = toBST(nums, l, mid - 1) // 这里就不用改变数组,而是改变左右索引的位置
root.right = toBST(nums, mid + 1, r)
return root
}
反转二叉树
翻转一棵二叉树。
示例:
输入:
4<br /> / \<br /> 2 7<br /> / \ / \<br />1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
备注:
这个问题是受到 Max Howell 的 原问题 启发的 :
谷歌:我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。
结题思路:
二叉树肯定要从根节点开始对整棵树进行递归的遍历,解题中我们只需要关注一个节点的情况,然后对各个节点递归调用。
这个节点可能为null,可能其左右节点为null,分别处理。
最终仍要返回这个root,只不过是修改后的了。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
if (!root || !root.val) return root
if (root.left || root.right) {
[root.left, root.right] = [root.right, root.left]
if (root.left) invertTree(root.left)
if (root.right) invertTree(root.right)
}
return root
};
合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
思路 两个值都为null,返回null 一个是null,则返回非null的值 两个都非null,值相加。其左右也相加
// 深度优先
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} t1
* @param {TreeNode} t2
* @return {TreeNode}
*/
function mergeTrees (t1, t2) {
if (!t1 || !t2) return t1 || t2
t1.val += t2.val
t1.left = mergeTrees(t1.left, t2.left)
t2.right = mergeTrees(t2.right, t2.right)
return t1
}
// 广度优先
// 把两个节点都放入队列,然后取出节点累加,然后再看它们的左右节点,如果存在,则继续放入队列中。
function mergeTrees (t1, t2) {
if (!t1 || !t2) return t1 || t2
let queue = [ [t1, t2] ]
while(queue.length) {
const [ h1, h2 ] = queue.shift() // 每次取出一个来处理
const { left: l1, right: r1 } = h1
const { left: l2, right: r2 } = h2
h1.val += h2.val
if (l1 && l2) queue.push([l1, l2])
if (r1 && r2) queue.push([r1, r2])
if (!l1 && l2) h1.left = h2.left // l1存在l2不存在的情况不用处理
if(!r1 && r2) h1.right = h2.right
}
return t1
}
广度优先
二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3<br /> / \<br /> 9 20<br /> / \<br /> 15 7<br />返回它的最大深度 3 。
思路:仍然是递归的方式,
这个深度其实是从下而上计算的。从终止层的0开始,每层+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 {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
if (!root) return 0
const left = root.left
const right = root.right
return Math.max(maxDepth(left), maxDepth(right)) + 1
};
BFS写法
const maxDepth = (root) => {
if (root == null) return 0;
const queue = [root];
let depth = 1;
while (queue.length) {
// 当前层的节点个数
const levelSize = queue.length;
// 逐个让当前层的节点出列
for (let i = 0; i < levelSize; i++) {
// 当前出列的节点
const cur = queue.shift();
// 左右子节点入列
if (cur.left) queue.push(cur.left);
if (cur.right) queue.push(cur.right);
}
// 当前层所有节点已经出列,如果队列不为空,说明有下一层节点,depth+1
if (queue.length) depth++;
}
return depth;
};
作者:xiao_ben_zhu
链接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/solution/liang-chong-jie-fa-di-gui-dfs-bfs-by-hyj8/