这有位大哥,执意让先刷二叉树,培养所谓的框架思维,其实,我刚刚刷完100道题,正在总结题目,觉得也无可厚非:在做一些动态规划的题目时,确实也感受到了,再说这是一种常见的思维,需要特意训练一下:
https://labuladong.github.io/algo/2/20/33/
https://www.yuque.com/wangtengyun/vwapnv/vxe7ay
前序遍历:
1448. 统计二叉树中好节点的数目
var goodNodes = function(root) {let count = 0;function dfs(node, preMax) {if (!node) {return;}if (node.val >= preMax) { // 先判断是先序的操作count++;preMax = node.val;}dfs(node.left, preMax);dfs(node.right, preMax);}dfs(root, -Infinity);return count;};
带着深度depth 的 dfs
104. 二叉树的最大深度
var maxDepth = function(root) {let maxDepth = 0;function traverse(node, depth) { // depth 是 node 所在 深度,初始为0if (node === null) {if (maxDepth < depth) {maxDepth = depth;}return;}traverse(node.left, depth + 1);traverse(node.right, depth + 1);}traverse(root, 0);return maxDepth;};
199. 二叉树的右视图
下面的解法是错的,但是想强调的一点是: 技法的熟练,下面解法思路是 1)先只递归最右边的一只 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 {TreeNode} root* @return {number[]}*/var rightSideView = function(root) {let ans = [];let maxRightDepth = 0;function dfsRight(node, depth) {if (!node) {maxRightDepth = depth;return;}ans.push(node.val); // 前序 or 中序?dfsRight(node.right, depth + 1);}dfsRight(root, 0);let maxDepth = 0;let maxDepthPath = [];function dfs(node, path) {if (!node) {return;}path.push(node); // 记录本节点if (maxDepth < path.length) {maxDepth = path.length;maxDepthPath = path;}dfs(node.left, [...path]);dfs(node.right, [...path]);}dfs(root, []);if (maxRightDepth < maxDepth) {let tmp = maxDepthPath.slice(maxRightDepth).map(n => n.val);return [...ans, ...tmp];}return ans;};
层序遍历轻松解决:
这几天写dfs多了,好的方法考虑少了。
var rightSideView = function(root) {if (!root) {return [];}let ans = [];let queue = [root];while (queue.length > 0) {let size = queue.length;ans.push(queue[size - 1].val);for (let i = 0; i < size; i++) {let cur = queue.shift();cur.left && queue.push(cur.left);cur.right && queue.push(cur.right);}}return ans;};
543. 二叉树的直径
不是遍历树的思路,而是递归分解问题的思路
var diameterOfBinaryTree = function(root) {if (root === null) {return 0;}let max = 0;function getMaxDepth (node) {if (node === null) {return 0;}let leftDepth = getMaxDepth(node.left);let rightDepth = getMaxDepth(node.right);if (max < (leftDepth + rightDepth + 1)) {max = leftDepth + rightDepth + 1;}return Math.max(leftDepth, rightDepth) + 1;}getMaxDepth(root);return max - 1;};
100. 相同的树
同时遍历两颗树,和使用下标同时遍历两个数组时一样的。
var isSameTree = function(p, q) {let isDifferent = false;function walk(node_1, node_2) {if (isDifferent) {return;}if (node_1 === null && node_2 === null) {return;}if (node_1 && node_2 && (node_1.val === node_2.val)) {walk(node_1.left, node_2.left);walk(node_1.right, node_2.right);} else {isDifferent = true;return;}walk(node_1.left, node_2.left);walk(node_1.right, node_2.right);}walk(p, q);return !isDifferent;};
101. 对称二叉树
企图前序遍历左子树 和 后续遍历右子树 完成对称的检查
var isSymmetric = function(root) {if (root === null) return true;const preArray = [];function pre(node) {if (node === null) {return;}preArray.push(node.val);pre(node.left);pre(node.right);}pre(root.left);const postArray = [];function post(node) {if (node === null) {return;}// post(node.left);// post(node.right);// 交换顺序,就是逆序了!!post(node.right);post(node.left);postArray.push(node.val);}post(root.left);if (preArray.length !== postArray.length) {return false;}let n = preArray.length;for (let i = 0; i < Math.floor(n / 2) + 1; i++) {if (preArray[i] !== postArray[n - i - 1]) {return false;}}return true;};
这样的解过不去:
如同 100 的思路,去同时遍历两颗(子)树:
var isSymmetric = function(root) {if (root === null) return true;let isSame = true;function walk(node_1, node_2) {if (!isSame) {return;}if (node_1 === null && node_2 === null) {return;}if (node_1 && node_2 && (node_1.val === node_2.val)) {walk(node_1.left, node_2.right);walk(node_1.right, node_2.left);} else {isSame = false;return;}}walk(root.left, root.right);return isSame;};
226. 翻转二叉树
遍历思路:找到每个节点要做什么 和 以及什么时机做(前序 中序 后序)
var invertTree = function(root) {function walk(node) {if (!node) {return;}let tmp = node.left;node.left = node.right;node.right = tmp;walk(node.left);walk(node.right);}walk(root);return root;};
划分小问题:
var invertTree = function(root) {function _invertTree(node) {if (node === null) {return null; // 终止条件而非返回}// node.left 被先后改变,影响了结果// node.left = _invertTree(node.right);// node.right = _invertTree(node.left);let left = _invertTree(node.right); // 先存下,后赋值let right = _invertTree(node.left);node.left = left;node.right = right;return node;}return _invertTree(root);};
116. 填充每个节点的下一个右侧节点指针
1)利用 层遍历的套路:
var connect = function(root) {if (root === null) {return null;}const queue = [];queue.push(root);while (queue.length) {let preNode = null;let size = queue.length;for (let i = 0; i < size; i++) {let curNode = queue.shift();curNode.left && queue.push(curNode.left);curNode.right && queue.push(curNode.right);if (preNode) {preNode.next = curNode;}preNode = curNode;}preNode.next = null;}return root;};
但是还有更明确的遍历套路:
// 也是好方法:以下写法层遍历更明确!!!var connect = function(root) {if (!root) return null;let queue = [root];while(queue.length) {const nums = queue.length;for(let i = 0; i < nums; i++) {let node = queue.shift();node.left && queue.push(node.left);node.right && queue.push(node.right);}for(let i = 0; i< queue.length - 1; i++) { // 多么明确queue[i].next = queue[i+1];}}return root;};
2)很另类,我觉得我学不会
https://labuladong.github.io/algo/2/20/34/
二叉树路径
236. 二叉树的最近公共祖先
这道题的遍历过程很简单,代码也简单。关键是要抓住题目的限制条件:
所以有很多推测,对于一个 node如果一个子树 node.left 没有 p 或 q,而另一个子树 node.right 只检测出来有 p 或 q 那么可以说,结果一定在 node.left。
var lowestCommonAncestor = function(root, p, q) {function walk(node) { // 找到 p 或 q,直接返回if (node === null || node.val == p.val || node.val === q.val) {return node;}let left = walk(node.left);let right = walk(node.right);if (left && right) {return node;}// else if (left && !right) {// return left;// } else if (!left && right) {// return right;// } else {// return null;// }return left || right;}return walk(root);};// Tree 相关代码在后面const root = new Tree([3,5,1,6,2,0,8,null,null,7,4]);const p = new TreeNode(5);const q = new TreeNode(1);lowestCommonAncestor(root, p, q);
235. 二叉搜索树的最近公共祖先 (235 和 236 是一样的题目)
var lowestCommonAncestor = function(root, p, q) {function walk(node) {if (!node) {return;}if ([p.val, q.val].includes(node.val)) {return node;}// 在 walk 上加条件判断let left;let right;if (p.val <= node.val && q.val <= node.val) {left = walk(node.left);} else if (p.val <= node.val && q.val >= node.val || q.val <= node.val && p.val >= node.val) {left = walk(node.left);right = walk(node.right);} else if (p.val >= node.val && q.val >= node.val) {right = walk(node.right);}if (left && right) {return node;}return left || right;}return walk(root);};
1650. 二叉树的最近公共祖先 III
题目一开始想当然了。题目并没有给出 树的 root ,只给出两个节点而已。
思路也比较简单:节点通过 parent 能找到继承链。
1)先找到一个节点的完整继承链,
2)然后再一步一步找 另一个节点 继承链:每个都比较是否已经出现在另一个节点的继承链中,有就是公共节点。
注意,继承链一开始要把自己包括进去。
var lowestCommonAncestor = function(p, q) {let visitedOfp = new Set();while (p) { // 有值才加入visitedOfp.add(p);p = p.parent;}while(q) {if (visitedOfp.has(q)) {return q;}q = q.parent;}return null;};
124. 二叉树中的最大路径和
我这个是传统的递归写法,但不是遍历二叉树的标准姿势。
这次我们递归的返回 和 题目要求有关,题目要找最大路径和,那我们就返回某个节点的最大路径。
var maxPathSum = function(root) {// let max = 0;let max = root.val;function walk(node) { // 递归返回的是,包含 node 节点的 “串”。注意:这个串需要和父节点去组成大串!if (!node) {// return 0;return -Infinity; // 不想让别人算到这一支}if (!node.left && !node.right) {return node.val;}let leftSum = walk(node.left);let rightSum = walk(node.right);let _max = Math.max(node.val, leftSum + node.val, rightSum + node.val); // 带有父节点的最大值let sum = node.val + leftSum + rightSum;max = Math.max(max, _max, sum, leftSum, rightSum); // 和 不带父节点的最大值 比较return _max; // _max 代表的是递归体系}walk(root);return max;}
112. 路径总和
比 124 简单多了,平时写叶子节点 dfs 的边界处理(有时直接忽略),不需要在遍历过程中使用map记录值,二叉的树遍历,只要前序遍历就可以。我现在对二叉树的理解也慢慢牛逼了。
var hasPathSum = function(root, targetSum) {if (!root) {return false;}let has = false;function dfs(node, parentVal) {if (!node) { // 这里不能完全判断 parent 是叶子节点return;}if (has) { // 优化!!!return;}if (!node.left && !node.right) { // 当前是叶子节点if (parentVal + node.val === targetSum) {has = true;}}dfs(node.left, parentVal + node.val);dfs(node.right, parentVal + node.val);}dfs(root, 0);return has;};
剑指 Offer 27. 二叉树的镜像
var mirrorTree = function(root) {if (!root) { // 边界条件1return root;}if (!root.left && !root.right) { // 边界条件2return root;}let right = mirrorTree(root.left);let left = mirrorTree(root.right);root.left = left;root.right = right;return root;};
使用递归去试错
951. 翻转等价二叉树
var flipEquiv = function(root1, root2) {if (root1 && root2 && root1.val === root2.val) {return (flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right))|| (flipEquiv(root1.left, root2.right) && flipEquiv(root1.right, root2.left));}if (root1 === root2) { // root1 root2 都是 nullreturn true;}return false;}
572. 另一棵树的子树
和 951 一样,是用递归去试错的思想
var isSubtree = function(root, subRoot) {if (root === null && subRoot === null) {return true;}if (!(root && subRoot)) {return false;}if (!root.left && !root.right && !subRoot.left && !subRoot.right && root.val === subRoot.val) {return true;}// 后面的是 root 和 subRoot 同时存在if (root.val !== subRoot.val) {return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot)}if (isSameTree(root.left, subRoot.left) && isSameTree(root.right, subRoot.right)) {return true;}return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);};function isSameTree(root1, root2) {if (!root1 && !root2) {return true;}if (root1 && root2 && root1.val === root2.val) {return isSameTree(root1.left, root2.left) && isSameTree(root1.right, root2.right);}return false;}
114. 二叉树展开为链表
分解为子问题去解决:细节是魔鬼
var flatten = function(root) {function _flatten(node) {if (node === null) {return [null, null];}if (!node.left && !node.right) {return [node, node];}const [leftHead, leftTail] = _flatten(node.left);const [rightHead, rightTail] = _flatten(node.right);let _head = null;let _tail = null;if (leftTail) { // 连接两个串leftTail.right = rightHead;}_head = leftHead || rightHead;_tail = rightTail || leftTail; // 细节要注意了node.right = _head;node.left = null;return [node, _tail];}_flatten(root);return root;};
25. K 个一组翻转链表
和 114. 二叉树展开为链表 类似的题目。
生成树:
654. 最大二叉树
看看思路解析,就能轻易地想到拆分子问题 递归的方法
var constructMaximumBinaryTree = function(nums) {if (nums.length === 0) {return null;}if (nums.length === 1) {return new TreeNode(nums[0]);}let max = -Infinity;let maxIndex = null;for (let i = 0; i < nums.length; i++) {let cur = nums[i];if (max < cur) {max = cur;maxIndex = i;}}let node = new TreeNode(max);node.left = constructMaximumBinaryTree(nums.slice(0, maxIndex));node.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1));return node;};
105. 从前序与中序遍历序列构造二叉树
和 654 一样拆分子问题
var buildTree = function(preorder, inorder) {if (preorder.length === 1 && inorder.length === 1) {return new TreeNode(preorder[0]);}if (preorder.length === 0 && inorder.length === 0) {return null;}const rootValue = preorder[0];const rootValueIndex = inorder.indexOf(rootValue);const root = new TreeNode(rootValue);root.left = buildTree(preorder.slice(1, rootValueIndex + 1), inorder.slice(0, rootValueIndex));root.right = buildTree(preorder.slice(rootValueIndex + 1), inorder.slice(rootValueIndex + 1));return root;};
106. 从中序与后序遍历序列构造二叉树
完全相同的结构去做的
var buildTree = function(inorder, postorder) {if (inorder.length === 1 && postorder.length === 1) {return new TreeNode(inorder[0]);}if (inorder.length === 0 && postorder.length === 0) {return null;}const rootValue = postorder[postorder.length - 1];const rootValueIndex = inorder.indexOf(rootValue);const root = new TreeNode(rootValue);root.left = buildTree(inorder.slice(0, rootValueIndex), postorder.slice(0, rootValueIndex));root.right = buildTree(inorder.slice(rootValueIndex + 1), postorder.slice(rootValueIndex, postorder.length - 1));return root;};
小结:
生成树的算法:
1)找到子问题,
2)构建边界条件
3)注意子问题的传值
根据二叉树的层遍历结果,反序列化生成树
前序 和 中序,或 中序 和 后序 都能唯一确定树。
层序 单独就可以确定 唯一的树。
function Tree(arr) { // 两个队列,要想实现宽度遍历,必须使用队列。let val = arr.shift();let root = new TreeNode(val);let queue = [];queue.push(root);while (queue.length) {const cur = queue.shift();if (arr.length > 0) {let leftVal = arr.shift();if (leftVal !== null) {let leftNode = new TreeNode(leftVal);cur.left = leftNode;queue.push(leftNode);}} else {break;}if (arr.length > 0) {let rightVal = arr.shift();if (rightVal !== null) {let rightNode = new TreeNode(rightVal);cur.right = rightNode;queue.push(rightNode);}} else {break;}}return root;}
297. 二叉树的序列化与反序列化
var serialize = function(root) {// 先序遍历输出function pre(node) {if (!node) {return 'None'; // 也不知是什么狗屁}let leftStr = pre(node.left);let rightStr = pre(node.right);return node.val + ',' + leftStr + ',' + rightStr;}return pre(root);};/*** Decodes your encoded data to tree.** @param {string} data* @return {TreeNode}*/var deserialize = function(data) { // 这个反序列化 的递归很有思思,可谓是经典吧const list = data.split(',');function dfs(list) {let cur = list.shift();if (cur === 'None') {return null;}let newNode = new TreeNode(cur);newNode.left = dfs(list);newNode.right = dfs(list);return newNode;}return dfs(list);};
层序遍历的模板
专门讲后序
652. 寻找重复的子树
不算什么高见:
https://mp.weixin.qq.com/s/LJbpo49qppIeRs-FbgjsSQ
自己写一版吧,练练手:
var findDuplicateSubtrees = function(root) {let result = [];let map = new Map(); // key: 子树遍历的后序结果字符串,value 为 子树的根节点, value 改为 次数function walk(node) {if (node === null) {return 'none';}let leftVal = walk(node.left);let rightVal = walk(node.right);let key = `${leftVal},${rightVal},${node.val}`;if (map.has(key)) {if (map.get(key) === 1) { // 只想记录第二次 相同的 key,第三次不记录了result.push(node);}map.set(key, map.get(key) + 1)} else {map.set(key, 1);}return key;}walk(root);return result;};
很容易就写出来了,后序 在 写 子树 确实简单很多。
归并排序其实后序遍历
逻辑阐述能力,也非常重要。所以要讲清楚自己的思考过程。

