关于二叉树的一些基本遍历模版就不说了
94.二叉树中序遍历
2/29-10/23
已经忘完了,前中后序遍历指的是根节点的顺序;注意根节点判空
在这里有个问题,ArrayList是定义的包装类,而add的是int类型,得益于Java的自动装箱(大佬勿喷)。 空间时间都是O(n)。
埋坑:迭代方法和Morris 中序遍历
105.从前序和中序序列构造二叉树
2/29
根据前序和中序或者后序和中序才能复原二叉树,要恢复二叉树必须知道中序序列,只是知道前序和后序,不能恢复二叉树,因为不知道中序序列无法获知根节点的左右子树位置。
还是看题解完成的,结果输出树的根节点,应该就是这个树的层序遍历,root是根节点,一开始还没特别明白,好像也不是层序遍历。题目的要求是构造二叉树返回根节点。
class Solution {private Map<Integer, Integer> indexMap;public TreeNode buildTree(int[] preorder, int[] inorder) {int n = preorder.length;//hash映射indexMap = new HashMap<Integer,Integer>();for(int i = 0; i < n; i++){indexMap.put(inorder[i],i);}return getroot(preorder, 0, n-1, inorder, 0, n-1);}//前序遍历的第一个元素在中序遍历中定位,左右部分再递归private TreeNode getroot(int[] preorder, int pre_start, int pre_end, int[] inorder, int in_start, int in_end){if(pre_start > pre_end){return null;}//得到前序第一个元素在中序中的下标int in_index = indexMap.get(preorder[pre_start]);//定义根节点TreeNode root = new TreeNode(preorder[pre_start]);//左子树长度int left_Subtree_size = in_index - in_start;//圈出两个序列中的左子树root.left = getroot(preorder, pre_start+1, pre_start + left_Subtree_size, inorder, in_start, in_index -1);//圈出两个序列中的右子树root.right = getroot(preorder, pre_start + left_Subtree_size + 1, pre_end,inorder, in_index + 1, in_end );return root;}
}
day02
104.二叉树的最大深度easy√
看到题目最先想到dfs,深度优先搜索的数据结构为栈(stack)。在每次遍历到达叶子节点的时候,为当前最长路径并更新结果, 关键是到达叶子节点后的做法,附上自己的代码
class Solution {private int maxDepth = 0;private int current_maxDepth = 0;public int maxDepth(TreeNode root) {Traverse(root);return maxDepth;}private void Traverse(TreeNode root){if(root != null){current_maxDepth++;Traverse(root.left);Traverse(root.right);current_maxDepth--;} else{if(current_maxDepth > maxDepth)maxDepth = current_maxDepth;//current_maxDepth = 0;}}}
还是递归,三种不同顺序的遍历其实都是dfs,每进入一层递归,深度加一。如果当前节点为null,说明遍历到了叶子节点,这时更新最大深度。不可以在更新之后马上置零,首先置零就是不对,毕竟递归返回时深度是逐渐减一,应该在遍历完右子树的时候减一,也就是弹栈的时候,更简明的说,深度减一的时候应该发生在当前层遍历完的时候。
官方解法
递归,最大深度是左右子树的最大深度+1
102.二叉树的层序遍历Medium
有点印象,要用队列,和一般的BFS不同的是,这里记录每层节点的个数,更够出队节点个数个元素。
- 首先根元素入队
- 当队列不为空的时候
- 求当前队列的长度 si
- 依次从队列中取 si个元素进行拓展,然后进入下一次迭代
class Solution {public List<List<Integer>> levelOrder(TreeNode root) {//结果集List<List<Integer>> res = new ArrayList<>();//每次都忘记这个空条件if(root == null){return res;}Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while(!queue.isEmpty()){//定义一个集合存放每层元素List<Integer> level_ret = new ArrayList<>();int levelLength = queue.size();for(int i = 0; i < levelLength; i++){TreeNode node = queue.poll();level_ret.add(node.val);//又忘记判空if(node.left != null){queue.offer(node.left);}if(node.right != null){queue.offer(node.right);}}res.add(level_ret);}return res;}
}
对Queue和相关API不熟悉
day03
199.二叉树的右视图Medium
看到题目后想到的就是昨天的层序遍历,直接在每层的最后一个节点处将该节点添加到集合中。
class Solution {public List<Integer> rightSideView(TreeNode root) {//结果集合List<Integer> res = new ArrayList<>();//判空if(root == null)return res;Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while(!queue.isEmpty()){int currenr_level_Length = queue.size();for(int i = 0; i < currenr_level_Length; i++){TreeNode node = queue.poll();if(i == currenr_level_Length - 1){res.add(node.val);}if(node.left != null){queue.offer(node.left);}if(node.right !=null){queue.offer(node.right);}}}return res;}
}
第一次写有2点小错误,记一下:
- 定义队列的时候没有说明类型,后面出队操作存储到node中时类型不能转换
- 等于判断时是双等于
- 判空的时候,如果说先判空返回的结果怎么都不对,先定义结果集合,root空就直接返回
题解里的DFS看一下,下次可以用这个方法:
class Solution {List<Integer> res = new ArrayList<>();public List<Integer> rightSideView(TreeNode root) {dfs(root, 0); // 从根节点开始访问,根节点深度是0return res;}private void dfs(TreeNode root, int depth) {if (root == null) {return;}// 先访问 当前节点,再递归地访问 右子树 和 左子树。if (depth == res.size()) { // 如果当前节点所在深度还没有出现在res里,说明在该深度下当前节点是第一个被访问的节点,因此将当前节点加入res中。res.add(root.val);}depth++;dfs(root.right, depth);dfs(root.left, depth);}
}
这个方法的遍历顺序和先序遍历的左右顺序正好相反,为根右左,这样就可以保证每次第一个访问到的是该层最右边的元素。res中的结果元素和深度对应,每层一个。目前还没怎么做DFS的题。
114.二叉树展开为链表Medium
二叉树展开为链表 - 力扣(LeetCode),可以看一下这个题解。方法很多,感觉首先就理解错了题目的意思,要求是展开,应该是原地算法,你自己再构造一个不行。也行,就像题解那样获取前序序列后从根节点构造,你是又搞了个根节点,看清题目
看了题解就先用最简单的方法吧:
class Solution {List<TreeNode> pre_res= new ArrayList<>();public void flatten(TreeNode root) {preorder(root);TreeNode node = new TreeNode();node = root;int length = pre_res.size();for(int i = 1; i < pre_res.size(); i++){node.right = pre_res.get(i); node.left = null;node = node.right;}return ;}private void preorder(TreeNode root){if(root == null)return ;pre_res.add(root);preorder(root.left);preorder(root.right);}
}
与官方主要的不同在for循环里,官方是拿到两个元素进行的操作,当时还不明白怎么返回的,应该是根据根节点的地址来判定的,就看这个地址存储的内容。
class Solution {public void flatten(TreeNode root) {List<TreeNode> list = new ArrayList<TreeNode>();preorderTraversal(root, list);int size = list.size();for (int i = 1; i < size; i++) {TreeNode prev = list.get(i - 1), curr = list.get(i);prev.left = null;prev.right = curr;}}public void preorderTraversal(TreeNode root, List<TreeNode> list) {if (root != null) {list.add(root);preorderTraversal(root.left, list);preorderTraversal(root.right, list);}}
}
这题磨磨蹭蹭的搞了好久,注意效率不会的及时看答案。
day04
108.将有序数组转化为二叉搜索树
BST:
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。
二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树都是二叉搜索树。
首先,了解二叉搜索树BST及高度平衡二叉搜索树AVL,看了两分钟b站的视频有了思路,这题和前面做过的根据前序和中序构造二叉树已经种种分治类题目很像,有了思路自己基本能写出来:
因为数组是有序的,要求构造高度平衡的BST,因此我们找到数组的中间节点作为根节点,进行数组的划分,然后再根据左右区间进行相同的递归操作。构造时递归调用返回的是节点值,与遍历时不同。
class Solution {//private TreeNode root;public TreeNode sortedArrayToBST(int[] nums) {int length = nums.length;return buildSubAVL(nums, 0, length-1);//return root;}private TreeNode buildSubAVL(int[] nums, int start, int end){if(start > end){return null;}int index = (start + end)/2;TreeNode root = new TreeNode();root.val = nums[index];root.left = buildSubAVL(nums, start, index-1);root.right = buildSubAVL(nums, index + 1, end);return root;}
}
原来的判空条件是这样写的,结果超时,栈溢出了。
if(start == end){return new TreeNode(nums[start]);
}
230.二叉搜索树中第K小的元素
递归法完全遍历
根据BST性质,对其中序遍历得到的就是树中元素值递增的排列,能直接得到其中第K小的元素。
class Solution {private List<TreeNode> inorder = new ArrayList<>();public int kthSmallest(TreeNode root, int k) {inorderTraverse(root);return inorder.get(k-1).val;}private void inorderTraverse(TreeNode root){if(root == null)return;inorderTraverse(root.left);inorder.add(root);inorderTraverse(root.right); }
}
刚想在遍历到第K个元素的时候进行return,不行好像因为它还在递归的过程中,return之后还是在函数里面。
迭代法不完全遍历
有进阶要求:如果BST频繁变化,比如频繁的插入删除等,每次查找都要遍历效率不高
视频二叉树中序遍历迭代,抓住访问的顺序和要处理的顺序是否一致,
右孩子为空弹栈,到上一层,很像前面的一题,附上迭代中序遍历的代码,结合迭代的ppt理解一下吧
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res = new ArrayList<Integer>();Deque<TreeNode> stk = new LinkedList<TreeNode>();while (root != null || !stk.isEmpty()) {while (root != null) {stk.push(root);root = root.left;}root = stk.pop();res.add(root.val);root = root.right;}return res;}
}
那关于这一题也就是在弹栈的时候计数,也就是处理的时候记录是第几个处理的,本来想在递归中写的,好像写不出来?
class Solution {public int kthSmallest(TreeNode root, int k) {Deque<TreeNode> stack = new ArrayDeque<TreeNode>();while (root != null || !stack.isEmpty()) {while (root != null) {stack.push(root);root = root.left;}root = stack.pop();--k;if (k == 0) {break;}root = root.right;}return root.val;}
}
day05
226.翻转二叉树
这题比较简单,就不贴代码了,主要是要注意不能中序遍历,可以想象一棵最简单的二叉树,首先是遍历完左孩子,然后执行交换左右孩子,再去遍历右孩子,这时遍历的右孩子其实是没遍历前的左孩子,如果左孩子还有孩子那么在根节点翻转之前已经遍历过了,翻转之后再遍历翻转相当于没翻转。就这样吧,写的很明白了。
98.验证二叉搜索树
根据二叉搜索树的性质吧,中序遍历之后一定是递增数组序列。拿到中序结果后一个个比较,这应该是最笨的方法。不贴代码了,主要是写的时候注意判断节点值相等的情况。
也看了题解,本来自己也想到用两个变量来比较而不像都存储在数组里那样占空间,但是自己不懂Long.min和long.max这种。贴个官方递归的代码
class Solution {public boolean isValidBST(TreeNode root) {return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);}public boolean isValidBST(TreeNode node, long lower, long upper) {if (node == null) {return true;}if (node.val <= lower || node.val >= upper) {return false;}return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);}
}
顺便复习一下中序的迭代方法
class Solution {public boolean isValidBST(TreeNode root) {Deque<TreeNode> stack = new LinkedList<TreeNode>();double inorder = -Double.MAX_VALUE;while (!stack.isEmpty() || root != null) {while (root != null) {stack.push(root);root = root.left;}root = stack.pop();// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树if (root.val <= inorder) {return false;}inorder = root.val;root = root.right;}return true;}
}
题解还有很多方法和思想,留个坑吧。
437.路径总和 III
这题是medium,自己ac了,但是是笨法子,看题目肯定是要遍历完所有路径的,2点要求:路径只能往下走,路径开头不一定是根节点。首先想到前序遍历,对每个节点的访问时机就是处理时机,
- 嵌套进行递归,外层的递归遍历是遍历每个节点为路径的起始节点,内层的递归遍历是对于固定起始的根节点进行路径遍历
- 还是什么时候进行递归弹栈(路径和减去当前节点路径)的问题,遍历完右节点。这个点第三次遇到了,这个和官方的嵌套递归还不一样。
- 路径和可能会超过int,要定义为long(第127个测试用例)
贴个自己的代码:
双重递归遍历所有情况
class Solution {private int routeNum = 0;private int targetSum = 0; public int pathSum(TreeNode root, int targetSum) {this.targetSum = targetSum;preorder(root);return routeNum;}private void preorder(TreeNode root) {if(root == null)return;//long curLenth = 0;sure_node_get(root,curLenth);preorder(root.left);preorder(root.right);}private void sure_node_get(TreeNode node, long curLenth) {if(node == null)return;curLenth = curLenth + node.val;if(curLenth == targetSum)routeNum++;sure_node_get(node.left, curLenth);sure_node_get(node.right, curLenth);curLenth = curLenth - node.val;}
}
官方的,很巧妙:
class Solution {public int pathSum(TreeNode root, int targetSum) {if (root == null) {return 0;}int ret = rootSum(root, targetSum);ret += pathSum(root.left, targetSum);ret += pathSum(root.right, targetSum);return ret;}public int rootSum(TreeNode root, int targetSum) {int ret = 0;if (root == null) {return 0;}int val = root.val;if (val == targetSum) {ret++;} ret += rootSum(root.left, targetSum - val);ret += rootSum(root.right, targetSum - val);return ret;}
}
前缀和+DFS:
上面那种方法要递归遍历所有的情况有点浪费时间,而且有的路径长度重复计算了。既然要找到一条起始节点可以不是根节点的固定长度路径,可以用作差的方法。
例如一个一维数组,我们要找到连续子数组的和为固定值的子数组(好像有这道题目),定义两个以数组第一个元素开头的动态连续子数组,对每个子数组求和作差。在这道题中也是一样的道理,二叉树从根节点到某个子节点的路径是唯一的。
所以这个看了题解已经众多评论之后我认为有以下几点需要重点理解:
- 前缀和定义:本题是从根节点到遍历到的当前节点的路径上所有元素之和,包括根节点。也就是说每个节点都有自己的前缀和。
- 前缀和的存储:采用hashmap,key值为不同节点的前缀和的值,value为该前缀和的值出现次数;更具体一点,我认为是从根节点到遍历到的当前节点的位置这条唯一路径上某个具体的前缀和的值出现的次数。
- 整体流程和所谓的“状态恢复”:采用dfs遍历树,首先初始化map集合。从根节点开始,每遍历到一个节点:
- 首先计算当前节点的前缀和,
- 然后看看集合中是否又满足条件的前缀和(集合中是否存在前缀和的值为当前节点前缀和减去所求路径长度)
- 将当前缀和的信息更新or添加到map集合
- 递归遍历左右子树
- 关于最后的状态恢复,其实就是上面说的提到很多次的弹栈恢复,因为只遍历了整个数一次,而且要求所求路径必须向下,我们计算时也以根节点到某个节点的一条也只有一条路径上去计算。因此这个前缀和其实和你自己想的那个解法的路径是一个东西。都需要弹栈,该节点左右子树遍历完后,也标志着该节点的遍历完成,所以要在集合中减去一次该节点的前缀和。
写了好多,有点烦了,贴代码自己理解吧:
这篇文章在ipad题解评论看到了但是在电脑上没找到。
class Solution {public int pathSum(TreeNode root, int targetSum) {Map<Long, Integer> prefix = new HashMap<Long, Integer>();prefix.put(0L, 1);return dfs(root, prefix, 0, targetSum);}public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) {if (root == null) {return 0;}int ret = 0;curr += root.val;ret = prefix.getOrDefault(curr - targetSum, 0);prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);ret += dfs(root.left, prefix, curr, targetSum);ret += dfs(root.right, prefix, curr, targetSum);prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);return ret;}
}
Java高级之HashMap中的put()方法和putIfAbsent()方法_hashmap putifabsent-CSDN博客
Java HashMap getOrDefault() 方法 | 菜鸟教程 (runoob.com)
day06
124.二叉树中的最大路径和Hard
以为想到了却又没想到,最后还是屎山代码憋出来了。这道hard说难也不难。分析题目,主要是路径,这个路径可以是跨越根节点的,也就是可以左根右这种,之前做的遍历题目都是从根节点出发不会出现横跨根节点的路径。
对于二叉树类的复杂题目,好像还是先考虑最简单的二叉树情况然后递归,因此如果是最简单的三个节点的二叉树,它的最大路径和都有6种,三个节点和三个组合(根+左,根+右,左+根+右),我们需要分类讨论,这也是DP(dynamic programing)的一个重要思想。
根据之前的经验,我们分类成从根节点向下的路径(不跨越根节点)和跨越根节点的路径,说个废话,这里是一定要包含根节点的,因为递归中每个点都是被看做根节点进行遍历的。如果我们知道了左右子树的MRL(max route length最大路径和),那么自然而然的就会去思考以根节点的二叉树的MRL;
也就是,把遍历到的根节点代表的子树的作用抽象成一个节点,官方题解里为什么要和0比较也是这个意思,那么就分类讨论吧,自己的方法中对于跨越根节点的路径和单独存放在集合,后面再去比较,也是没有进化完全。
class Solution {List<Integer> maybe_res = new ArrayList<>(); public int maxPathSum(TreeNode root) {int max1 = curr_node_start_Max(root);int max2 = Collections.max(maybe_res);return Math.max(max1, max2);}//以当前节点为头的最大路径和,向下private int curr_node_start_Max(TreeNode root) {if(root == null)return 0;int leftMax = curr_node_start_Max(root.left);int rightMax = curr_node_start_Max(root.right);int x = Math.max(leftMax, rightMax);if(root.val < 0) {//针对只有-3的样例if(root.left != null)maybe_res.add(leftMax);if(root.right != null)maybe_res.add(rightMax);//maybe_res.add(x);maybe_res.add(root.val + leftMax + rightMax);//原来这个地方是直接返回root.val的//后来[8,9,-6,null,null,5,9]这个样例没过才改的return Math.max(root.val, root.val + x);} //if(root.val >= 0) else{if(leftMax>=0&&rightMax>=0){maybe_res.add(root.val+leftMax+rightMax);return root.val + x;} else if(x <0){//如果左右子树的MRL都小于0,不如一个只要根节点return root.val; }else{return root.val + x;}}}
}
像一坨,但是思路是有点模样的
官方题解:
class Solution {int maxSum = Integer.MIN_VALUE;public int maxPathSum(TreeNode root) {maxGain(root);return maxSum;}public int maxGain(TreeNode node) {if (node == null) {return 0;}// 递归计算左右子节点的最大贡献值// 只有在最大贡献值大于 0 时,才会选取对应子节点int leftGain = Math.max(maxGain(node.left), 0);int rightGain = Math.max(maxGain(node.right), 0);// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值int priceNewpath = node.val + leftGain + rightGain;// 更新答案maxSum = Math.max(maxSum, priceNewpath);// 返回节点的最大贡献值return node.val + Math.max(leftGain, rightGain);}
}
很巧妙,官方的这个priceNewpath是在左右子树都遍历完的时候计算的,想想你前面写的代码在右子树遍历完之后的弹栈操作,都是有相通的点,而且也是根左右相加,解决了跨根节点的问题,其实这里就是和自己的把跨根节点的MRL存到集合里一样,人家是及时更新,你是最原始的方法选出最大值。
返回时也只能返回不跨根节点的路径,因为跨的都更新比较过了。就写到这吧,越写越感觉自己的解法和官方有很多相同之处。
236.二叉树的最近公共祖先
分别记录p,q节点的遍历路径然后比较,怎么记录?不会。。好像会了,最后没写出来
看题解了,有两种方法
存储节点父子关系
class Solution {Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();Set<Integer> visited = new HashSet<Integer>();public void dfs(TreeNode root) {if (root.left != null) {parent.put(root.left.val, root);dfs(root.left);}if (root.right != null) {parent.put(root.right.val, root);dfs(root.right);}}public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {dfs(root);while (p != null) {visited.add(p.val);p = parent.get(p.val);}while (q != null) {if (visited.contains(q.val)) {return q;}q = parent.get(q.val);}return null;}
}
这种方法就是先遍历所有节点,可以将所有的父子关系存储在hashmap中,然后直接从其中一个节点p出发找到它的所有父节点并加入到集合中,再一个个查找另一个节点的父节点是否在其中。这种方法还是比较好理解的,之前学的时候好像也接触过这种方法。
递归,分类讨论
咱也不知道这个方法为啥叫递归,所有的遍历方法也用到了递归啊。
一,对于结果节点的判断条件还是能理解,两种情况,分别是p和q没有父子节点关系,p和q其中一个是另一个的父节点。
二,对fx函数的定义,以x为根节点的子树中是否包含p和q节点。
三,对于dfs返回条件的理解,首先触发的一定是遍历到的节点值是p或者q,从lson,rson是递归的返回值也能说明这一点。
class Solution {private TreeNode ans;public Solution() {this.ans = null;}private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {if (root == null) return false;boolean lson = dfs(root.left, p, q);boolean rson = dfs(root.right, p, q);if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson))) {ans = root;} return lson || rson || (root.val == p.val || root.val == q.val);}public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {this.dfs(root, p, q);return this.ans;}
}
可以再理解一下dfs返回值的写法含义,自己再写的时候dfs里root.left能写成root真是服了。
这题拖了挺久,从上周四下午,中间就没leetcode。。。
day07
剩下的两道easy的二叉树的题目,二叉树也就暂时告一段落
这两个是easy??还是我太菜了,打扰了
101.对称二叉树√
两次遍历比较:
比较任意两种遍历顺序的颠倒左右子树遍历的结果是否相同,这个方法不行,题目里第二个样例不过的。原因就是空节点影响了判断,在遍历到空节点时不要直接返回,向遍历结果集合中添加一个元素(-101,)来标记空节点。ac了。不贴代码了这个很好写。
对于官方题解先不看迭代方法了,挖坑。。
递归:
关键是两个节点的左右节点都要判断,以及各种情况的分类
迭代:还是两两节点处理
543.二叉树的直径
这题也是有点思考的,所以现在easy的标准是代码量还是思路呢?这道题的直径指的是任意两个节点之间最长路径的长度。题目中说的路径不一定经过根节点,根节点不也就是个节点,每个节点也是每次遍历的根节点。那么最长的路径一定是跨域某个节点的,左加右总比单边大的,这题和前面的路径和也像,这里想到二叉树的最大深度这题,稍作修改,遍历到某个节点时,跨域节点的最大路径一定是左右子树最大深度和。
对于return后的最大深度,其实是左右子树的而不是本节点的,所以不用去掉加一,一开始没想清楚还去掉了,直接不能走出来,lrsum一直是0。不写那么细了。
class Solution {private int lrsum;public int diameterOfBinaryTree(TreeNode root) {lrsum = 0;maxDepth(root);return lrsum ;}private int maxDepth(TreeNode root) {if(root == null){return 0;}int leftHeight = maxDepth(root.left);int rightHeight = maxDepth(root.right);int x = leftHeight + rightHeight;if(x > lrsum)lrsum = x;return Math.max(leftHeight, rightHeight) + 1;}
}
然后呢,看了官方题解,好好好,原来是一样的,看了就当巩固一下了,特别是求树深度的代码。
二叉树的部分到这就结束了,那么那么,感觉还有很多要理清的,比如递归是隐式栈?(评论里看到的),遍历的迭代写法,这个是二刷的重点,及时复习吧,及时记录,坚持。