543. 二叉树的直径
乍看是 根节点的 左子树最大高度 + 右子树最大高度 + 1 (如图一)
但其实不能这样,因为路径可能并不经过根节点(如图二)
因此要用一个 max 来保存最后的最大路径和
在求二叉树高度的递归中,在每个根节点(在每次递归中),比较 max 与 以这个当前根节点的 左子树最大高度 + 右子树最大高度 + 1
图一
图二
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static int maxDiameter = 0; public Solution() { maxDiameter = 0; } public int diameterOfBinaryTree(TreeNode root) { dfs(root); // 在递归求树高的过程中求出来的 maxDiameter return maxDiameter; } private int dfs(TreeNode nowRoot) { if (nowRoot == null) { return 0; } int leftHeight = dfs(nowRoot.left); int rightHeight = dfs(nowRoot.right); // 路径长度最大值为左子树 + 右子树高度,与之前的最大值比较 maxDiameter = Math.max(leftHeight + rightHeight, maxDiameter); // 因为要通过递归求树高度,所以最后仍要返回高度:左右子树中较高的那个(而不是路径和) return Math.max(leftHeight, rightHeight) + 1; } }
124. 二叉树中的最大路径和
最大路径怎么求?
最后的最大路径,任一节点 都有可能作为这个最大路径的根节点
所以有一个全局的最大路径 maxPathSum, 每次递归的时候(在每个根节点)都要与之比较
又如果当前节点作为 最后最大路径的根节点,那么 只当前根节点/当前根节点+左子树/当前根节点+右子树/当前根节点+左子树+右子树 这四种情况都行
所以,要取这四种情况中最大的来与 maxPathSum 比较,这四种一定会包含当前根节点,所以四种中最大的求法就是:初始值为当前根节点的值,左子树>0 就加左子树,右子树>0 就加右子树
递归的返回值应该是什么?
之前比较路径的时候,由于当前节点可能作为最后最长路径的根,所以要把四种情况都算上
但是返回的时候,当前节点的情况是作为之后的一些结果的子集,这时候当前节点一定不能为最长路径的根节点
所以要么选择左分支 root.val + leftSum,要么选择右分支 root.val + rightSum, 取两者中较大的那个,如果收益小于0,直接舍弃返回 0
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static int maxPathSum = Integer.MIN_VALUE; public Solution() { maxPathSum = Integer.MIN_VALUE; } public int maxPathSum(TreeNode root) { dfs(root); return maxPathSum; } private int dfs(TreeNode root) { int thisRootSum = 0; if (root == null) { return thisRootSum; } int leftSum = dfs(root.left); int rightSum = dfs(root.right); // 最长路径可能以任一结点为根。只当前根节点/根节点+左子树/根节点+右子树/根节点+左子树+右子树 这四种情况都行,选出这四种情况下最大的 int thisRootMax = root.val; if (leftSum > 0) { thisRootMax += leftSum; } if (rightSum > 0) { thisRootMax += rightSum; } // 最长路径可能以任一结点为根,所以每个节点的最大与之前的最大比较一下 maxPathSum = Math.max(maxPathSum, thisRootMax); // 之前比较路径的时候,由于当前节点可能作为最后最长路径的根,所以要把四种情况都算上 // 但是返回的时候,是作为之后的一些结果的子集,这时候当前节点一定不能为最长路径的根节点 // 所以要么选择左分支 root.val + leftSum,要么选择右分支 root.val + rightSum int chooseLeftOrRightMax = Math.max(root.val + leftSum, root.val + rightSum); // 收益小于0,直接舍弃 if (chooseLeftOrRightMax < 0) { chooseLeftOrRightMax = 0; } return chooseLeftOrRightMax; } }
96. 不同的二叉搜索树
如果整数1 ~ n中的 k 作为根节点值,则 1 ~ k-1 会去构建左子树,k+1 ~ n 会去构建右子树。
左子树出来的形态有a 种,右子树出来的形态有 b 种,则整个树的形态有 a∗b 种。
class Solution { public int numTrees(int n) { // 最后要返回 dp[n] int dp[] = new int[n+1]; // dp[0] 代表用 0 个数去构建左子二叉搜索树,dp[n-1] 代表用 n-1 个数(除掉根节点)去构建右子二叉搜索树 // dp[n] = dp[0]*dp[n-1] + dp[1]*dp[n-2] + dp[2]*dp[n-3] + ..... + dp[n-2]*dp[0] + ... dp[n-1]*dp[0] // 一共有 i 个数,左子树用 j 个,右子树用 i-j-1 个 dp[i] = dp[j]dp[i-j-1]之和 0<=j<=i-1(左子树可以用0个到i-1个(除根节点)) // 空树,只有一种 dp[0] = 1; // 一个数字的,只有一种 dp[1] = 1; // dp[0] 和 dp[1] 已知,所以从 dp[2] 开始 for (int i=2;i<=n;i++) { // 左子树可以用0个到i-1个(除根节点) for (int j=0;j<=i-1;j++) { dp[i] += dp[j]*dp[i-j-1]; } } return dp[n]; } }
104. 二叉树的最大深度
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public int maxDepth(TreeNode root) { return dfs(root); } public int dfs(TreeNode root) { if (root == null) { return 0; } // 左右子树每次 + 1 int leftHeiht = dfs(root.left) + 1; int rightHeight = dfs(root.right) + 1; // 返回左右子树中较高的那个 return leftHeiht >= rightHeight ? leftHeiht: rightHeight; } }
剑指 Offer 68 - II. 二叉树的最近公先
按官方解法:
将最近公共祖先称为 LCA
情况1: p 和 q分别在 LCA 的左右子树
——> lhasporq && rhasporq
情况2: p 是 q 的父节点(此时 q 在 p 的左右子树都行),那么它们俩的 LCA 就是p。
——> (root == p || root == q) && (lhasporq || rhasporq)
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ class Solution { private TreeNode res; public Solution() { res = null; } public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { dfs(root, p, q); return res; } private boolean dfs(TreeNode root, TreeNode p, TreeNode q) { if (root == null) { return false; } // 递归,左子树中是否有 p 或 q boolean lhasporq = dfs(root.left, p, q); // 递归,右子树中是否有 p 或 q boolean rhasporq = dfs(root.right, p, q); // 情况1:lhasporq && rhasporq p和q分别在左右子树 // 情况2: (root == p || root == q) && (lhasporq || rhasporq) 例如 p 是 q 的父节点,那么它们俩的最近公共祖先是p。 if ((lhasporq && rhasporq) || ((root == p || root == q) && (lhasporq || rhasporq))) { res = root; } // dfs递归从底向上返回,父节点的 lhasporq 和 rhasporq由子节点的推断而来 // 如果某节点的左子节点 lhasporq 或 rhasporq,那么此节点一定 lhasporq // 如果某节点的右子节点 lhasporq 或 rhasporq,那么此节点一定 rhasporq // 如果某节点的左子节点 == p 或 == q,那么此节点一定 lhasporq // 如果某节点的右子节点 == p 或 == q,那么此节点一定 rhasporq // 综上,如果某节点的左子节点满足这四个条件中的任何一个,那么此节点一定 lhasporq,所以用 || 连接 // 综上,如果某节点的右子节点满足这四个条件中的任何一个,那么此节点一定 rhasporq,所以用 || 连接 return lhasporq || rhasporq || root == p || root == q; } }
94. 二叉树的中序遍历
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private List<Integer> res; public Solution() { res = new ArrayList(); } public List<Integer> inorderTraversal2(TreeNode root) { dfs(root); return res; } // 递归 private void dfs(TreeNode root) { if (root == null) { return; } dfs(root.left); res.add(root.val); dfs(root.right); } // 非递归 public List<Integer> inorderTraversal(TreeNode root) { Stack<TreeNode> stack = new Stack(); while(root != null || !stack.isEmpty()) { // 左节点不为空的话,就一直将左节点入栈 if (root != null) { stack.push(root); root = root.left; } // 遇到第一个左节点为空的,就弹出它,并令 root = 它的右节点, 然后继续往下 else { root = stack.pop(); res.add(root.val); root = root.right; } } return res; } }
105. 从前序与中序遍历序列构造二叉树
递归:
前序:[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
中序:[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
递归函数
public TreeNode myBuildTree(int[] preorder, int[] inorder,
int preorder_startIndex,
int preorder_endIndex,
int inorder_startIndex,
int inorder_endIndex)
在递归函数里,先构造一个根节点 nowroot,根节点的值是已知的,就是 preorder[preorder_startIndex]
然后就可以开始下一次递归,来构造这个根节点的左右子节点了:
nowroot.left = myBuildTree(preorder, inorder,
preorder_startIndex+1,
preorder_startIndex+1+leftSubSize,
inorder_startIndex,
inorder_startIndex+leftSubSize
);
nowroot.right = myBuildTree(preorder, inorder,
preorder_startIndex+1+leftSubSize,
preorder_endIndex,
inorder_startIndex+leftSubSize+1,
inorder_endIndex);
我们发现 左右子树的 preorder_startIndex, preorder_endIndex, inorder_startIndex, inorder_endIndex
都可以由这个树的 preorder_startIndex, preorder_endIndex, inorder_startIndex, inorder_endIndex 再加上 leftSubSize 获得
leftSubSize 就是左子树的长度,比如说左子树的 preorder_endIndex 就为 preorder_startIndex + 1 + leftSubSize (其它的同理)
那么 leftSubSize 怎么获得呢?
起始就是 inorder 的根节点下标 减去 inorder 的起始下标,而 inorder 中的根节点下标我们是不知道的
我们已知的是 preorder 里面的根节点位置,即 preorder_startIndex,从而也知道了根节点的值
所以可以通过 根节点的值 来在 inorder 中找 inorder 中根节点的下标
每次都遍历会比较慢,所以我们在可以用一个 inorderValue2IndexMap,一开始就初始化好,后面直接就可以根据根节点的值求出根节点在 inorder 中的下标
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public HashMap<Integer, Integer> inorderValue2IndexMap; public Solution() { inorderValue2IndexMap = new HashMap(); } public TreeNode buildTree(int[] preorder, int[] inorder) { int n = inorder.length; // 构造 inorderValue2IndexMap for (int i=0;i<inorder.length;i++) { inorderValue2IndexMap.put(inorder[i], i); } // 递归 // 为什么这里的 endIndex 是 n 不是 n-1 呢?? return myBuildTree(preorder, inorder, 0, n, 0, n); } // 前序:[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ] // 中序:[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ] public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_startIndex, int preorder_endIndex, int inorder_startIndex, int inorder_endIndex) { // 注意返回条件:preorder 为空 if (preorder_startIndex == preorder_endIndex) { return null; } // 根节点所在位置就是 preorder 的开头位置,所以根节点的值是已知的 int preorder_nowroot_index = preorder_startIndex; int nowroot_val = preorder[preorder_startIndex]; // 构造当前的根节点 TreeNode nowroot = new TreeNode(nowroot_val); // 根据下面的递归可知,要求下次递归的边界条件--左右子树的 preorder_startIndex,preorder_endIndex,inorder_startIndex,inorder_endIndex // 只需要求出左子树的长度 leftSubSize 即可 // 左子树的长度 leftSubSize 求法:inorder 中根节点 index - inorder 的 startIndex // 而 inorder 中根节点 index 需要根据根节点的值在 inorder 中找到下标,这也就是为什么要搞一个inorderValue2IndexMap int inorder_nowroot_index = inorderValue2IndexMap.get(nowroot_val); int leftSubSize = inorder_nowroot_index - inorder_startIndex; // 根据下面这两个,来找左右子树的 preorder_startIndex,preorder_endIndex,inorder_startIndex,inorder_endIndex // 只要通过这次的值再加上 leftSubSize 就可以全部知道了 // 前序:[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ] // 中序:[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ] nowroot.left = myBuildTree(preorder, inorder, preorder_startIndex+1, preorder_startIndex+1+leftSubSize, inorder_startIndex, inorder_startIndex+leftSubSize ); nowroot.right = myBuildTree(preorder, inorder, preorder_startIndex+1+leftSubSize, preorder_endIndex, inorder_startIndex+leftSubSize+1, inorder_endIndex); return nowroot; } }
226. 翻转二叉树
嗯嗯,没想到这么简单
直接 dfs 和交换就可以了,而且试验过, dfs 和 交换操作 的顺序并没有影响,谁在前结果都是正确的
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode invertTree(TreeNode root) { dfs(root); return root; } public void dfs(TreeNode root) { if (root == null) { return; } TreeNode leftNode = root.left; TreeNode rightNode = root.right; root.left = rightNode; root.right = leftNode; dfs(root.left); dfs(root.right); } }
101. 对称二叉树
我们可以实现这样一个递归函数,通过「同步移动」两个指针的方法来遍历这棵树,
p 指针和 q 指针一开始都指向这棵树的根,随后
p 右移时,q 左移,
p 左移时,q 右移。
每次检查当前 p 和 q 节点的值是否相等,如果相等再判断左右子树是否对称。
话不多说,直接看代码
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public boolean isSymmetric(TreeNode root) { return dfs(root.left, root.right); } public boolean dfs(TreeNode p, TreeNode q) { // 两个都为 null ,都没有子树,那么二者肯定是对称的 if (p == null && q == null) { return true; } // 前面已经排除了都为 null 的情况,这里就是一个为 null,一个不为 null 的情况,一定不对称 if (p == null || q == null) { return false; } // 二者都不为 null 的情况 // 不仅要它们两个本身相等,还要它们的左右子树分别对称 return p.val == q.val && dfs(p.left, q.right) && dfs(p.right, q.left); } }
98. 验证二叉搜索树
如果只像下面第一次写的错误方法 dfs2 那样,只在每个节点判断 左<自己,右>自己 的话
那么就会出现下面这种情况
5 / \ 4 6 / \ 3 7
每一个微观看都是正确的,比如 3<6 这里微观看是正确的,但是 3 比 5 小却在 5 的右边,是不对的
所以要每次 dfs 的时候,还要定义一个当前允许的 最小值 low 和 最大值 high
boolean dfs2(TreeNode root, long low, long high)
每次递归:
- 如果当前节点值 <=low || >=high return false
- 之后再递归判断左右子树 dfs(root.left,low,root.val) && dfs(root.right,root.val,high)
递归左子树时,low 还是 low,但是因为左子树的所有值都要比当前根小,所以 high 变为 root.val
递归右子树时,high 还是 high,但是因为右子树的所有值都要比当前根大,所以 low 变为 root.val
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public boolean isValidBST(TreeNode root) { return dfs2(root, Long.MIN_VALUE, Long.MAX_VALUE); } public boolean dfs2(TreeNode root, long low, long high) { if (root == null) { return true; } // 判断当前节点,而不是判断 root.left root.right if (root.val <= low || root.val >= high) { return false; } // 当前节点符合时,且其左子树右子树也符合 return dfs2(root.left, low, root.val) && dfs2(root.right, root.val, high); } // 错误的解法,如下面这个 /* 5 / \ 4 6 / \ 3 7 */ // 每一个微观看都是正确的,但是 3<6 这里 // 微观看是正确的,但是 3 比 5 小却在 5 的右边,是不对的 public boolean dfs(TreeNode root) { if (root == null) { return true; } // 两者都为 Null if (root.left == null && root.right == null) { return true; } // 前面已经把都为 Null 的排除了,这里是 left 不为 Null, right 为 null 的 if (root.right == null) { return root.left.val < root.val && dfs(root.left); } // 前面已经把都为 Null 的排除了,这里是 right 不为 Null, left 为 null 的 if (root.left == null) { return root.right.val > root.val && dfs(root.right); } // 都不为 null 时 // 当前节点符合,且其左子树右子树也符合 return root.left.val < root.val && root.right.val > root.val && dfs(root.left) && dfs(root.right); } }
108. 将有序数组转换为二叉搜索树
解法有点类似前面的 从前序与中序遍历中构建二叉树
也是用递归
TreeNode buildBST(int[] nums, int startIndex, int endIndex)
在递归里面,先构建一个根节点 nowroot,根节点在 nums 里的下标是 (startIndex + endIndex) / 2
然后 nowroot 的左子节点和右子节点就通过递归得到
nowRoot.left = buildBST(nums, startIndex, nowRootIndex);
nowRoot.right = buildBST(nums, nowRootIndex + 1, endIndex);
return nowRoot;
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode sortedArrayToBST(int[] nums) { return buildBST(nums, 0, nums.length); } public TreeNode buildBST(int[] nums, int startIndex, int endIndex) { if (startIndex == endIndex) { return null; } int nowRootIndex = (startIndex + endIndex) / 2; TreeNode nowRoot = new TreeNode(nums[nowRootIndex]); nowRoot.left = buildBST(nums, startIndex, nowRootIndex); nowRoot.right = buildBST(nums, nowRootIndex + 1, endIndex); return nowRoot; } }
因为二叉搜索树和中序遍历的性质,所以二叉搜索树的中序遍历是按照键增加的顺序进行的。于是,我们可以通过中序遍历找到第 k 个最小元素。
复杂度分析
- 时间复杂度:O(H+k),其中 H 是树的高度。在开始遍历之前,我们需要 O(H) 到达叶结点。当树是平衡树时,时间复杂度取得最小值 O(logN+k);当树是线性树(树中每个结点都只有一个子结点或没有子结点)时,时间复杂度取得最大值
- 空间复杂度:O(H),栈中最多需要存储 H 个元素。当树是平衡树时,高度为 logN 空间复杂度取得最小值 O(logN);当树是线性树时,高度为 N 空间复杂度取得最大值 O(N)。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static int i=0; private static int res=0; public Solution () { res = 0; i = 0; } public int kthSmallest(TreeNode root, int k) { // 中序遍历 inorder(root, k); return res; } private void inorder(TreeNode root, int k) { if (root == null) { return; } inorder(root.left, k); i++; if (i==k) { res = root.val; return; } inorder(root.right, k); } }
199. 二叉树的右视图
BFS:
每层的最后一个节点加入结果,就是二叉树的右视图
关键在于层序遍历怎么区分开每一层呢?
while(!queue.isEmpty()) {
int curLayerSize = queue.size();
// while 里面还有 for 循环,目的是每一层一个 for 循环,不同层不用混在一起
for(int i=0;i<curLayerSize;i++) {
DFS:
我们按照 「根结点 -> 右子树 -> 左子树」 的顺序访问,就可以保证每层都是最先访问最右边的节点的。
(与先序遍历 「根结点 -> 左子树 -> 右子树」 正好相反,先序遍历每层最先访问的是最左边的节点)
结果里每个深度放一个节点,所以 res.size 就是当前的深度,与 depth 相等的时候,说明正好来到了新的一层
if (depth == res.size()) { res.add(root.val); }
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private List<Integer> res; public Solution() { res = new ArrayList(); } public List<Integer> rightSideView(TreeNode root) { dfs(root, 0); return res; } public void bfs(TreeNode root) { if (root == null) { return; } Queue<TreeNode> queue = new LinkedList(); queue.offer(root); while(!queue.isEmpty()) { int curLayerSize = queue.size(); // while 里面还有 for 循环,目的是每一层一个 for 循环,不同层不用混在一起 for(int i=0;i<curLayerSize;i++) { TreeNode top = queue.poll(); if (top.left != null) { queue.offer(top.left); } if (top.right != null) { queue.offer(top.right); } // 每层的最后一个节点就是右视图看到的 if (i==curLayerSize-1) { res.add(top.val); } } } } public void dfs(TreeNode root,int depth) { if (root == null) { return; } // 结果里每个深度放一个节点,所以 res.size 就是当前的深度 if (depth == res.size()) { res.add(root.val); } depth++; // 我们按照 「根结点 -> 右子树 -> 左子树」 的顺序访问,就可以保证每层都是最先访问最右边的节点的。 //(与先序遍历 「根结点 -> 左子树 -> 右子树」 正好相反,先序遍历每层最先访问的是最左边的节点) dfs(root.right, depth); dfs(root.left, depth); } }
114. 二叉树展开为链表
非递归前序遍历的同时,通过一个 pre 指针来改变指向
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public void flatten(TreeNode root) { if (root == null) { return; } Stack<TreeNode> stack = new Stack(); stack.push(root); TreeNode pre = null; TreeNode cur = null; TreeNode resHead = root; // 非递归前序遍历 while(!stack.isEmpty()){ cur = stack.pop(); // 除了第一次循环,即 cur 是根节点的时候,都会走到这里改变指向 if (pre != null) { pre.right = cur; pre.left = null; } pre = cur; // 因为栈后进先出,所以右节点先入栈 if (cur.right != null) { stack.push(cur.right); } if (cur.left != null) { stack.push(cur.left); } } } }
路径和问题
112. 路径总和
root.left == null && root.right == null 判断是叶子节点的条件
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static boolean res = false; public Solution() { res = false; } public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) { return false; } dfs(root, 0, targetSum); return res; } private void dfs(TreeNode root, int curSum, int targetSum) { if (root == null) { return; } curSum += root.val; if (root.left == null && root.right == null && curSum == targetSum) { res = true; } dfs(root.left, curSum, targetSum); dfs(root.right, curSum, targetSum); } }
113. 路径总和 II
List 不是 Integer 那种每次都 new 一个新对象的无状态类型,所以 dfs 完后得恢复现场
注意用 LinkedList,有 addLast() removeLast() 方法
恢复现场这一句是在什么时候执行?
- 一个节点的左子节点及其子树都 dfs 完了,右子节点及其左右子树都 dfs 完了,得回到这一层的上一层,所以得清除这一层带来的影响
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static List<List<Integer>> res; public Solution() { res = new ArrayList(); } public List<List<Integer>> pathSum(TreeNode root, int targetSum) { dfs(root, 0, targetSum, new LinkedList()); return res; } private void dfs(TreeNode root, int curSum, int targetSum, LinkedList<Integer> curPath) { if (root == null) { return; } curSum += root.val; // 在末尾添加 curPath.addLast(root.val); // 判断为叶子节点以后 if (root.left == null && root.right == null && curSum == targetSum) { res.add(new LinkedList(curPath)); // 自己写过的错误写法,这样每次加到结果中后 curPath 会清为空 // 这样是不对的,curPath 应该永远不清空,res 里保存的是 curPath 某些时刻的快照 // res.add(curPath) // curPath = new ArrayList(); } dfs(root.left, curSum, targetSum, curPath); dfs(root.right, curSum, targetSum, curPath); // List 不是 Integer 那种每次都 new 一个新对象的无状态类型,所以 dfs 完后得恢复现场// 在末尾删除 curPath.removeLast(); }
437. 路径总和 III
Map 不是 Integer 那种每次都 new 一个新对象的无状态类型,所以 dfs 完后得恢复现场
前缀和
假设 left 到 right 下标的子数组和为 k
- nums[left...right] = k
- preSum[right] - preSum[left] = k
- preSum[left] = preSum[right] - k
所以要 每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个
用一个 map 来记录,前缀和的 count
key: 前缀和的值
value: 前缀和为这个值的个数
注意!!!:
- map 要放入一个初始值 {0,1}
- 一定要先 getPreSumCount ,再把加上当前节点的 PreSum 放进 map +1(因为当前节点的和不属于当前节点的前缀和)
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private static Integer res = 0; public Solution() { res = 0; } public int pathSum(TreeNode root, int targetSum) { // Long 是因为有极端用例 Map<Long, Integer> preSum2CountMap = new HashMap(); // 一定要提前放进去 (0, 1) 即不加前面任何数字的话,就是0 preSum2CountMap.put(0L, 1); dfs(root, preSum2CountMap, 0L, targetSum); return res; } private void dfs(TreeNode root, Map<Long, Integer> preSum2CountMap, Long curSum, Integer targetSum) { if (root == null) { return; } curSum+=root.val; res += preSum2CountMap.getOrDefault(curSum - targetSum, 0); // 将现在的 curSum 放进 map 一定要在 map.get(preSum) 即 map.get(curSum-targetSum) 之后 // 因为到自己这里的所有和,不属于自己的前缀和 preSum2CountMap.put(curSum, preSum2CountMap.getOrDefault(curSum, 0)+1); dfs(root.left, preSum2CountMap, curSum, targetSum); dfs(root.right, preSum2CountMap, curSum, targetSum); preSum2CountMap.put(curSum, preSum2CountMap.getOrDefault(curSum, 0)-1); } }
208. 实现 Trie (前缀树)
包含三个单词 "sea","sells","she" 的 Trie 会长啥样呢?
下标隐式的定义了对应的字符,同时对于末端的字符结束,要有一个 isEnd 来表示是否是一个字符的结束
根节点,就是 Trie root = this
char c = word.charAt(i); int cindex = c - 'a'; root.children[cindex]
class Trie { private Trie[] children; private boolean isEnd; public Trie() { // 每个下标是否有 child 表示这个字符是否有 children = new Trie[26]; isEnd = false; } public void insert(String word) { // 从根节点开始,根节点 = this Trie root = this; for (int i=0;i<word.length();i++) { char c = word.charAt(i); int cindex = c - 'a'; // 往下找,没有的话就添加新节点 if (root.children[cindex] == null){ Trie newTrie = new Trie(); root.children[cindex] = newTrie; } root = root.children[cindex]; } // 无论最后的这个是新加的,还是以前就有的。isEnd 都要新设为 true root.isEnd = true; } public boolean search(String word) { Trie node = searchNode(word); // 有这样的节点,并且该节点是最后一个 return node != null && node.isEnd; } public boolean startsWith(String prefix) { return searchNode(prefix) != null; } public Trie searchNode(String prefix) { // 从根节点开始,根节点 = this Trie root = this; for (int i=0;i<prefix.length();i++) { char c = prefix.charAt(i); int cindex = c - 'a'; if (root.children[cindex] == null){ return null; } // 往下找 root = root.children[cindex]; } return root; } } /** * Your Trie object will be instantiated and called as such: * Trie obj = new Trie(); * obj.insert(word); * boolean param_2 = obj.search(word); * boolean param_3 = obj.startsWith(prefix); */
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器