本节内容
- 110.平衡二叉树
- 257. 二叉树的所有路径
- 404.左叶子之和
110.平衡二叉树 (优先掌握递归)※
建议:再一次涉及到,什么是高度,什么是深度,可以巩固一下。
题目链接: https://leetcode.cn/problems/balanced-binary-tree/
文章讲解: https://programmercarl.com/0110.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91.html
题目分析



递归调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution {
private boolean FLAG = true; public boolean isBalanced(TreeNode root) { recursion(root); return FLAG; }
private int recursion(TreeNode root) { if (root == null) return 0;
int recursionLwft = recursion(root.left); int recursionRight = recursion(root.right);
if (Math.abs(recursionLwft - recursionRight) > 1) { FLAG = false; } return Math.max(recursionLwft, recursionRight) + 1; } }
|
结果
解答成功:
执行耗时:0 ms,击败了100.00% 的Java用户
内存消耗:42 MB,击败了28.11% 的Java用户
分析
时间复杂度:
O( n )
空间复杂度:
O( logn )
代码随想录
https://programmercarl.com/0110.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91.html
强调一波概念:
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
但leetcode中强调的深度和高度很明显是按照节点来计算的,如图:

关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。
因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)
那么为什么104.二叉树的最大深度 (opens new window)中求的是二叉树的最大深度,也用的是后序遍历。
那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这棵树的最大深度,所以才可以使用后序遍历。
递归
递归三步曲分析:
- 明确递归函数的参数和返回值
参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。
那么如何标记左右子树是否差值大于1呢?
如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。
所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。
代码如下:
1 2
| int getHeight(TreeNode* node)
|
- 明确终止条件
递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
代码如下:
1 2 3
| if (node == NULL) { return 0; }
|
- 明确单层递归的逻辑
如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution {
public boolean isBalanced(TreeNode root) { return getHeight(root) != -1; }
private int getHeight(TreeNode root) { if (root == null) { return 0; } int leftHeight = getHeight(root.left); if (leftHeight == -1) { return -1; } int rightHeight = getHeight(root.right); if (rightHeight == -1) { return -1; } if (Math.abs(leftHeight - rightHeight) > 1) { return -1; } return Math.max(leftHeight, rightHeight) + 1; } }
|
迭代
我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
本题的迭代方式可以先定义一个函数,专门用来求高度。
这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)
然后再用栈来模拟后序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合
当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!
因为对于回溯算法已经是非常复杂的递归了,如果再用迭代的话,就是自己给自己找麻烦,效率也并不一定高。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| class Solution {
public boolean isBalanced(TreeNode root) { if (root == null) { return true; } Stack<TreeNode> stack = new Stack<>(); TreeNode pre = null; while (root!= null || !stack.isEmpty()) { while (root != null) { stack.push(root); root = root.left; } TreeNode inNode = stack.peek(); if (inNode.right == null || inNode.right == pre) { if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) { return false; } stack.pop(); pre = inNode; root = null; } else { root = inNode.right; } } return true; }
public int getHeight(TreeNode root) { if (root == null) { return 0; } Deque<TreeNode> deque = new LinkedList<>(); deque.offer(root); int depth = 0; while (!deque.isEmpty()) { int size = deque.size(); depth++; for (int i = 0; i < size; i++) { TreeNode poll = deque.poll(); if (poll.left != null) { deque.offer(poll.left); } if (poll.right != null) { deque.offer(poll.right); } } } return depth; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class Solution {
public boolean isBalanced(TreeNode root) { if (root == null) { return true; } Stack<TreeNode> stack = new Stack<>(); TreeNode pre = null; while (root != null || !stack.isEmpty()) { while (root != null) { stack.push(root); root = root.left; } TreeNode inNode = stack.peek(); if (inNode.right == null || inNode.right == pre) { if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) { return false; } stack.pop(); pre = inNode; root = null; } else { root = inNode.right; } } return true; }
public int getHeight(TreeNode root) { if (root == null) { return 0; } int leftHeight = root.left != null ? root.left.val : 0; int rightHeight = root.right != null ? root.right.val : 0; int height = Math.max(leftHeight, rightHeight) + 1; root.val = height; return height; } }
|
257. 二叉树的所有路径 (优先掌握递归)※
建议:这是大家第一次接触到回溯的过程, 我在视频里重点讲解了 本题为什么要有回溯,已经回溯的过程。
如果对回溯 似懂非懂,没关系, 可以先有个印象。
题目链接: https://leetcode.cn/problems/binary-tree-paths/
文章讲解: https://programmercarl.com/0257.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84.html
题目分析


递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { private List<String> result = new ArrayList<>(); public List<String> binaryTreePaths(TreeNode root) { String a = root.val + "";
if (root.left == null && root.right == null) { result.add(a); return result; } recursion(root.left, a); recursion(root.right, a);
return result; }
private void recursion(TreeNode root, String st){ if (root == null) return; if (root.left == null && root.right == null) { result.add(st + "->" + root.val); return; } recursion(root.left, st + "->" + root.val); recursion(root.right, st + "->" + root.val); } }
|
结果
解答成功:
执行耗时:8 ms,击败了34.03% 的Java用户
内存消耗:40.4 MB,击败了93.33% 的Java用户
分析
时间复杂度:
O( n )
递归 + 回溯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Solution {
public List<String> binaryTreePaths(TreeNode root) {
if (root == null) return new ArrayList<>(); List<String> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); recall(root, path, result); return result; }
private void recall(TreeNode root, List<Integer> path, List<String> result) { path.add(root.val); if (root.left == null && root.right == null) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < path.size() - 1; i++) { stringBuilder.append(path.get(i)).append("->"); } stringBuilder.append(path.get(path.size() - 1)); result.add(stringBuilder.toString()); return; }
if (root.left != null) { recall(root.left, path, result); path.remove(path.size() - 1); } if (root.right != null) { recall(root.right, path, result); path.remove(path.size() - 1); } } }
|
结果
解答成功:
执行耗时:1 ms,击败了100.00% 的Java用户
内存消耗:40.4 MB,击败了94.51% 的Java用户
迭代
结果
代码随想录
https://programmercarl.com/0257.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84.html
思路
这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。
在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。
前序遍历以及回溯的过程如图:

先使用递归的方式,来做前序遍历。要知道递归和回溯就是一家的,本题也需要回溯。
递归
1. 递归函数参数以及返回值
要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下:
1
| void traversal(TreeNode* cur, vector<int>& path, vector<string>& result)
|
2. 确定递归终止条件
在写递归的时候都习惯了这么写:
1 2 3
| if (cur == NULL) { 终止处理逻辑 }
|
但是本题的终止条件这样写会很麻烦,因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。
那么什么时候算是找到了叶子节点? 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。
所以本题的终止条件是:
1 2 3
| if (cur->left == NULL && cur->right == NULL) { 终止处理逻辑 }
|
为什么没有判断cur是否为空呢,因为下面的逻辑可以控制空节点不入循环。
再来看一下终止处理的逻辑。
这里使用vector 结构path来记录路径,所以要把vector 结构的path转为string格式,再把这个string 放进 result里。
那么为什么使用了vector 结构来记录路径呢? 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。
可能有的同学问了,我看有些人的代码也没有回溯啊。
其实是有回溯的,只不过隐藏在函数调用时的参数赋值里,下文我还会提到。
这里我们先使用vector结构的path容器来记录路径,那么终止处理逻辑如下:
1 2 3 4 5 6 7 8 9 10
| if (cur->left == NULL && cur->right == NULL) { string sPath; for (int i = 0; i < path.size() - 1; i++) { sPath += to_string(path[i]); sPath += "->"; } sPath += to_string(path[path.size() - 1]); result.push_back(sPath); return; }
|
3. 确定单层递归逻辑
因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。
path.push_back(cur->val);
然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。
所以递归前要加上判断语句,下面要递归的节点是否为空,如下
1 2 3 4 5 6
| if (cur->left) { traversal(cur->left, path, result); } if (cur->right) { traversal(cur->right, path, result); }
|
此时还没完,递归完,要做回溯啊,因为path 不能一直加入节点,它还要删节点,然后才能加入新的节点。
那么回溯要怎么回溯呢,一些同学会这么写,如下:
1 2 3 4 5 6 7
| if (cur->left) { traversal(cur->left, path, result); } if (cur->right) { traversal(cur->right, path, result); } path.pop_back();
|
这个回溯就有很大的问题,我们知道,回溯和递归是一一对应的,有一个递归,就要有一个回溯,这么写的话相当于把递归和回溯拆开了, 一个在花括号里,一个在花括号外。
所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!
那么代码应该这么写:
1 2 3 4 5 6 7 8
| if (cur->left) { traversal(cur->left, path, result); path.pop_back(); } if (cur->right) { traversal(cur->right, path, result); path.pop_back(); }
|
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Solution {
public List<String> binaryTreePaths(TreeNode root) { List<String> res = new ArrayList<>(); if (root == null) { return res; } List<Integer> paths = new ArrayList<>(); traversal(root, paths, res); return res; }
private void traversal(TreeNode root, List<Integer> paths, List<String> res) { paths.add(root.val); if (root.left == null && root.right == null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < paths.size() - 1; i++) { sb.append(paths.get(i)).append("->"); } sb.append(paths.get(paths.size() - 1)); res.add(sb.toString()); return; } if (root.left != null) { traversal(root.left, paths, res); paths.remove(paths.size() - 1); } if (root.right != null) { traversal(root.right, paths, res); paths.remove(paths.size() - 1); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution {
List<String> result = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) { deal(root, ""); return result; }
public void deal(TreeNode node, String s) { if (node == null) return; if (node.left == null && node.right == null) { result.add(new StringBuilder(s).append(node.val).toString()); return; } String tmp = new StringBuilder(s).append(node.val).append("->").toString(); deal(node.left, tmp); deal(node.right, tmp); } }
|
迭代法
至于非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程
这里除了模拟递归需要一个栈,同时还需要一个栈来存放对应的遍历路径。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Solution {
public List<String> binaryTreePaths(TreeNode root) { List<String> result = new ArrayList<>(); if (root == null) return result; Stack<Object> stack = new Stack<>(); stack.push(root); stack.push(root.val + ""); while (!stack.isEmpty()) { String path = (String) stack.pop(); TreeNode node = (TreeNode) stack.pop(); if (node.left == null && node.right == null) { result.add(path); } if (node.right != null) { stack.push(node.right); stack.push(path + "->" + node.right.val); } if (node.left != null) { stack.push(node.left); stack.push(path + "->" + node.left.val); } } return result; } }
|
404.左叶子之和 (优先掌握递归)※
建议:其实本题有点文字游戏,搞清楚什么是左叶子,剩下的就是二叉树的基本操作。
题目链接: https://leetcode.cn/problems/sum-of-left-leaves/
文章讲解: https://programmercarl.com/0404.%E5%B7%A6%E5%8F%B6%E5%AD%90%E4%B9%8B%E5%92%8C.html
题目分析


递归调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution {
private long result = 0L; public int sumOfLeftLeaves(TreeNode root) { recursion(root, false);
return (int) result; }
private void recursion(TreeNode root, boolean flag) { if (root == null) return;
if (flag && root.right == null && root.left == null) { result += root.val; } recursion(root.right, false); recursion(root.left, true); } }
|
结果
解答成功:
执行耗时:0 ms,击败了100.00% 的Java用户
内存消耗:39.1 MB,击败了89.16% 的Java用户
分析
时间复杂度:
O( n )
代码随想录
https://programmercarl.com/0404.%E5%B7%A6%E5%8F%B6%E5%AD%90%E4%B9%8B%E5%92%8C.html
思路
首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。
因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点
大家思考一下如下图中二叉树,左叶子之和究竟是多少?

其实是0,因为这棵树根本没有左叶子!
但看这个图的左叶子之和是多少?

相信通过这两个图,大家对最左叶子的定义有明确理解了。
那么判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子,判断代码如下:
1 2 3
| if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) { 左叶子节点处理逻辑 }
|
递归法
递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。
递归三部曲:
1. 确定递归函数的参数和返回值
判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int
使用题目中给出的函数就可以了。
2. 确定终止条件
如果遍历到空节点,那么左叶子值一定是0
1
| if (root == NULL) return 0;
|
注意,只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子。 所以如果当前遍历的节点是叶子节点,那其左叶子也必定是0,那么终止条件为:
1 2
| if (root == NULL) return 0; if (root->left == NULL && root->right== NULL) return 0;
|
3. 确定单层递归的逻辑
当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
代码如下:
1 2 3 4 5 6 7 8
| int leftValue = sumOfLeftLeaves(root->left); if (root->left && !root->left->left && !root->left->right) { leftValue = root->left->val; } int rightValue = sumOfLeftLeaves(root->right);
int sum = leftValue + rightValue; return sum;
|
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public int sumOfLeftLeaves(TreeNode root) { if (root == null) return 0; int leftValue = sumOfLeftLeaves(root.left); int rightValue = sumOfLeftLeaves(root.right); int midValue = 0; if (root.left != null && root.left.left == null && root.left.right == null) { midValue = root.left.val; } int sum = midValue + leftValue + rightValue; return sum; } }
|
迭代法
本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了,
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public int sumOfLeftLeaves(TreeNode root) { if (root == null) return 0; Stack<TreeNode> stack = new Stack<> (); stack.add(root); int result = 0; while (!stack.isEmpty()) { TreeNode node = stack.pop(); if (node.left != null && node.left.left == null && node.left.right == null) { result += node.left.val; } if (node.right != null) stack.add(node.right); if (node.left != null) stack.add(node.left); } return result; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Solution { public int sumOfLeftLeaves(TreeNode root) { int sum = 0; if (root == null) return 0; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); while (size -- > 0) { TreeNode node = queue.poll(); if (node.left != null) { queue.offer(node.left); if (node.left.left == null && node.left.right == null){ sum += node.left.val; } } if (node.right != null) queue.offer(node.right); } } return sum; } }
|