这有位大哥,执意让先刷二叉树,培养所谓的框架思维,其实,我刚刚刷完100道题,正在总结题目,觉得也无可厚非:在做一些动态规划的题目时,确实也感受到了,再说这是一种常见的思维,需要特意训练一下:
https://labuladong.github.io/algo/2/20/33/

https://www.yuque.com/wangtengyun/vwapnv/vxe7ay

前序遍历:

1448. 统计二叉树中好节点的数目

  1. var goodNodes = function(root) {
  2. let count = 0;
  3. function dfs(node, preMax) {
  4. if (!node) {
  5. return;
  6. }
  7. if (node.val >= preMax) { // 先判断是先序的操作
  8. count++;
  9. preMax = node.val;
  10. }
  11. dfs(node.left, preMax);
  12. dfs(node.right, preMax);
  13. }
  14. dfs(root, -Infinity);
  15. return count;
  16. };

带着深度depth 的 dfs

104. 二叉树的最大深度

  1. var maxDepth = function(root) {
  2. let maxDepth = 0;
  3. function traverse(node, depth) { // depth 是 node 所在 深度,初始为0
  4. if (node === null) {
  5. if (maxDepth < depth) {
  6. maxDepth = depth;
  7. }
  8. return;
  9. }
  10. traverse(node.left, depth + 1);
  11. traverse(node.right, depth + 1);
  12. }
  13. traverse(root, 0);
  14. return maxDepth;
  15. };

199. 二叉树的右视图

下面的解法是错的,但是想强调的一点是: 技法的熟练,下面解法思路是 1)先只递归最右边的一只 2)然后再找树的最大深度。两者合并就是答案。但是在1)中最右边的不一定就是右视图的上半部分。 所以还是要使用层遍历的手段。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number[]}
  12. */
  13. var rightSideView = function(root) {
  14. let ans = [];
  15. let maxRightDepth = 0;
  16. function dfsRight(node, depth) {
  17. if (!node) {
  18. maxRightDepth = depth;
  19. return;
  20. }
  21. ans.push(node.val); // 前序 or 中序?
  22. dfsRight(node.right, depth + 1);
  23. }
  24. dfsRight(root, 0);
  25. let maxDepth = 0;
  26. let maxDepthPath = [];
  27. function dfs(node, path) {
  28. if (!node) {
  29. return;
  30. }
  31. path.push(node); // 记录本节点
  32. if (maxDepth < path.length) {
  33. maxDepth = path.length;
  34. maxDepthPath = path;
  35. }
  36. dfs(node.left, [...path]);
  37. dfs(node.right, [...path]);
  38. }
  39. dfs(root, []);
  40. if (maxRightDepth < maxDepth) {
  41. let tmp = maxDepthPath.slice(maxRightDepth).map(n => n.val);
  42. return [...ans, ...tmp];
  43. }
  44. return ans;
  45. };

层序遍历轻松解决:
这几天写dfs多了,好的方法考虑少了。

  1. var rightSideView = function(root) {
  2. if (!root) {
  3. return [];
  4. }
  5. let ans = [];
  6. let queue = [root];
  7. while (queue.length > 0) {
  8. let size = queue.length;
  9. ans.push(queue[size - 1].val);
  10. for (let i = 0; i < size; i++) {
  11. let cur = queue.shift();
  12. cur.left && queue.push(cur.left);
  13. cur.right && queue.push(cur.right);
  14. }
  15. }
  16. return ans;
  17. };

543. 二叉树的直径

不是遍历树的思路,而是递归分解问题的思路

  1. var diameterOfBinaryTree = function(root) {
  2. if (root === null) {
  3. return 0;
  4. }
  5. let max = 0;
  6. function getMaxDepth (node) {
  7. if (node === null) {
  8. return 0;
  9. }
  10. let leftDepth = getMaxDepth(node.left);
  11. let rightDepth = getMaxDepth(node.right);
  12. if (max < (leftDepth + rightDepth + 1)) {
  13. max = leftDepth + rightDepth + 1;
  14. }
  15. return Math.max(leftDepth, rightDepth) + 1;
  16. }
  17. getMaxDepth(root);
  18. return max - 1;
  19. };

100. 相同的树

同时遍历两颗树,和使用下标同时遍历两个数组时一样的。

  1. var isSameTree = function(p, q) {
  2. let isDifferent = false;
  3. function walk(node_1, node_2) {
  4. if (isDifferent) {
  5. return;
  6. }
  7. if (node_1 === null && node_2 === null) {
  8. return;
  9. }
  10. if (node_1 && node_2 && (node_1.val === node_2.val)) {
  11. walk(node_1.left, node_2.left);
  12. walk(node_1.right, node_2.right);
  13. } else {
  14. isDifferent = true;
  15. return;
  16. }
  17. walk(node_1.left, node_2.left);
  18. walk(node_1.right, node_2.right);
  19. }
  20. walk(p, q);
  21. return !isDifferent;
  22. };

101. 对称二叉树

企图前序遍历左子树 和 后续遍历右子树 完成对称的检查

  1. var isSymmetric = function(root) {
  2. if (root === null) return true;
  3. const preArray = [];
  4. function pre(node) {
  5. if (node === null) {
  6. return;
  7. }
  8. preArray.push(node.val);
  9. pre(node.left);
  10. pre(node.right);
  11. }
  12. pre(root.left);
  13. const postArray = [];
  14. function post(node) {
  15. if (node === null) {
  16. return;
  17. }
  18. // post(node.left);
  19. // post(node.right);
  20. // 交换顺序,就是逆序了!!
  21. post(node.right);
  22. post(node.left);
  23. postArray.push(node.val);
  24. }
  25. post(root.left);
  26. if (preArray.length !== postArray.length) {
  27. return false;
  28. }
  29. let n = preArray.length;
  30. for (let i = 0; i < Math.floor(n / 2) + 1; i++) {
  31. if (preArray[i] !== postArray[n - i - 1]) {
  32. return false;
  33. }
  34. }
  35. return true;
  36. };

这样的解过不去:
image.png
如同 100 的思路,去同时遍历两颗(子)树:

  1. var isSymmetric = function(root) {
  2. if (root === null) return true;
  3. let isSame = true;
  4. function walk(node_1, node_2) {
  5. if (!isSame) {
  6. return;
  7. }
  8. if (node_1 === null && node_2 === null) {
  9. return;
  10. }
  11. if (node_1 && node_2 && (node_1.val === node_2.val)) {
  12. walk(node_1.left, node_2.right);
  13. walk(node_1.right, node_2.left);
  14. } else {
  15. isSame = false;
  16. return;
  17. }
  18. }
  19. walk(root.left, root.right);
  20. return isSame;
  21. };

226. 翻转二叉树

遍历思路:找到每个节点要做什么 和 以及什么时机做(前序 中序 后序)

  1. var invertTree = function(root) {
  2. function walk(node) {
  3. if (!node) {
  4. return;
  5. }
  6. let tmp = node.left;
  7. node.left = node.right;
  8. node.right = tmp;
  9. walk(node.left);
  10. walk(node.right);
  11. }
  12. walk(root);
  13. return root;
  14. };

划分小问题:

  1. var invertTree = function(root) {
  2. function _invertTree(node) {
  3. if (node === null) {
  4. return null; // 终止条件而非返回
  5. }
  6. // node.left 被先后改变,影响了结果
  7. // node.left = _invertTree(node.right);
  8. // node.right = _invertTree(node.left);
  9. let left = _invertTree(node.right); // 先存下,后赋值
  10. let right = _invertTree(node.left);
  11. node.left = left;
  12. node.right = right;
  13. return node;
  14. }
  15. return _invertTree(root);
  16. };

116. 填充每个节点的下一个右侧节点指针

1)利用 层遍历的套路:

  1. var connect = function(root) {
  2. if (root === null) {
  3. return null;
  4. }
  5. const queue = [];
  6. queue.push(root);
  7. while (queue.length) {
  8. let preNode = null;
  9. let size = queue.length;
  10. for (let i = 0; i < size; i++) {
  11. let curNode = queue.shift();
  12. curNode.left && queue.push(curNode.left);
  13. curNode.right && queue.push(curNode.right);
  14. if (preNode) {
  15. preNode.next = curNode;
  16. }
  17. preNode = curNode;
  18. }
  19. preNode.next = null;
  20. }
  21. return root;
  22. };

但是还有更明确的遍历套路:

  1. // 也是好方法:以下写法层遍历更明确!!!
  2. var connect = function(root) {
  3. if (!root) return null;
  4. let queue = [root];
  5. while(queue.length) {
  6. const nums = queue.length;
  7. for(let i = 0; i < nums; i++) {
  8. let node = queue.shift();
  9. node.left && queue.push(node.left);
  10. node.right && queue.push(node.right);
  11. }
  12. for(let i = 0; i< queue.length - 1; i++) { // 多么明确
  13. queue[i].next = queue[i+1];
  14. }
  15. }
  16. return root;
  17. };

2)很另类,我觉得我学不会
https://labuladong.github.io/algo/2/20/34/
image.png

二叉树路径

236. 二叉树的最近公共祖先

这道题的遍历过程很简单,代码也简单。关键是要抓住题目的限制条件:
image.png
所以有很多推测,对于一个 node如果一个子树 node.left 没有 p 或 q,而另一个子树 node.right 只检测出来有 p 或 q 那么可以说,结果一定在 node.left。

  1. var lowestCommonAncestor = function(root, p, q) {
  2. function walk(node) { // 找到 p 或 q,直接返回
  3. if (node === null || node.val == p.val || node.val === q.val) {
  4. return node;
  5. }
  6. let left = walk(node.left);
  7. let right = walk(node.right);
  8. if (left && right) {
  9. return node;
  10. }
  11. // else if (left && !right) {
  12. // return left;
  13. // } else if (!left && right) {
  14. // return right;
  15. // } else {
  16. // return null;
  17. // }
  18. return left || right;
  19. }
  20. return walk(root);
  21. };
  22. // Tree 相关代码在后面
  23. const root = new Tree([3,5,1,6,2,0,8,null,null,7,4]);
  24. const p = new TreeNode(5);
  25. const q = new TreeNode(1);
  26. lowestCommonAncestor(root, p, q);

235. 二叉搜索树的最近公共祖先 (235 和 236 是一样的题目)

  1. var lowestCommonAncestor = function(root, p, q) {
  2. function walk(node) {
  3. if (!node) {
  4. return;
  5. }
  6. if ([p.val, q.val].includes(node.val)) {
  7. return node;
  8. }
  9. // 在 walk 上加条件判断
  10. let left;
  11. let right;
  12. if (p.val <= node.val && q.val <= node.val) {
  13. left = walk(node.left);
  14. } else if (p.val <= node.val && q.val >= node.val || q.val <= node.val && p.val >= node.val) {
  15. left = walk(node.left);
  16. right = walk(node.right);
  17. } else if (p.val >= node.val && q.val >= node.val) {
  18. right = walk(node.right);
  19. }
  20. if (left && right) {
  21. return node;
  22. }
  23. return left || right;
  24. }
  25. return walk(root);
  26. };

1650. 二叉树的最近公共祖先 III

题目一开始想当然了。题目并没有给出 树的 root ,只给出两个节点而已。
思路也比较简单:节点通过 parent 能找到继承链。
1)先找到一个节点的完整继承链,
2)然后再一步一步找 另一个节点 继承链:每个都比较是否已经出现在另一个节点的继承链中,有就是公共节点。
注意,继承链一开始要把自己包括进去。

  1. var lowestCommonAncestor = function(p, q) {
  2. let visitedOfp = new Set();
  3. while (p) { // 有值才加入
  4. visitedOfp.add(p);
  5. p = p.parent;
  6. }
  7. while(q) {
  8. if (visitedOfp.has(q)) {
  9. return q;
  10. }
  11. q = q.parent;
  12. }
  13. return null;
  14. };

124. 二叉树中的最大路径和

我这个是传统的递归写法,但不是遍历二叉树的标准姿势。
这次我们递归的返回 和 题目要求有关,题目要找最大路径和,那我们就返回某个节点的最大路径。

  1. var maxPathSum = function(root) {
  2. // let max = 0;
  3. let max = root.val;
  4. function walk(node) { // 递归返回的是,包含 node 节点的 “串”。注意:这个串需要和父节点去组成大串!
  5. if (!node) {
  6. // return 0;
  7. return -Infinity; // 不想让别人算到这一支
  8. }
  9. if (!node.left && !node.right) {
  10. return node.val;
  11. }
  12. let leftSum = walk(node.left);
  13. let rightSum = walk(node.right);
  14. let _max = Math.max(node.val, leftSum + node.val, rightSum + node.val); // 带有父节点的最大值
  15. let sum = node.val + leftSum + rightSum;
  16. max = Math.max(max, _max, sum, leftSum, rightSum); // 和 不带父节点的最大值 比较
  17. return _max; // _max 代表的是递归体系
  18. }
  19. walk(root);
  20. return max;
  21. }

112. 路径总和

比 124 简单多了,平时写叶子节点 dfs 的边界处理(有时直接忽略),不需要在遍历过程中使用map记录值,二叉的树遍历,只要前序遍历就可以。我现在对二叉树的理解也慢慢牛逼了。

  1. var hasPathSum = function(root, targetSum) {
  2. if (!root) {
  3. return false;
  4. }
  5. let has = false;
  6. function dfs(node, parentVal) {
  7. if (!node) { // 这里不能完全判断 parent 是叶子节点
  8. return;
  9. }
  10. if (has) { // 优化!!!
  11. return;
  12. }
  13. if (!node.left && !node.right) { // 当前是叶子节点
  14. if (parentVal + node.val === targetSum) {
  15. has = true;
  16. }
  17. }
  18. dfs(node.left, parentVal + node.val);
  19. dfs(node.right, parentVal + node.val);
  20. }
  21. dfs(root, 0);
  22. return has;
  23. };

体验递归:

剑指 Offer 27. 二叉树的镜像

  1. var mirrorTree = function(root) {
  2. if (!root) { // 边界条件1
  3. return root;
  4. }
  5. if (!root.left && !root.right) { // 边界条件2
  6. return root;
  7. }
  8. let right = mirrorTree(root.left);
  9. let left = mirrorTree(root.right);
  10. root.left = left;
  11. root.right = right;
  12. return root;
  13. };

使用递归去试错

递归使用的天花板:使用递归去试错

951. 翻转等价二叉树

  1. var flipEquiv = function(root1, root2) {
  2. if (root1 && root2 && root1.val === root2.val) {
  3. return (flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right))
  4. || (flipEquiv(root1.left, root2.right) && flipEquiv(root1.right, root2.left));
  5. }
  6. if (root1 === root2) { // root1 root2 都是 null
  7. return true;
  8. }
  9. return false;
  10. }

572. 另一棵树的子树

和 951 一样,是用递归去试错的思想

  1. var isSubtree = function(root, subRoot) {
  2. if (root === null && subRoot === null) {
  3. return true;
  4. }
  5. if (!(root && subRoot)) {
  6. return false;
  7. }
  8. if (!root.left && !root.right && !subRoot.left && !subRoot.right && root.val === subRoot.val) {
  9. return true;
  10. }
  11. // 后面的是 root 和 subRoot 同时存在
  12. if (root.val !== subRoot.val) {
  13. return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot)
  14. }
  15. if (isSameTree(root.left, subRoot.left) && isSameTree(root.right, subRoot.right)) {
  16. return true;
  17. }
  18. return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
  19. };
  20. function isSameTree(root1, root2) {
  21. if (!root1 && !root2) {
  22. return true;
  23. }
  24. if (root1 && root2 && root1.val === root2.val) {
  25. return isSameTree(root1.left, root2.left) && isSameTree(root1.right, root2.right);
  26. }
  27. return false;
  28. }

114. 二叉树展开为链表

分解为子问题去解决:细节是魔鬼

  1. var flatten = function(root) {
  2. function _flatten(node) {
  3. if (node === null) {
  4. return [null, null];
  5. }
  6. if (!node.left && !node.right) {
  7. return [node, node];
  8. }
  9. const [leftHead, leftTail] = _flatten(node.left);
  10. const [rightHead, rightTail] = _flatten(node.right);
  11. let _head = null;
  12. let _tail = null;
  13. if (leftTail) { // 连接两个串
  14. leftTail.right = rightHead;
  15. }
  16. _head = leftHead || rightHead;
  17. _tail = rightTail || leftTail; // 细节要注意了
  18. node.right = _head;
  19. node.left = null;
  20. return [node, _tail];
  21. }
  22. _flatten(root);
  23. return root;
  24. };

25. K 个一组翻转链表

114. 二叉树展开为链表 类似的题目。

生成树:

654. 最大二叉树

看看思路解析,就能轻易地想到拆分子问题 递归的方法

  1. var constructMaximumBinaryTree = function(nums) {
  2. if (nums.length === 0) {
  3. return null;
  4. }
  5. if (nums.length === 1) {
  6. return new TreeNode(nums[0]);
  7. }
  8. let max = -Infinity;
  9. let maxIndex = null;
  10. for (let i = 0; i < nums.length; i++) {
  11. let cur = nums[i];
  12. if (max < cur) {
  13. max = cur;
  14. maxIndex = i;
  15. }
  16. }
  17. let node = new TreeNode(max);
  18. node.left = constructMaximumBinaryTree(nums.slice(0, maxIndex));
  19. node.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1));
  20. return node;
  21. };

105. 从前序与中序遍历序列构造二叉树

和 654 一样拆分子问题

  1. var buildTree = function(preorder, inorder) {
  2. if (preorder.length === 1 && inorder.length === 1) {
  3. return new TreeNode(preorder[0]);
  4. }
  5. if (preorder.length === 0 && inorder.length === 0) {
  6. return null;
  7. }
  8. const rootValue = preorder[0];
  9. const rootValueIndex = inorder.indexOf(rootValue);
  10. const root = new TreeNode(rootValue);
  11. root.left = buildTree(preorder.slice(1, rootValueIndex + 1), inorder.slice(0, rootValueIndex));
  12. root.right = buildTree(preorder.slice(rootValueIndex + 1), inorder.slice(rootValueIndex + 1));
  13. return root;
  14. };

106. 从中序与后序遍历序列构造二叉树

完全相同的结构去做的

  1. var buildTree = function(inorder, postorder) {
  2. if (inorder.length === 1 && postorder.length === 1) {
  3. return new TreeNode(inorder[0]);
  4. }
  5. if (inorder.length === 0 && postorder.length === 0) {
  6. return null;
  7. }
  8. const rootValue = postorder[postorder.length - 1];
  9. const rootValueIndex = inorder.indexOf(rootValue);
  10. const root = new TreeNode(rootValue);
  11. root.left = buildTree(inorder.slice(0, rootValueIndex), postorder.slice(0, rootValueIndex));
  12. root.right = buildTree(inorder.slice(rootValueIndex + 1), postorder.slice(rootValueIndex, postorder.length - 1));
  13. return root;
  14. };

小结:

生成树的算法:
1)找到子问题,
2)构建边界条件
3)注意子问题的传值

根据二叉树的层遍历结果,反序列化生成树

前序 和 中序,或 中序 和 后序 都能唯一确定树。
层序 单独就可以确定 唯一的树。

  1. function Tree(arr) { // 两个队列,要想实现宽度遍历,必须使用队列。
  2. let val = arr.shift();
  3. let root = new TreeNode(val);
  4. let queue = [];
  5. queue.push(root);
  6. while (queue.length) {
  7. const cur = queue.shift();
  8. if (arr.length > 0) {
  9. let leftVal = arr.shift();
  10. if (leftVal !== null) {
  11. let leftNode = new TreeNode(leftVal);
  12. cur.left = leftNode;
  13. queue.push(leftNode);
  14. }
  15. } else {
  16. break;
  17. }
  18. if (arr.length > 0) {
  19. let rightVal = arr.shift();
  20. if (rightVal !== null) {
  21. let rightNode = new TreeNode(rightVal);
  22. cur.right = rightNode;
  23. queue.push(rightNode);
  24. }
  25. } else {
  26. break;
  27. }
  28. }
  29. return root;
  30. }

297. 二叉树的序列化与反序列化

  1. var serialize = function(root) {
  2. // 先序遍历输出
  3. function pre(node) {
  4. if (!node) {
  5. return 'None'; // 也不知是什么狗屁
  6. }
  7. let leftStr = pre(node.left);
  8. let rightStr = pre(node.right);
  9. return node.val + ',' + leftStr + ',' + rightStr;
  10. }
  11. return pre(root);
  12. };
  13. /**
  14. * Decodes your encoded data to tree.
  15. *
  16. * @param {string} data
  17. * @return {TreeNode}
  18. */
  19. var deserialize = function(data) { // 这个反序列化 的递归很有思思,可谓是经典吧
  20. const list = data.split(',');
  21. function dfs(list) {
  22. let cur = list.shift();
  23. if (cur === 'None') {
  24. return null;
  25. }
  26. let newNode = new TreeNode(cur);
  27. newNode.left = dfs(list);
  28. newNode.right = dfs(list);
  29. return newNode;
  30. }
  31. return dfs(list);
  32. };

层序遍历的模板

专门讲后序

为什么专门讲后序?感觉不凡呢。
image.png
后序 去解决“子树”问题。

652. 寻找重复的子树

不算什么高见:
https://mp.weixin.qq.com/s/LJbpo49qppIeRs-FbgjsSQ
自己写一版吧,练练手:

  1. var findDuplicateSubtrees = function(root) {
  2. let result = [];
  3. let map = new Map(); // key: 子树遍历的后序结果字符串,value 为 子树的根节点, value 改为 次数
  4. function walk(node) {
  5. if (node === null) {
  6. return 'none';
  7. }
  8. let leftVal = walk(node.left);
  9. let rightVal = walk(node.right);
  10. let key = `${leftVal},${rightVal},${node.val}`;
  11. if (map.has(key)) {
  12. if (map.get(key) === 1) { // 只想记录第二次 相同的 key,第三次不记录了
  13. result.push(node);
  14. }
  15. map.set(key, map.get(key) + 1)
  16. } else {
  17. map.set(key, 1);
  18. }
  19. return key;
  20. }
  21. walk(root);
  22. return result;
  23. };

很容易就写出来了,后序 在 写 子树 确实简单很多。

归并排序其实后序遍历

逻辑阐述能力,也非常重要。所以要讲清楚自己的思考过程。