在前端中确实用到不少与树相关的的知识,比方说 DOM 树,Diff 算法,包括原型链其实都算是树,学会树,其实对于学这些知识还是有比较大的帮助的,当然我们学算法还是得考虑面试,而树恰好也是一个大重点 – 起码在前端而言;
主要原因在于,树它华而不实,比较下里巴人,需要抽象但是又能把图画出来不至于让你毫无头绪,简单而言就是看上去很厉害,但实际上也很接地气,俗称比较一般
;要知道做前端的面试算法,考的不就是你有么得主动学习能力,抽象能力等,但是考虑到参差不齐的前端娱乐圈,考得难吧可能就全是漏网之鱼了,所以既要筛选出鱼,但是又不能难度过大,树就是那个比较适中的,所以赶紧刷起来吧朋友们;
这里本来是要遵照 3:5:2 难度来刷,预计刷个30题就差不多,但是实际中等题刷得欲罢不能,难题是欲仙欲死,容易题是味如嚼蜡,所以 XDM 担待一下。选题主要是那个男人精选的例题以及 Leetcode 中 HOT 题和字节专题,总的来说代表性还是够的,刷完应该大概或许能够应付一下树这方面的算法了。
如果觉得有那么点帮助,请点个赞留个言,点赞超过10个就更新下一part;好吧,即便不过也会更新,就是这么臭不要脸,大家伙加油吧,欧力给!!
递归遍历
迭代遍历 – 双色标记法
按照那个男人的指示,正常我们就用递归做就好,就好像我们做非排序题排序的时候,sort 一下就好了,但是一旦面试官问到用另外的迭代方式的时候,我们再套个模板,会比记住多个迭代写法要简单,毕竟内存容量有限,而后续遍历的迭代写法确实挺坑的,能省一点内存就省一点吧
// 144. 二叉树的前序遍历
/** * @分析 -- 递归 */
var preorderTraversal = function (root) {
const ret = [];
const recursion = (root) => {
if (!root) return;
ret.push(root.val);
recursion(root.left);
recursion(root.right);
};
recursion(root);
return ret;
};
/** * @分析 -- 迭代 -- 双色标记法 * 1. 使用颜色标记节点状态,新节点为白色,已经访问的节点为灰色 -- 可以用数字或者其他任意标签标示 * 2. 如果遇到的节点是白色,则标记为灰色,然后将右节点,自身,左节点一次入栈 -- 中序遍历 * 3. 如果遇到的节点是灰色的,则将节点输出 * 4. 注意这里是用 stack 栈来存储的,所以是后进先出,这里是前序遍历,中 - 左 - 右 ,那么在插入栈的时候要反过来 右 - 左 - 中 */
var preorderTraversal = function (root) {
const ret = [];
const stack = [];
stack.push([root, 0]); // 0 是白色未处理的,1 是灰色处理过的
while (stack.length) {
const [root, color] = stack.pop();
if (root) {
if (color === 0) {
// 遇到白球,则插入 -- 前序
stack.push([root.right, 0]);
stack.push([root.left, 0]);
stack.push([root, 1]);
} else {
// 遇到灰球,则收网
ret.push(root.val);
}
}
}
return ret;
};
参考视频:传送门
// 94. 二叉树的中序遍历
/** * @分析 * 1. 递归的时候前中后序都能直接处理完了 * 2. 递归是前中后序遍历最简单也是最容易出理解的方法,不懂的画个图就好了 */
var inorderTraversal = function(root) {
const ret = []
const recursion = root => {
if(!root) return
recursion(root.left)
// 这里是中序,所以在两个递归之间,如果是前序就在前面,后序就在后面
ret.push(root.val)
recursion(root.right)
}
recursion(root)
return ret
};
/** * @分析 -- 迭代 -- 双色标记法 * 1. 使用颜色标记节点状态,新节点为白色,已经访问的节点为灰色 -- 可以用数字或者其他任意标签标示 * 2. 如果遇到的节点是白色,则标记为灰色,然后将右节点,自身,左节点一次入栈 -- 中序遍历 * 3. 如果遇到的节点是灰色的,则将节点输出 * 4. 注意这里是用 stack 栈来存储的,所以是后进先出,所以如果是中序遍历,左 - 中 - 右 ,那么在插入栈的时候要反过来 右 - 中 - 左 */
var inorderTraversal = function(root) {
const ret = []
const stack = []
stack.push([root,0]) // 0 是白色未处理的,1 是灰色处理过的
while(stack.length) {
const [root,color] = stack.pop()
if(root){
if(color === 0){
// 遇到白球,则插入 -- 中序遍历
stack.push([root.right,0])
stack.push([root,1])
stack.push([root.left,0])
}else{
// 遇到灰球,则收网
ret.push(root.val)
}
}
}
return ret
};
// 145. 二叉树的后序遍历
/** * @分析 -- 递归 */
var postorderTraversal = function(root) {
const ret = []
const dfs = (root) => {
if(!root) return
dfs(root.left)
dfs(root.right)
ret.push(root.val)
}
dfs(root)
return ret
};
/** * @分析 -- 迭代 -- 双色球 */
var postorderTraversal = function(root) {
const ret = []
const stack = []
stack.push([root,0])
while(stack.length){
const [root,color] = stack.pop()
if(root) {
if(color === 0){
stack.push([root,1])
stack.push([root.right,0])
stack.push([root.left,0])
}else{
ret.push(root.val)
}
}
}
return ret
}
这两个名词在很多讲树的题解中经常会出现,而这与我们遍历树求值到底关联点在哪里,慢慢刷题之后我发现,虽然 dfs 有三种形式,但在抽象到具体题目的时候,其实是属于不同的方法的。
对于前序遍历
而言,就是先获取到根节点的信息,然后做了一定编码后,再向下遍历,这种遍历方式就是所谓的 自顶向下
的思维,我们从根节点开始,可以携带一定的信息,再继续往下遍历时,先处理,得到临时性结果,给顶层的节点作为信息;
对于自顶向下
的遍历而已,遍历到根节点,就处理结束所有的节点,也相应的得到预期结果了,所以一般使用前序遍历
方法解题的,都会声明一个全局变量,然后遍历完之后,返回这个值.
例子:563. 二叉树的坡度
分析
1. 自底向上返回子树值之和,然后求出对应的坡度,累加起来即可.
2. 需要注意的是,左右子树的累加值大小不确定,需要用绝对值
3. 时间复杂度 ${O(N)}$
var findTilt = function (root) {
let ret = 0;
const recursion = (root) => {
if (!root) return 0;
const left = recursion(root.left);
const right = recursion(root.right);
ret += Math.abs(left - right);
return left + right + root.val;
};
recursion(root);
return ret;
};
对于后序遍历
而言,是想遍历到叶子节点,然后再向上去处理根节点,也就是所谓的 自底向上
;
实际上,自底向上
是一种递归的方法,先递
到叶子节点,处理完返回一定的值,再归
回来,后续的处理都是根据子树的值作为入参的,所以不要被 遍历
迷惑,后续遍历
可不是遍历完就结束了,那才刚刚开始呢。
所以后面为了区分,在处理自底向上
题目的时候,函数名字都不再使用 dfs,而是直接使用 recursion ;
例子:
先来解释一下,在做 dfs 遍历的时候,我们需要遍历到叶子节点,然后做最终的处理,有的题目我们看到的是判 null
时返回 null/0 等;有的时候我们直接判断是否叶子节点,if(!root.left && !root.right)
;
这是在刷题过程中感觉忒迷惑的地方,在最开始的时候,我喜欢使用 null ,因为它写的更少,而且顺便把根节点为空的边界也做了,最近刷的时候我开始觉得判断节点
会更稳妥一点,而且不用做更深的处理,直到我再写上面的文字时,有那么一点想法
在我们使用自底向上
的时候,因为需要从子节点中 return 值,这个时候即便是 null 也是有用
的,所以使用 null
基本是 OK 的。
例子: 104. 二叉树的最大深度
/** * 1. 自顶向下,带个层数参数,判定为叶子节点就进行最大值判断 */
var maxDepth = function (root) {
if (!root) return 0;
let ret = 0;
const dfs = (root, depth) => {
if (root.left) dfs(root.left, depth + 1);
if (root.right) dfs(root.right, depth + 1);
ret = Math.max(ret, depth);
return;
};
dfs(root, 1);
return ret;
};
// 自低向上
var maxDepth = function (root) {
const dfs = (root) => {
if (!root) return 0;
return Math.max(dfs(root.left), dfs(root.right))+1;
};
return dfs(root);
};
而在一些携带数据,自顶向下
求值的题目中,如果跑到 null
才结束遍历,就比较容易出现重复计算的错误,而且由于不需要获取 return 值,这个时候我建议是使用判断节点
的方法。
例子:1022. 从根到叶的二进制数之和
/** * @分析 * 1. 自顶向下求出每一条路当前对应的数字,保存在入参中 * 2. 在叶子节点处将值累加起来即可 * 3. 需要注意的是,要在叶子节点就处理,而不是在 null 的时候处理,不然会重复计算 */
var sumRootToLeaf = function(root) {
// if(!root) return 0 //题目已知节点是 1-1000
let ret = 0
const dfs = (root,sum) => {
const temp = (sum<<1) + root.val
if(!root.left && !root.right){
ret +=temp
return
}
if(root.left) dfs(root.left,temp)
if(root.right) dfs(root.right,temp)
}
dfs(root,0)
return ret
};
分析
// 101. 对称二叉树
var isSymmetric = function (root) {
if (!root) return false;
const dfs = (left, right) => {
if (!left && !right) return true;
if (!left || !right || left.val !== right.val) return false;
return dfs(left.left, right.right) && dfs(left.right, right.left);
};
return dfs(root.left, root.right);
};
// 104. 二叉树的最大深度
/** * 1.无论是深度,层数等,直接用层序遍历找到最后一层的最后一个叶子节点即可 */
var maxDepth = function (root) {
if (!root) return 0;
let ret = 0;
const queue = [];
queue.push(root);
while (queue.length) {
ret++; // 进入一层
let len = queue.length;
while (len--) {
// 层序遍历
const root = queue.shift();
if (root.left) queue.push(root.left);
if (root.right) queue.push(root.right);
}
}
return ret;
};
/** * 1. 自顶向上,带个层数参数,判定为叶子节点就进行最大值判断 */
var maxDepth = function (root) {
if (!root) return 0;
let ret = 0;
const dfs = (root, depth) => {
if (root.left) dfs(root.left, depth + 1);
if (root.right) dfs(root.right, depth + 1);
// 走到这的时候,证明是叶子节点了,所以取最大值,就结束这一次的
ret = Math.max(ret, depth);
};
dfs(root, 1);
return ret;
};
// 自低向上
var maxDepth = function (root) {
const recursion = (root) => {
// 只是到了底部,所以高度为 0
if (!root) return 0;
// 每一个节点的高度是多少,就是两个节点树的最大高度+自己所处的这一层1
return Math.max(recursion(root.left), recursion(root.right)) + 1;
};
return recursion(root);
};
分析 – 自底向上
// 226. 翻转二叉树
var invertTree = function (root) {
const dfs = (root) => {
// 到达了最底部,直接返回 null
if (!root) return null;
// 1.递归获取翻转后的左右子树
const left = dfs(root.left);
const right = dfs(root.right);
// 2.反转两棵树的位置
root.left = right;
root.right = left;
// 最后返回这个反转之后的树
return root;
};
return dfs(root);
};
分析
var findTilt = function (root) {
let ret = 0;
const recursion = (root) => {
if (!root) return 0;
const left = recursion(root.left);
const right = recursion(root.right);
ret += Math.abs(left - right);
return left + right + root.val;
};
recursion(root);
return ret;
};
分析
var sumRootToLeaf = function(root) {
// if(!root) return 0 //题目已知节点是 1-1000
let ret = 0
const dfs = (root,sum) => {
const temp = (sum<<1) + root.val
if(!root.left && !root.right){
ret +=temp
return
}
if(root.left) dfs(root.left,temp)
if(root.right) dfs(root.right,temp)
}
dfs(root,0)
return ret
};
分析
var minDiffInBST = function(root) {
let ret = Infinity
let prev = undefined // 保存上一个值
const dfs = (root) => {
if(!root) return
dfs(root.left)
// 在这里处理
if(prev === undefined){
// 第一个值,由于差值需要两个值,所以这相当于初始化了
prev = root.val
}else{
ret = Math.min(ret,root.val-prev)
prev = root.val
}
dfs(root.right)
}
dfs(root)
return ret
};
分析 – 基于完全二叉树的特性
// 662. 二叉树最大宽度
var widthOfBinaryTree = function (root) {
const queue = [];
queue.push([root, 1n]);
let max = 0;
let steps = 0,
curDepth = 0; // 这是用来确定第一个节点的
let leftPos = 0n; // 每一次左侧节点的位置
while (queue.length) {
// 层数+1
steps++;
// 如果还有层
let len = queue.length;
while (len--) {
// 开始一层的遍历了
const [root, pos] = queue.shift(); // 取出节点
if (root) {
// 只要有子节点,那么即便有一个不存在,也得放到队列中
queue.push([root.left, 2n * pos]);
queue.push([root.right, 2n * pos + 1n]);
if (curDepth !== steps) {
// 第一个节点
curDepth = steps; // 这个时候更新一下深度
leftPos = pos; // 左侧节点的位置
}
// 每一个存在的节点,都会不断进行更新
// 由于 bigInt 和 number 是不能进行数学运算的,所以先将 bigint 转成字符串类型,然后隐式转成数字,然后进行比较
max = Math.max(max, (pos - leftPos + 1n).toString());
}
}
}
return max;
};
分析:
var flipMatchVoyage = function(root, voyage) {
if(root.val !== voyage[0]) return [-1] // 用来在进入 dfs 前对根节点的判断
const ret = []
let pos = 0 // 这是用来获取 voyage 值,也是遍历树 V 的,如果没有走完,证明无法匹配 root
const dfs = root => {
// 每一次的遍历 pos 都要跟随着
pos++
// 对于每一个节点,都是按照先序遍历的写法
if(root.left && root.left.val === voyage[pos] ){
// 如果在这节点左树适配,那就继续走,因为这是先序遍历
dfs(root.left)
}
if(root.right && root.right.val === voyage[pos] ){
dfs(root.right)
// 右树完成之后,需要看看现在 pos 所在的值是否可以匹配左树,即是否先走右树再走左树,成立即当前的 root 节点就是需要进行翻转的节点
if(root.left && root.left.val === voyage[pos] ){
ret.push(root.val)
dfs(root.left)
}
}
}
dfs(root)
if(pos<voyage.length){
// voyage 还没有走完,就被限制条件卡住了
return [-1]
}
return ret
};
分析
找到距离根节点 K 的节点
,是不是一下就可以找到,从根节点出发,走 K 步就好了找出距离节点 target K 的子节点
,那么也一样,就是只能从子节点中去找,这道题之所以能成为 medium,就是因为它的 target 不一定是根节点,同时它可以往上去找;var distanceK = function (root, target, k) {
let targetNode = undefined;
// 第一个 dfs ,是为了在 根节点 -> target 之间的的节点打上 parent 的指针,方便从下往上找
const setDfs = (root) => {
if (root === target) {
// 找到了, 就让剩下的搜索停止
targetNode = root;
}
if (root.left && !targetNode) {
root.left.parent = root;
setDfs(root.left);
}
if (root.right && !targetNode) {
root.right.parent = root;
setDfs(root.right);
}
};
setDfs(root);
const ret = [];
const paths = []; // 向上取父节点时,走过节点
// 从上往下去找, 其中 index 表示距离 target 的距离
const find = (root, index) => {
if (index === k) {
ret.push(root.val);
}
if (index < k) {
if (root.left && !paths[root.left.val]) find(root.left, index + 1);
if (root.right && !paths[root.right.val]) find(root.right, index + 1);
}
};
let index = 0;
while (targetNode && index <= k) {
// 记录向上取的父节点
paths[targetNode.val] = targetNode.val;
// 从根节点向下求取合适的值
find(targetNode, index);
targetNode = targetNode.parent;
// 每网上一次,就要将节点走一次
index++;
}
return ret;
};
分析
var inorderSuccessor = function(root, p) {
if(!root) return null
let arr = []
let ret = 0
const dfs = (root) => {
if(!root) return
dfs(root.left)
arr.push(root)
if(root === p) {
ret = arr.length
}
dfs(root.right)
}
dfs(root)
return arr[ret] || null
};
分析
var isValidBST = function(root) {
if(!root) return false
let pre = -Infinity //最小值
let ret = true // 默认就是
const inorder = (root) => {
if(root.left && ret) inorder(root.left)
if(root.val<=pre) {
ret = false // 一旦有一组失败,都不是 BST
return
}else {
pre = root.val
}
if(root.right && ret) inorder(root.right)
}
inorder(root)
return ret
};
分析
// 暴力法 -- 空间复杂度为 ${O(N)}$
var recoverTree = function(root) {
const ret = []
const dfs = (root) => {
if(!root) return
dfs(root.left)
ret.push(root)
dfs(root.right)
}
dfs(root);
// 移动两个值,使得数组 ret 单增
// 另开一个数组 ret2,排序
let sorted = ret.map(item => item.val)
sorted.sort((a,b)=> a-b)
sorted.forEach((sorted,index) => {
if(sorted !== ret[index].val) {
ret[index].val = sorted
}
})
};
直接遍历一次就可以得到节点的数量
var countNodes = function(root) {
let ret = 0
const preorder = root => {
if(!root) return
ret++
preorder(root.left)
preorder(root.right)
}
preorder(root)
return ret
};
分析 – 双 dfs
var pathSum = function (root, sum) {
let ret = 0;
// 遍历节点的 dfs
const dfs = (root) => {
if (!root) return;
find(root, 0);
dfs(root.left);
dfs(root.right);
};
const find = (root, total) => {
total += root;
// if (total > sum) return; // 结束这跳线 -- 这里
if (total === sum) {
// 符合条件
ret++;
// return;
}
if (root.left) find(root.left, total);
if (root.right) find(root.right, total);
};
dfs(root);
return ret;
};
分析:
var sumNumbers = function (root) {
let ret = 0
const dfs = (root,num) => {
let cur = num*10+root.val
if(!root.left && !root.right) {
// 叶子节点 -- 这里判断有节点才走,主要是为了找到叶子节点,而不是到叶子结点下的 null,这样会重复计算
ret+=cur
}
if(root.left) dfs(root.left,cur)
if(root.right) dfs(root.right,cur)
}
dfs(root,0)
return ret
}
分析
var goodNodes = function(root) {
let ret = 0
const dfs = (root,max) => {
if(root.val>=max) {
ret++
max = ret
}
if(root.left) dfs(root.left,max)
if(root.right) dfs(root.right,max)
}
dfs(root,-Infinity)
return ret
};
分析
var pruneTree = function (root) {
const recursion = (root) => {
if (!root) return null; // 到叶子节点的下的 null 了
// 求出左右树
root.left = recursion(root.left);
root.right = recursion(root.right);
if (!root.left && !root.right && root.val === 0) return null; //左右树都为null,且自身值为 0 ,则这课子树减除
return root; //还可以抢救一下
};
return recursion(root);
};
分析
复阳
,然后需要继续删除var removeLeafNodes = function (root, target) {
const postOrder = (root) => {
if (!root) return null;
root.left = postOrder(root.left);
root.right = postOrder(root.right);
// 叶子节点且值等于 target 的时候
if (!root.left && !root.right && root.val === target) return null;
return root;
};
return postOrder(root);
};
分析
var maxAncestorDiff = function (root) {
let ret = 0;
const dfs = (root, min, max) => {
if (!root) return;
ret = Math.max(ret, Math.abs(max - root.val), Math.abs(root.val - min));
min = Math.min(min, root.val);
max = Math.max(max, root.val);
dfs(root.left, min, max);
dfs(root.right, min, max);
};
// 题目给定最少两个节点,少于两个节点也确实无法进行差值比较
// 所以这里直接初始化的时候,根节点的值作为初始化的路径最大最小值
dfs(root.left, root.val, root.val);
dfs(root.right, root.val, root.val);
return ret;
};
var subtreeWithAllDeepest = function (root) {
let max = 0; // 最大深度
let ret = undefined; // 目标节点
const dfs = (root, depth) => {
// 叶子节点 -- 前序遍历求出最大深度
if (!root) {
max = Math.max(max, depth); //求出最大深度
return depth;
}
const left = dfs(root.left, depth + 1);
const right = dfs(root.right, depth + 1);
// 后序遍历根据左右子树的最大深度是否同时满足 max,判断是否需要替换成更大的子树
if (left === max && right === max) {
// 只有在两子树的最大深度同时等于最大深度的时候,才需置换节点
ret = root;
}
// 后序遍历完了,到达根节点
return Math.max(left, right); //返回的是当前节点子树中的的最大深度
};
dfs(root, 0);
return ret;
};
最大的深度
以及对应的最小子树
,最大的深度
以及对应的最小子树
,就可以不需要额外的变量了var subtreeWithAllDeepest = function (root) {
const dfs = (root, depth) => {
if (!root) return [root, depth];
const [lr, ld] = dfs(root.left, depth + 1); // lr -- left root, ld -- left depth
const [rr, rd] = dfs(root.right, depth + 1);
if (ld === rd) return [root, ld]; // 如果左右树的最大值相同,即最大深度节点两边都有,所以要更新一下最小树节点
if (ld > rd) return [lr, ld];
if (ld < rd) return [rr, rd];
};
return dfs(root, 0);
};
分析
var countPairs = function (root, distance) {
const ret = 0;
const dfs = (root) => {
if (!root) return [];
if (!root.left && !root.right) return [0]; // 叶子节点
// 求出叶子节点到当前节点的距离
const left = dfs(root.left).map((i) => i + 1);
const right = dfs(root.right).map((i) => i + 1);
// 然后找出所有小于 dis 的节点对
for (let l of left) {
for (let r of right) {
if (l + r <= distance) ret++;
}
}
// 将叶子节点合起来返回回去
return [...left, ...right];
};
dfs(root);
return ret;
};
分析
满二叉树
,只是对应的节点数 n 有所区别而言 – 换句话说,对于每一个子节点,都要进行一次构建满二叉树,知道边界为止后续遍历
的方式遍历到边界条件处,然后开始进行处理;var allPossibleFBT = function (n) {
const recursion = (n) => {
if (n % 2 === 0) return []; // 偶数
if (n === 1) return [new TreeNode(0)];
const ret = []; // 保存当前节点下,所有满足`满二叉树`情况的节点
for (let i = 0; i < n; i++) {
const left_num = i,
right_num = n - i - 1; // 之所以再减去 1 个,因为根节点占据了 1
// 构建左树的满二叉树
const lefts = recursion(left_num);
const rights = recursion(right_num);
if (lefts && rights) {
// 必须同时存在的时候,才是满的;要不都没有
for (let l of lefts) {
for (let r of rights) {
const root = new TreeNode(0);
root.left = l;
root.right = r;
ret.push(root);
}
}
}
}
return ret;
};
return recursion(n);
};
var numTrees = function (n) {
const recursion = (n) => {
if (n === 0) return 1;
if (n === 1) return 1;
let temp = 0;
for (let i = 1; i <= n; i++) {
const l = i - 1,
r = n - i;
const left = recursion(l);
const right = recursion(r);
temp += left * right;
}
return temp;
};
return recursion(n);
};
var numTrees = function (n) {
const dp = new Array(n + 1).fill(0);
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
for (let j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
return dp[n];
};
分析
var pathSum = function (root, targetSum) {
let ret = 0;
const inner = (root, sum) => {
const temp = sum + root.val;
if (temp === targetSum) ret++;
if (root.left) inner(root.left, temp);
if (root.right) inner(root.right, temp);
};
const outer = (root) => {
if (!root) return;
inner(root, 0);
outer(root.left);
outer(root.right);
};
outer(root);
return ret;
};
分析
同行同列相同时,才会将值从小到大排序
; var verticalTraversal = function (root) {
const ret = []
const queue = []
const map = new Map(); // 总的存储不同垂序位置的数组
queue.push([root,0]) // 第一个参数的节点,第二个参数是垂序距离 -- 这里以根节点为 0
while(queue.length) {
let len = queue .length
const tempMap = new Map() // 每一层的临时 map
while(len--){
// 进入每一层处理
const [root,index] = queue.shift()
if(root.left) queue.push([root.left,index-1])
if(root.right) queue.push([root.right,index+1])
// 处理当前节点的存放位置
if(tempMap.has(index)){
tempMap.set(index,tempMap.get(index).concat(root.val))
}else{
tempMap.set(index,[root.val])
}
}
for(let [index,val] of tempMap.entries()){
val.sort((a,b) =>a-b)
if(map.has(index)){
map.set(index,map.get(index).concat(val))
}else{
map.set(index,val)
}
}
}
// 处理完了
return [...map.keys()].sort((a,b) => a-b).map(key => map.get(key))
}
var serialize = function (root) {
if (!root) return "";
let ret = "";
const queue = [];
queue.push(root);
while (queue.length) {
let len = queue.length;
let isNull = true; // 确定一下这一层是不是全是 null,如果是,那么就要结束了
let str = "";
while (len--) {
const root = queue.shift();
if (!root) {
// 因为在反序列化的时候,你可不知道当前一层对应的节点的位置在哪里,所以只能用 null 来做占位符了
str += "null"+ ",";
} else {
isNull = false;
str += root.val + ",";
queue.push(root.left);
queue.push(root.right);
}
}
// 一层遍历完了
if (isNull) {
// 这一层都是 null,所以结束了
return ret.substr(0,ret.length-1);
} else {
ret += str; // 将字符串加上
}
}
};
var deserialize = function (data) {
if(!data) return null // 空节点
const nodes = data.split(',') // 切割成数组
const root = new TreeNode(nodes[0]); //根节点
const queue = [] // 队列,用来存储每一层的节点;
queue.push(root)
let index = 0; // 当前节点的下标
while(index < nodes.length - 2){
const root = queue.shift()
const lv = nodes[index+1]
const rv = nodes[index+2]
if(lv!== 'null') {
const lnode = new TreeNode(lv)
root.left = lnode
queue.push(lnode)
}
if(rv!== 'null') {
const rnode = new TreeNode(rv)
root.right = rnode
queue.push(rnode)
}
index +=2
}
return root
};
分析
根节点
的最大值,这样可以保证衔接上左右子树的最大值,如果其中一课子树的最大值比这个大,那么在后面递归中自然会替代当前值,不需要额外处理var maxPathSum = function(root) {
let max = -Infinity
const dfs = root => {
if(!root) return 0
const l = dfs(root.left)
const r = dfs(root.right)
// 这里的 root.val 不需要和0比较,必须包含根节点,否则无法衔接
// 同时单子树最大值如果更大,会在后面的 dfs 中取代 max
const tempMax = Math.max(l,0)+Math.max(r,0)+root.val
max =Math.max(max,tempMax)
return Math.max(0,l,r)+root.val // 这里的根节点是必须存在的,不然没法衔接上
}
dfs(root)
return max
};
参考题解: leetcode-cn.com/problems/su…
分析
var sumOfDistancesInTree = function(n, edges) {
const graph= new Array().fill(null).map(() => []) // 下标就是根节点的坐标,值就是子节点的坐标数组
for(let [from,to] of edges){
graph[from].push(to)
graph[to].push(from)
}
const distSum = new Array(n).fill(0) // 1. 存储的是子树节点的距离之和
const nodeNum = new Array(n).fill(1) // 存储的是子树总的节点数,最少都有一个
// 这里是自底向上求出每个节点的 distSum 和 nodeNum , 所以用后续遍历
const postOrder = (root,parent) => {
const childs = graph[root]
for(let child of childs) {
if(child === parent) continue // parent 节点就是之前刚走过的节点
postOrder(child,root)
nodeNum[root] += nodeNum[child]
distSum[root] += distSum[child]+nodeNum[child]
}
}
// 用前序遍历更新 distSum, 这个时候 distSum 就变成了全部节点的距离和
const preOrder = (root,parent) => {
const childs = graph[root]
for(let child of childs) {
if(child === parent) continue // parent 节点就是之前刚走过的节点
distSum[child] = distSum[root] - nodeNum[child] + ( n- nodeNum[child])
preOrder(child,root)
}
}
postOrder(0,-1)
preOrder(0,-1)
return distSum
};