今日内容
- leetcode. 513 找树左下角的值
- leetcode. 112 路径总和
- leetcode. 106 从中序与后续遍历序列构造二叉树
Leetcode. 513 找树左下角的值
本题用层序遍历反而更简单一点。用层序遍历的思路就是记录每一层的第一个节点,这样遍历到最后一层时就可以得到树的左下角的值了。代码如下:
/**
* 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 findBottomLeftValue(TreeNode root) {
if (root == null){return 0;}
Queue<TreeNode> queue = new LinkedList<>();
int result = 0;
queue.offer(root);
while (!queue.isEmpty()){
int len = queue.size();
for (int i = 0; i < len; i++){
TreeNode n = queue.peek();
queue.poll();
if (i == 0){result = n.val;} // 记录每层的第一个节点
if (n.left != null){queue.offer(n.left);}
if (n.right != null){queue.offer(n.right);}
}
}
return result;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
也可以使用递归来解决该题,在本题中采用前序遍历来解决。
递归三要素:
- 判断递归函数的参数和返回值。节点肯定需要被传入,还有记录深度的变量也需要被传入。我们并不需要递归函数返回什么结果,所以有两个参数、没有结果。
- 根据题意,当前节点为叶子节点时,就可以记录当前深度,并停止递归了。
- 单层逻辑就是查看节点是否为叶子节点,不是则先探左子树,再探右子树;是则按照步骤 2 进行。
根据上述三个递归要素,写出如下代码:
// 递归法
class Solution {
int maxDepth = 0;
int result = 0;
public int findBottomLeftValue(TreeNode root) {
result = root.val; // 考虑root为空或者root就是叶子节点的情况
findBottom(root, 0);
return result;
}
public void findBottom(TreeNode node, int depth){
if (node.left == null && node.right == null){ // 要素 2
if (depth > maxDepth){
maxDepth = depth;
result = node.val;
}
return;
}
if (node.left != null){
findBottom(node.left, depth + 1);
/** 隐含了回溯,其等同于
depth++;
findBottom(node.left, depth);
depth--;
*/
}
if (node.right != null){
findBottom(node.right, depth + 1);
}
return;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
Leetcode. 112 路径总和
本题要求从根节点开始,查找一条到叶子节点的符合要求的路径。那么这可以直接使用前序遍历来解决了。
这样又要分析递归三要素了:
- 递归函数的参数和返回值。参数肯定要有节点。本题需要判断路径上节点值的和是否等于targetSum,因此还需要传入一个状态参数用于判断。为了表示某一条路径是否符合要求,该函数需要返回Boolean类型。
- 递归函数停止条件。当发现是叶子节点时就可以停下,判断该路径是否符合要求,符合则返回True,不符合则回溯,继续探测下一条路径。
- 单层逻辑。首先判断是否为叶子节点,是则按步骤 2 执行;不是则先探测左子树,再探测右子树。
此处细说一下步骤 1 中的状态参数。一般我们会想直接把路径上所有节点的元素值相加,将得到的结果和targetSum进行比较,但实现这个代码比较麻烦。我们可以使用递减思维,每遍历一个节点就将targetSum减去该节点值,如果到叶子节点发现targetSum等于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 {
// 步骤 1,因为本身这个函数就符合条件了,所以不需要重新创建递归函数
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null){return false;}
// 中
targetSum -= root.val; // 根节点的值一开始就要减去
// 是叶子节点且targetSum等于0时,说明符合条件
if (root.left == null && root.right == null && targetSum == 0){return true;}
// 否则不符合
if (root.left == null && root.right == null){return false;}
// 左
if (root.left != null){
boolean isPath = hasPathSum(root.left, targetSum); // 此处一定要这样用一个变量接收递归函数结果,否则直接会返回false。
if (isPath){return true;}
}
// 右
if (root.right != null){
boolean isPath = hasPathSum(root.right, targetSum);
if (isPath){return true;}
}
return false;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
根据本题思路还可以解决 113. 路径总和 II - 力扣(LeetCode),不同之处在于Leetcode. 113 需要返回所有满足条件的路径的列表。
代码如下:
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> result = new ArrayList<>();
if (root == null){return result;}
List<Integer> path = new ArrayList<>();
findPath(root, result, path, targetSum);
return result;
}
// 本题返回的是列表,列表是引用类型,所以递归函数不用返回值
public void findPath(TreeNode n, List<List<Integer>> result, List<Integer> path, int targetSum){
if (n == null){return;}
// 中
targetSum -= n.val;
path.add(n.val);
if (n.left == null && n.right == null && targetSum == 0){
result.add(new ArrayList<>(path)); // 此处还需要套一层New,否则插入的不是整个路径列表
}
// 左
if (n.left != null){
findPath(n.left, result, path, targetSum);
path.remove(path.size() - 1); // 遍历完一个节点后就回溯
}
// 右
if (n.right != null){
findPath(n.right, result, path, targetSum);
path.remove(path.size() - 1);
}
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
Leetcode. 106 从中序与后序遍历序列构造二叉树
本题也让我们复习了一个小知识点:前序/后序遍历 和 中序遍历 可以构造出唯一的二叉树
本题思路也是围绕着这两种遍历的方式来的:
- 后序遍历顺序为左右中,最后一个元素必是根元素。因此先获取后序遍历序列的最后一个元素。
- 中序遍历顺序为左中右。在中序遍历序列中进行查找,找到上一步获取到的根元素。将中序遍历进行拆分,分成左右两部分。
- 相同序列、相同根节点的左右子树长度肯定相同。根据上一步拆分的左右部分的长度,将后序遍历也拆分成同样长度的左右部分。
- 不断重复步骤 1-3,直到左右部分不可拆分时,二叉树构造完成。
上述思路可以如下图所示:
根据上述思路,写出如下代码,要注意区间不变量,本题一直贯彻“左闭右开”:
/**
* 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 buildTree(int[] inorder, int[] postorder) {
if (inorder.length == 0 || postorder.length == 0){return null;}
TreeNode root = splitOrder(inorder, 0, inorder.length, postorder, 0, postorder.length);
return root;
}
public TreeNode splitOrder(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd){
// 判断是否已经不可分割
if (postStart == postEnd){return null;}
// 步骤 1 ,获取后序遍历序列的最后一个元素
int postValue = postorder[postEnd - 1];
TreeNode root = new TreeNode(postValue);
// 步骤 2 ,拆分中序遍历序列
int delimeterIndex = 0;
for (delimeterIndex = 0; delimeterIndex < inorder.length; delimeterIndex++){
if (inorder[delimeterIndex] == postValue){
break;
}
}
// 切割中序遍历
int leftInorderStart = inStart;
int leftInorderEnd = delimeterIndex;
int rightInorderStart = delimeterIndex + 1;
int rightInorderEnd = inEnd;
// 切割后序遍历
int leftPostorderStart = postStart;
int leftPostorderEnd = leftPostorderStart + (delimeterIndex - leftInorderStart);
int rightPostorderStart = leftPostorderEnd;
int rightPostorderEnd = postEnd - 1;
root.left = splitOrder(inorder, leftInorderStart, leftInorderEnd, postorder, leftPostorderStart, leftPostorderEnd);
root.right = splitOrder(inorder, rightInorderStart, rightInorderEnd, postorder, rightPostorderStart, rightPostorderEnd);
return root;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
解决了后序和中序遍历序列构造二叉树,使用该思路还可以解决前序和中序遍历序列构造二叉树的问题。题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
该题代码如下:
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0 || inorder.length == 0){
return null;
}
return splitOrder(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
public TreeNode splitOrder(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){
if (preStart == preEnd){return null;}
// 前序是第一个元素为根元素
int preValue = preorder[preStart];
TreeNode root = new TreeNode(preValue);
int delimeterIndex = 0;
for (delimeterIndex = 0; delimeterIndex < inorder.length; delimeterIndex++){
if (inorder[delimeterIndex] == preValue){break;}
}
// 划分中序遍历
int leftInorderStart = inStart;
int leftInorderEnd = delimeterIndex;
int rightInorderStart = delimeterIndex + 1;
int rightInorderEnd = inEnd;
// 划分前序遍历
int leftPreorderStart = preStart + 1;
int leftPreorderEnd = leftPreorderStart + (delimeterIndex - leftInorderStart);
int rightPreorderStart = leftPreorderEnd;
int rightPreorderEnd = preEnd;
root.left = splitOrder(preorder, leftPreorderStart, leftPreorderEnd, inorder, leftInorderStart, leftInorderEnd);
root.right = splitOrder(preorder, rightPreorderStart, rightPreorderEnd, inorder, rightInorderStart, rightInorderEnd);
return root;
}
}
总结
今天的题目都可以理解思路,但是将思路转换为代码后,会因为一堆小错误导致无法得到预期结果,像是递归函数的停止条件代码写错导致递归提前结束,还有嵌套List的结果有误之类的问题。但是经过排查和解决后,对这些容易出问题的地方就留了个心眼,其实也是蛮好的。