二叉树的解题思路
在前文中介绍了二叉树的相关知识,可以进行查看阅读。
解题技巧
前/中/后序
二叉树的遍历框架,非线性的递归遍历
class TreeNode {
constructor(val, left, right) {
this.val = val || undefined;
this.left = left || null;
this.right = right || null;
}
}
const recursion = (root) => {
//前序
recursion(root.left);
//中序
recursion(root.right);
//后序
};
层级遍历(bfs)
广度遍历的思想就是先进先出,以层来做为入队和出队的判断条件
const bfs = (root) => {
const queue = [root];
while (queue.length) {
const levelLength = queue.length;
for (let i = 0; i < levelLength; i++) {
const curr = queue.shift();
//对curr做处理
curr.left && queue.push(curr.left);
curr.right && queue.push(curr.right);
}
}
};
dfs
dfs 其实是树的前序遍历,上面的代码中已经使用递归的思想实现了 如果用迭代的思想,需要借助栈结构,采用先进后出的思想
- 递归
const dfs = (root) => {
//对当前数据做点啥
dfs(root.left);
dfs(root.right);
};
- 非递归
const bfs = (root) => {
const stack = [root];
while (stack.length) {
const curr = stack.pop();
//对curr做处理
curr.right && stack.push(curr.right);
curr.left && stack.push(curr.left);
}
};
基本性质
树的最大高度
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
leetcode 分析:求树的高度需要找出左右子树的高度,两者中最大的那个+1 就是当前树高,对于子树也是这个逻辑。当遍历的节点为空时就递归结束,root===null。
var maxDepth = function (root) {
if (!root) return 0;
let lefth = maxDepth(root.left);
let righth = maxDepth(root.right);
return Math.max(lefth, righth) + 1;
};
// Runtime: 52 ms, faster than 100% of JavaScript
二叉树的最小深度
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
分析 1:采用 BFS,一层层遍历。发现当前层的某个节点没有子节点,意味着这是树的最小深度。
var minDepth = function (root) {
if (!root) return 0;
const queue = [root];
let depth = 0;
while (queue.length) {
const size = queue.length;
depth++;
for (let i = 0; i < size; i++) {
const curr = queue.shift();
if (!curr.left && !curr.right) {
return depth;
}
curr.left && queue.push(curr.left);
curr.right && queue.push(curr.right);
}
}
return depth;
};
分析 2:采用 DFS
var minDepth = function (root) {
if (!root) return 0;
const dfs = (root) => {
if (!root) return;
const left = dfs(root.left);
const right = dfs(root.right);
if (!left && !right) return 1;
if (!left) return right + 1;
if (!right) return left + 1;
return Math.min(left, right) + 1;
};
return dfs(root);
};
平衡树
示例 1: 示例 2:
给定二叉树 [3,9,20,null,null,15,7] 给定二叉树 [1,2,2,3,3,null,null,4,4]
3 1
/ \ / \
9 20 2 2
/ \ / \
15 7 3 3
/ \
4 4
返回 true 返回 false
leetcode 分析:平衡树就是结点的左右子树的高度<=1。递归找到左右子树的高度,如果左右子树高度差大于 1 返回 false,否则返回 true。所以当前节点的左右子树高度<=1&&当前节点的左子树的左右子树高度<=1&&当前节点的右子树的左右子树高度<=1,但这个条件成立的时候,返回 true
var isBalanced = function (root) {
const getDepth = (root) => {
if (!root) return 0;
const left = getDepth(root.left);
const right = getDepth(root.right);
return Math.max(left, right) + 1;
};
if (!root) return true;
const curr = Math.abs(getDepth(root.left) - getDepth(root.right)) <= 1;
return curr && isBalanced(root.left) && isBalanced(root.right);
};
//Runtime: 96 ms, faster than 46.01% of JavaScript
翻转二叉树
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
leetcode 分析:利用递归,交换左右子树翻转结果,直到最后一个节点的左右子树都是 null,返回结果
题解:
var invertTree = function (root) {
if (!root) {
return null;
}
let left = root.left;
let right = root.right;
root.right = invertTree(left);
root.left = invertTree(right);
return root;
};
// Runtime: 72 ms, faster than 86.03% of JavaScript
归并两棵树
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
leetcode 分析:二叉树的合并就是一棵树的节点值加上另一棵树的节点值。使用递归,当前节点相加,左右子树分别合并的结果就是最后需要放回的树。直到两棵树都遍历完结束。
var mergeTrees = function (t1, t2) {
if (!t1 || !t2) {
return t1 || t2;
}
return new TreeNode(
t1.val + t2.val,
mergeTrees(t1.left, t2.left),
mergeTrees(t1.right, t2.right),
);
};
//Runtime: 112 ms, faster than 73.12% of JavaScript
二叉树遍历
从前序与中序遍历序列构造二叉树
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
leetcode 分析:对于前序遍历序列来说第一个数据就是当前树根节点,在中序遍历中根节点左右两侧的分别是左右子树序列。
var buildTree = function (preorder, inorder) {
const buildTreeFunc = (start, end) => {
if (start > end) return null;
const tree = new TreeNode();
const val = preorder.shift();
tree.val = val;
tree.left = buildTreeFunc(start, inorder.indexOf(val) - 1);
tree.right = buildTreeFunc(inorder.indexOf(val) + 1, end);
return tree;
};
return buildTreeFunc(0, preorder.length - 1);
};
//Runtime: 132 ms, faster than 46.85 % of javascript 待优化
左叶子之和
3
/ \
9 20
/ \
15 7
在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
分析:每次递归前,要先判断节点是不是空。如果左子树存在并且没有子节点,就累计结果;否则继续递归。
var sumOfLeftLeaves = function (root) {
let res = 0;
const recur = (root) => {
if (!root) return;
if (root.left && !root.left.left && !root.left.right) {
res += root.left.val;
}
root.left && recur(root.left);
root.right && recur(root.right);
};
recur(root);
return res;
};
二叉树的层平均值
输入:
3
/ \
9 20
/ \
15 7
输出:[3, 14.5, 11]
解释:
第 0 层的平均值是 3 , 第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11] 。
分析:采用层级遍历,套用上文的 bfs 模板。第一层循环表示层级,第二层循坏表示循环当前层级的所有节点。
var averageOfLevels = function (root) {
const queue = [];
const res = [];
queue.push(root);
while (queue.length) {
let sum = 0;
const levelLen = queue.length;
for (let index = 0; index < levelLen; index++) {
const curr = queue.shift();
sum += curr.val;
curr.left && queue.push(curr.left);
curr.right && queue.push(curr.right);
}
res.push(sum / levelLen);
}
return res;
};
//Runtime: 84 ms, faster than 94.02 % of javascript
二叉树的坡度
1
/ \
2 3
输出:1
解释:
结点 2 的坡度: 0
结点 3 的坡度: 0
结点 1 的坡度: |2-3| = 1
树的坡度 : 0 + 0 + 1 = 1
leetcode 分析:节点坡度是左子树的结点之和和右子树结点之和的差的绝对值,所以要从最深开始遍历 递归开始,设置一个值获取每个节点下面,左节点总和,右节点总和。然后去相减取绝对值 注意每次递归前,要先判断节点是不是空
var findTilt = function (root) {
let res = 0;
const recur = (root) => {
if (!root) return 0;
const left = recur(root.left);
const right = recur(root.right);
res += Math.abs(left - right);
return root.val + left + right;
};
recur(root);
return res;
};
//Runtime: 92 ms, faster than 61.49% of JavaScript
二叉树中第二小的节点
给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。
示例 1: 示例 2:
输入: 输入:
2 2
/ \ / \
2 5 2 2
/ \
5 7
输出: 5 输出: -1
说明: 最小的值是 2 ,第二小的值是 5 。 说明: 最小的值是 2, 但是不存在第二小的值。
leetcode 分析 1:采用 dfs 递归,借助数组存储数据。如果 res 中不存在当前节点值,就放入数组中,从而达到去重的效果。
var findSecondMinimumValue = function (root) {
const res = [];
const recur = (root) => {
if (!root) return;
const curr = root.val;
if (!res.includes(curr)) {
res.push(curr);
}
recur(root.left);
recur(root.right);
};
recur(root);
res.sort((a, b) => a - b);
return res[1] ? res[1] : -1;
};
//Runtime: 76 ms, faster than 61.46% of JavaScript
求根到叶子节点数字之和
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
leetcode 分析:每个节点都代表一个数字,等同于其父节点对应的数字*10 再加上该节点的值。计算出每个叶子节点对应的数字,计算出所有的叶子节点对应的数字之和。 分析 1:dfs,从根节点开始,遍历每个节点,如果遇到叶子节点就将数字加到数字之和。如果不是叶子节点,就计算当前节点的数字,然后对子节点递归遍历。
var sumNumbers = function (root) {
const dfs = (root, i) => {
if (!root) return 0;
let temp = i * 10 + root.val;
if (!root.left && !root.right) {
return temp;
}
return dfs(root.left, temp) + dfs(root.right, temp);
};
return dfs(root, 0);
};
//Runtime: 84 ms, faster than 52.22% of JavaScript
分析 2:bfs,需要维护两个队列,节点队列和节点对应的数字。如果当前节点是叶子节点,将节点对应的数字加到数字之和;如果不是叶子节点,就把子节点和子节点计算出的对应数字分别加入两个队列中。
var sumNumbers = function (root) {
if (!root) return 0;
const nodeQueue = [root];
const numQueue = [root.val];
let res = 0;
while (nodeQueue.length) {
const currNode = nodeQueue.shift();
const currNum = numQueue.shift();
if (!currNode.left && !currNode.right) {
res += currNum;
}
if (currNode.left) {
nodeQueue.push(currNode.left);
numQueue.push(currNum * 10 + currNode.left.val);
}
if (currNode.right) {
nodeQueue.push(currNode.right);
numQueue.push(currNum * 10 + currNode.right.val);
}
}
return res;
};
二叉树的路径
二叉树的所有路径
输入:
1
/ \
2 3
\
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
leetcode 分析:利用前序遍历,当某个节点的左右子树都为 null 的时候,说明已经走完该路径
var binaryTreePaths = function (root) {
const res = [];
const recur = (root, str) => {
if (!root) return '';
str += `${root.val}`;
if (!root.left && !root.right) {
res.push(str);
} else {
str += '->';
recur(root.left, str);
recur(root.right, str);
}
};
recur(root, '');
return res;
};
//Runtime: 76 ms,faster than 89.60 % of JavaScript
路径总和
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
分析:遍历树节点,当遍历完且遍历的所有结点和为 sum 返回 true,值得注意的一点根节点到叶子节点,所以条件中需要加入 root.left===null&&root.right===null
var hasPathSum = function (root, sum) {
let res = false;
const dfs = (root, sum) => {
if (!root) return;
if (root.val === sum && !root.left && !root.right) {
return (res = true);
}
root.left && dfs(root.left, sum - root.val);
root.right && dfs(root.right, sum - root.val);
};
dfs(root, sum);
return res;
};
//Runtime: 88 ms, faster than 65.75% of JavaScript
最长同值路径
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
注意:两个节点之间的路径长度由它们之间的边数表示。
示例 1: 示例 2:
输入: 输入:
5 1
/ \ / \
4 5 4 5
/ \ \ / \ \
1 1 5 4 4 5
输出: 2 输出: 2
分析:采用 DFS。对于当前节点,左子树能提供的长度为 left,如果当前节点值等于左子节点的值,则左链的长度等于 left+1,否则为 0(右子树也是如此)。当前子树对父节点提供的最大长度为左右链中较大的一个。当前子树的左右链之和,去和全局最大值比较
var longestUnivaluePath = function (root) {
let level = 0;
function helper(parent, current) {
if (current === null) return 0;
let left = helper(current.val, current.left);
let right = helper(current.val, current.right);
level = Math.max(level, left + right);
return current.val === parent ? Math.max(left, right) + 1 : 0;
}
if (root !== null) helper(root.val, root);
return level;
};
//Runtime: 252 ms, faster than 46.37% of JavaScript
二叉搜索树
将有序数组转换为二叉搜索树
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
leetcode 分析:为了达到平衡,每次都把数组二分,中间的值作为根节点,然后左右分别为左右子树,依次递归,直至数组为空。
var sortedArrayToBST = function (nums) {
if (!nums.length) return null;
const mid = Math.ceil((nums.length - 1) / 2);
return new TreeNode(
nums[mid],
sortedArrayToBST(nums.slice(0, mid)),
sortedArrayToBST(nums.slice(mid + 1)),
);
};
//Runtime: 124 ms, faster than 17.83 % of javascript 待优化
两数之和 IV - 输入 BST
给定一个二叉搜索树和一个目标结果,如果 BST 中存在两个元素且它们的和等于给定的目标结果,则返回 true。
案例 1: 案例 2:
输入: 输入:
5 5
/ \ / \
3 6 3 6
/ \ \ / \ \
2 4 7 2 4 7
Target = 9 Target = 28
输出: True 输出: False
分析 1:先把树转化为数组,将题目转换成为在数组中是否存在两个元素相加为目标结果
var findTarget = function (root, k) {
let nums = [];
const inOrder = (root) => {
if (!root) return;
inOrder(root.left);
nums.push(root.val);
inOrder(root.right);
};
inOrder(root);
const arrMap = new Map();
for (let i = 0; i < nums.length; i++) {
const result = k - nums[i];
if (arrMap.has(result)) {
return true;
}
arrMap.set(nums[i], i);
}
return false;
};
//Runtime: 100 ms, faster than 96.68% of JavaScript
分析 2:直接在树中,一个个值拿出来进行比较,采用 dfs 但先遍历右子树
var findTarget = function (root, k) {
const stack = [root];
const map = new Map();
while (stack.length) {
const curr = stack.pop();
if (map.has(k - curr.val)) {
return true;
}
map.set(curr.val, 1);
curr.left && stack.push(curr.left);
curr.right && stack.push(curr.right);
}
return false;
};
//Runtime: 132 ms, faster than 28.32% of JavaScript
二叉搜索树节点最小距离
输入: root = [4,2,6,1,3,null,null]
输出: 1
解释:
注意,root是树节点对象(TreeNode object),而不是数组。
给定的树 [4,2,6,1,3,null,null] 可表示为下图:
4
/ \
2 6
/ \
1 3
最小的差值是 1, 它是节点1和节点2的差值, 也是节点3和节点2的差值。
leetcode 分析:通过中序遍历,使用 last 存储上一个节点的 val 值,和当前节点的值做减法与 ans 做比较,获得最小的值
var minDiffInBST = function (root) {
let last = undefined;
let ans = Infinity;
const inOrder = (root) => {
if (!root) return;
inOrder(root.left);
if (last !== undefined) {
ans = Math.min(ans, Math.abs(root.val - last));
}
last = root.val;
inOrder(root.right);
};
inOrder(root);
return ans;
};
//Runtime: 68 ms, faster than 96.55% of JavaScript
二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
给定 BST [1,null,2,2]
1
\
2
/
2
返回[2]
leetcode 分析:采用 dfs 递归解法,当 maxCount 小于遍历结点值出现的次数,就更新 maxCount 并置空 res;相等就把节点值放入 res;小于不操作。
var findMode = function (root) {
const map = new Map(); //存储数字出现的次数
const res = []; //存储出现次数最多的数
let maxCount = 0; //当前最大出现次数
const recur = (root) => {
if (!root) return;
const curr = root.val;
map.has(curr) ? map.set(curr, map.get(curr) + 1) : map.set(curr, 1);
if (map.get(curr) >= maxCount) {
map.get(curr) > maxCount && (res.length = 0); //如果比当前maxCount大,则重置res
maxCount = map.get(curr);
res.push(curr);
}
recur(root.left);
recur(root.right);
};
recur(root);
return res;
};
//Runtime: 92 ms, faster than 76.30% of JavaScript