找树左下角的值
题目详细:LeetCode.513
由题可知:找出该二叉树的最底层、最左边
节点的值
- 最底层:节点所处的深度最深
- 最左边:统一深度的节点,优先取最左边的节点
因此,我们只要找到满足这两个条件的节点就是树的左下角的节点,难点在于如何找?
- 找最底层:定义一个变量,用于记录遍历到的节点的深度,以此来确定当前节点是不是最底层的节点
- 找最左边:优先找最左边的节点,那么在访问子节点时,左节点要优先于右节点被访问,所以这道题可以使用多种不同的遍历方式,如前序、后序等等,只要保证左节点优先被访问就行。
这道题使用迭代的方式,比递归要清晰易懂些。
既然我们要找最底层、最左边的节点,那么层序遍历从上到下、从左到右
的特点毫无疑问满足了我们所有的需求,只需要在遍历过程中,一直记录每一层最先访问的节点,那么当树遍历完时,返回记录的节点即是树左小角的值。
Java解法(迭代、层序遍历):
class Solution {
public int findBottomLeftValue(TreeNode root) {
return this.bfd(root);
}
public int bfd(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
if(null != root) queue.offer(root);
int leftNum = 0;
while(!queue.isEmpty()){
int n = queue.size();
boolean f_switch = true;
while(n-- > 0){
TreeNode node = queue.poll();
// 每一层从左到右访问,所以每一层最先访问的节点就是最左边的节点
if(f_switch){
leftNum = node.val;
f_switch = false;
}
if(null != node.left) queue.offer(node.left);
if(null != node.right) queue.offer(node.right);
}
}
return leftNum;
}
}
也可以使用递归,不过我们需要改动一下代码:
- 在递归函数中增加一个深度参数,记录当前节点的深度,在遍历过程中比较节点的深度,保证节点位于最底层的特点
- 采用前序遍历(或后序遍历),使每一次递归都优先访问左节点,保证节点位于最左边的特点
- 优化:因为最底层的节点一定是叶子节点,所以我们可以在遍历到叶子节点时,再比较已遍历的最大深度和当前节点的深度,以此来判断该叶子节点是否为最底层的节点。
Java解法(递归,前序遍历):
class Solution {
public int maxDepth = 0;
public int ans = 0;
public int findBottomLeftValue(TreeNode root) {
this.dfs(root, 1);
return this.ans;
}
public void dfs(TreeNode root, int depth){
if(null == root) return;
if(null == root.left && null == root.right){
if(depth > this.maxDepth){
this.ans = root.val;
this.maxDepth = depth;
}
}
if(null != root.left) this.dfs(root.left, depth + 1);
if(null != root.right) this.dfs(root.right, depth + 1);
}
}
路径总和
题目详细:LeetCode.112
由题可知:只要求判断树中是否能够找到一条路径总和满足目标值的路径
那么这道题的思路就比较简单了,就是对树进行深度优先遍历:
- 一边遍历一边累计路径上的节点值,到达叶子节点时,将路径总和和目标值做比较
- 当路径总和和目标值不想等时,说明当前路径是错误的
- 这时就需要回溯到上一个节点,进入其他路径,计算其他的路径总和
- 当存在一条路径总和与目标值相等时,返回true,否则返回false
由思路可知本题涉及到深度优先遍历和回溯法,那么我们可以利用递归法来解题:
Java解法(递归,深度优先遍历,前序遍历):
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
return dfs(root, targetSum, 0);
}
public boolean dfs(TreeNode root, int targetSum, int curSum){
if(null == root) return false;
curSum += root.val;
if(null == root.left && null == root.right){
if(curSum == targetSum) return true;
return false;
}
return dfs(root.left, targetSum, curSum) || dfs(root.right, targetSum, curSum);
}
}
Java解法(递归,回溯明显化):
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
return dfs(root, targetSum, 0);
}
public boolean dfs(TreeNode root, int targetSum, int curSum){
if(null == root) return false;
curSum += root.val;
if(null == root.left && null == root.right){
if(curSum == targetSum) return true;
return false;
}
boolean leftFlag = dfs(root.left, targetSum, curSum);
curSum -= root.val; // 回溯
curSum += root.val;
boolean rightFlag = dfs(root.right, targetSum, curSum);
curSum -= root.val; // 回溯
return leftFlag || rightFlag;
}
}
路径总和ii
题目详细:LeetCode.113
这道题与上一道题的不同点就在于:要求记录并输出所有满足目标总和的路径
仅此而已,那么把上一道题稍微一变:
- 在递归函数中增加两个参数:
- 参数一:路径列表,用来存放路径上的节点,表示一条路径
- 参数二:结果集,用来存放满足条件的所有路径
- 当到达叶子节点时
- 比较路径总和是否与目标总和相等,相等则添加到结果集
- 否则回溯到上一个节点,进入其他路径,计算其他的路径总和
- 直到所有节点都经历了访问和回溯,说明树遍历完毕,输出结果集即可
Java解法(递归,深度优先遍历):
class Solution {
public int target;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
this.target = targetSum;
this.dfs(root, path, 0, ans);
return ans;
}
public void dfs(TreeNode root, List<Integer> path, int sum, List<List<Integer>> ans){
if(null == root) return;
path.add(root.val);
sum += root.val;
if(null == root.left && null == root.right){
if(this.target == sum) ans.add(path);
}
// 对path进行拷贝后再传递,避免出现其他路径的节点(隐藏的回溯)
// 同时也使得path不会指向同一个引用,避免后续操作对path对象的影响
this.dfs(root.left, new ArrayList<Integer>(path), sum, ans);
this.dfs(root.right, new ArrayList<Integer>(path), sum, ans);
}
}
Java解法(递归,深度优先遍历,回溯明显化):
class Solution {
public int target;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<Integer> path = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
this.target = targetSum;
this.dfs(root, path, ans);
return ans;
}
public void dfs(TreeNode root, List<Integer> path, List<List<Integer>> ans){
if(null == root) return;
path.add(root.val);
if(null == root.left && null == root.right){
int pathSum = path.stream().reduce(0, (a,b) -> a + b);
List<Integer> clonedPath = path.stream()
.map(e -> Integer.valueOf(e))
.collect(Collectors.toList());
if(this.target == pathSum) ans.add(clonedPath);
}
this.dfs(root.left, path, ans);
path.remove(path.size() - 1); // 回溯
path.add(root.val);
this.dfs(root.right, path, ans);
path.remove(path.size() - 1); // 回溯
}
}
这道题还有一个更加需要注意的点,要注意递归参数的拷贝形式:
- 在Java中,拷贝的形式有两种,浅拷贝和深拷贝:
- 浅拷贝:又称引用传递,将对象的内存地址作为参数传递,后序对该参数进行的操作都会影响到原对象;
- 再举个浅拷贝的例子,也很重要,如果对列表进行浅拷贝,那么对列表中的元素也是浅拷贝操作,拷贝后的列表中的元素,与原列表中的元素都是来自同一个引用。
- 深拷贝:又称值传递,将对象作为参数传递,即整个对象包括对象的属性都进行一次拷贝,与原对象包括内部的属性都是不同的引用,对该参数包括其属性进行的操作都不会影响到原对象一丝一毫。
- Java默认的参数传递都是浅拷贝,需要我们自己去实现clone方法来进行深拷贝
- 例如,举个深拷贝的例子:
- 如果想对列表进行深拷贝,那么对列表中的元素也需要进行深拷贝操作,才是对列表完整的深拷贝
诸如 ArrayList 的 clone() 方法并不会进行深拷贝,而只会创建一个新的 ArrayList 对象,其中包含与原始 ArrayList 对象中的相同元素引用。
这意味着,如果原始 ArrayList 中的元素发生更改,克隆对象中对应的元素也会发生更改。
- 如果需要实现深拷贝,可以使用以下方法:
从前序与中序遍历序列构造二叉树
题目详细:LeetCode.105
学习二叉树理论的时候,也了解过构建二叉树的方法:
- 一棵
唯一的
二叉树可以通过中序遍历序列 + 前序遍历序列(或后序遍历序列)
来构建 - 前序遍历:顺序是根左右,所以其序列结果的节点分布状态,一定是根节点在前,子节点在后
- 中序遍历:顺序是左根右,所以当我们得到根节点时,只要在中序序列中找到根节点的位置,那么在序列中,其根节点位置之前的节点都是该节点的左子树上的节点,其根节点位置之后的节点都是该节点的右子树上的节点
由此可得:
- 构建一颗唯一的二叉树不仅需要前序序列来确定根节点,还需要中序序列来确定节点的左右分布
前序序列和后序序列无法确定一颗唯一的二叉树
,因为除了中序遍历序列以外,其他遍历序列都是无法确定树的子节点左右分布的,所以注意一定需要有中序遍历序列。
Java解法(递归):
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, inorder);
}
// 查找数组中目标值的索引
public static int findIndex(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
public TreeNode build(int[] preorder, int[] inorder){
// 特判,如果前序序列为空则说明是一棵空树
if(preorder.length == 0) return null;
int cur_val = preorder[0];
TreeNode root = new TreeNode(cur_val);
// 直到前序序列无法再分,说明到达叶子节点,开始回溯构建二叉树
if(preorder.length == 1) return root;
// 在中序遍历中确定当前节点的索引位置,以划分左子树和右子树的节点序列
int in_index = findIndex(inorder, cur_val);
preorder = Arrays.copyOfRange(preorder, 1, preorder.length);
// 分割左右节点序列,先确定中序遍历序列中左右节点序列的长度后,才能确定前序遍历序列中子节点的长度
int[] leftInorder = Arrays.copyOfRange(inorder, 0, in_index);
int[] rightInorder = Arrays.copyOfRange(inorder, in_index + 1, inorder.length);
int[] leftPreorder = Arrays.copyOfRange(preorder, 0, leftInorder.length);
int[] rightPreorder = Arrays.copyOfRange(preorder, 0 + leftInorder.length, preorder.length);
// 递归继续分割
root.left = this.build(leftPreorder, leftInorder);
root.right = this.build(rightPreorder, rightInorder);
return root;
}
}
从中序与后序遍历序列构造二叉树
题目详细:LeetCode.106
这一题与上一题有着异曲同工之妙,上一题是从中序与前序遍历序列构造二叉树,而这一题是从中序与后序遍历序列构造二叉树
不过两道题的主要问题都是一样的,区别就在于如何通过后序遍历序列来根节点?
由后序遍历的特点可知:
- 后序遍历:顺序是左右根,所以其序列结果的节点分布状态,一定是子节点在前,根节点在后
那么我们只需要修改两行代码,让它从后序遍历序列的尾部开始确定根节点,其他的代码都不用修改了:
Java解法(递归):
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
return build(postorder, inorder);
}
// 查找数组中目标值的索引
public static int findIndex(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
public TreeNode build(int[] postorder, int[] inorder){
// 特判,如果后序序列为空则说明是一棵空树
if(postorder.length == 0) return null;
int cur_val = postorder[postorder.length - 1];
TreeNode root = new TreeNode(cur_val);
// 直到后序序列无法再分,说明到达叶子节点,开始回溯构建二叉树
if(postorder.length == 1) return root;
// 在中序遍历中确定当前节点的索引位置,以划分左子树和右子树的节点序列
int in_index = findIndex(inorder, cur_val);
postorder = Arrays.copyOfRange(postorder, 0, postorder.length - 1);
// 分割左右节点序列,先确定中序遍历序列中左右节点序列的长度后,才能确定后序遍历序列中子节点的长度
int[] leftInorder = Arrays.copyOfRange(inorder, 0, in_index);
int[] rightInorder = Arrays.copyOfRange(inorder, in_index + 1, inorder.length);
int[] leftPostorder = Arrays.copyOfRange(postorder, 0, leftInorder.length);
int[] rightPostorder = Arrays.copyOfRange(postorder, 0 + leftInorder.length, postorder.length);
// 递归继续分割
root.left = this.build(leftPostorder, leftInorder);
root.right = this.build(rightPostorder, rightInorder);
return root;
}
}
时间过得真快啊,一天总感觉做了很多,但是却啥都没做,甚至想开始做时,却发现时间已经很晚了,什么时候才能让生活得更有效率呢,真是一天24小时都不够用啊:
子在川上曰:逝者如斯夫,不舍昼夜。