上接:【题解】—— LeetCode一周小结5
5.跳跃游戏 VI
题目链接:1696. 跳跃游戏 VI
给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。
一开始你在下标 0 处。每一步,你最多可以往前跳 k 步,但你不能跳出数组的边界。也就是说,你可以从下标 i 跳到 [i + 1, min(n - 1, i + k)] 包含 两个端点的任意位置。
你的目标是到达数组最后一个位置(下标为 n - 1 ),你的 得分 为经过的所有数字之和。
请你返回你能得到的 最大得分 。
示例 1:
输入:nums = [1,-1,-2,4,-7,3], k = 2
输出:7
解释:你可以选择子序列 [1,-1,4,3] (上面加粗的数字),和为 7 。
示例 2:
输入:nums = [10,-5,-2,4,0,3], k = 3
输出:17
解释:你可以选择子序列 [10,4,3] (上面加粗数字),和为 17 。
示例 3:
输入:nums = [1,-5,-20,4,-1,3,-6,-3], k = 2
输出:0
提示:
1 <= nums.length, k <= 105
-104 <= nums[i] <= 104
题解:
方法:动态规划 + 优先队列优化
动态规划的思想,具体来说是使用单调递减队列来优化动态规划过程。以下是算法的主要思路:
- 我们使用一个优先队列(PriorityQueue)来存储二元组 (f[j], j),其中
f[j]
表示到达位置j
的最大得分,j
表示位置的索引。 - 初始时,将第一个元素
(nums[0], 0)
加入队列,并初始化ans
为nums[0]
。 - 遍历数组
nums
,从位置1
开始,对于每个位置i
,执行以下步骤:
- 移除队列中索引超过限制
k
的元素,确保队列中的元素满足限制条件。 - 计算当前位置的得分
ans
,更新为队列中最大得分加上当前位置的得分nums[i]
。 - 将当前位置
(ans, i)
加入队列。
- 返回最终的
ans
,即到达最后一个位置的最大得分。
一个递减的得分队列可以确保队列中的元素始终按照得分递减的顺序排列。这样做的好处是在动态规划的过程中,我们只需关注在当前位置之前的 k
个位置中得分最大的情况,而不需要考虑更远的位置,从而减少了计算的复杂度。
import java.util.PriorityQueue; class Solution { public int maxResult(int[] nums, int k) { int n = nums.length; // 优先队列,元素为数组 [f[j], j] PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[0] - a[0]); pq.offer(new int[]{nums[0], 0}); int ans = nums[0]; for (int i = 1; i < n; ++i) { // 如果堆顶元素的索引与当前索引之差大于k,则移除堆顶元素 while (i - pq.peek()[1] > k) { pq.poll(); } ans = pq.peek()[0] + nums[i]; pq.offer(new int[]{ans, i}); } return ans; } }
方法:动态规划 + 单调队列优化
使用动态规划和单调队列的结合,以下是主要思路:
- 动态规划: 定义
dp[i]
表示到达位置i
的最大得分。初始时,dp[0] = nums[0]
。 - 单调队列: 使用双端队列(deque)维护一个递减的队列,队列中保存的是二元组
(dp[j], j)
。队列的头部始终保持最大的dp[j]
。 - 遍历数组:从位置
1
开始遍历数组nums
,对于每个位置i
,执行以下步骤:
- 移除队列中索引超过限制
k
的元素,确保队列中的元素满足限制条件。 - 计算当前位置的得分
dp[i]
,更新为队列中最大得分加上当前位置的得分nums[i]
。 - 将当前位置
(dp[i], i)
加入队列。 - 移除队列中不满足单调性的元素,确保队列中的元素按照得分递减的顺序排列。
- 返回最后一个位置的最大得分
dp[n-1]
。
这种做法的关键点在于使用单调队列维护一个在滑动窗口内的最大得分,并通过动态规划的思想递推计算每个位置的最大得分。这样可以有效减少不必要的计算,提高算法的效率。
import java.util.ArrayDeque; import java.util.Deque; class Solution { public int maxResult(int[] nums, int k) { int n = nums.length; // 单调队列,元素为数组 [f[j], j] Deque<int[]> q = new ArrayDeque<>(); q.offerLast(new int[]{nums[0], 0}); int ans = nums[0]; for (int i = 1; i < n; ++i) { // 移除队首元素,直到队首的 j 满足限制 while (i - q.peekFirst()[1] > k) { q.pollFirst(); } ans = q.peekFirst()[0] + nums[i]; // 移除队尾元素,直到队尾的 j 满足单调性 while (!q.isEmpty() && ans >= q.peekLast()[0]) { q.pollLast(); } q.offerLast(new int[]{ans, i}); } return ans; } }
6.魔塔游戏
题目链接:LCP 30. 魔塔游戏
小扣当前位于魔塔游戏第一层,共有 N 个房间,编号为 0 ~ N-1。每个房间的补血道具/怪物对于血量影响记于数组 nums,其中正数表示道具补血数值,即血量增加对应数值;负数表示怪物造成伤害值,即血量减少对应数值;0 表示房间对血量无影响。
小扣初始血量为 1,且无上限。假定小扣原计划按房间编号升序访问所有房间补血/打怪,为保证血量始终为正值,小扣需对房间访问顺序进行调整,每次仅能将一个怪物房间(负数的房间)调整至访问顺序末尾。请返回小扣最少需要调整几次,才能顺利访问所有房间。若调整顺序也无法访问完全部房间,请返回 -1。
示例 1:
输入:nums = [100,100,100,-250,-60,-140,-50,-50,100,150]
输出:1
解释:初始血量为 1。至少需要将 nums[3] 调整至访问顺序末尾以满足要求。
示例 2:
输入:nums = [-200,-300,400,0]
输出:-1
解释:调整访问顺序也无法完成全部房间的访问。
提示:
1 <= nums.length <= 105
-105 <= nums[i] <= 105
题解:
方法:贪心+优先队列
通过模拟小扣在每个房间的行走过程,使用优先队列来保存怪物房间的伤害,确保在小扣的血量小于等于0时调整访问顺序。以下是思路:
- 计算总血量: 首先计算小扣在所有回合结束后的总血量
sum
,遍历nums
数组,将每个元素累加到sum
中。 - 检查总血量是否为正: 如果总血量
sum
小于等于0,表示小扣无法在所有回合结束后保持正血量,直接返回 -1。 - 开始模拟过程: 初始化小扣的血量
blood
为1,初始化一个优先队列pq
用来保存怪物房间的伤害。同时初始化last
记录调整的次数。 - 遍历每个房间:使用
for
循环遍历nums
数组,对于每个房间进行以下操作:
- 如果当前房间为怪物房间(
nums[i] < 0
),将其伤害值加入优先队列pq
中。 - 如果加入当前怪物房间的伤害后,小扣的血量
blood + nums[i]
小于等于0,说明小扣在这回合结束后将会死亡,需要将之前扣除最多的血量的怪物房间移到末尾。记录移动的次数last++
,同时从队列中弹出最大伤害的怪物,将其伤害值加回小扣的血量中。 - 更新小扣的血量,将当前房间的血量变化加到小扣的血量中。
- 返回结果: 返回调整的次数
last
。
import java.util.PriorityQueue; class Solution { public int magicTower(int[] nums) { int sum = 1; // 计算初始血量 for (int i = 0; i < nums.length; i++) { sum += nums[i]; } // 如果所有回合后的血量为非正数,返回 -1 if (sum <= 0) { return -1; } // 开始模拟 long blood = 1; // 初始血量 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 优先队列用于保存怪物房间的伤害 int last = 0; // 记录调整的次数 for (int i = 0; i < nums.length; i++) { if (nums[i] < 0) { pq.offer(nums[i]); // 将怪物房间的伤害加入队列 // 如果加入当前怪物房间的伤害后,小扣的血量小于等于0 if (blood + nums[i] <= 0) { last++; // 记录移动的次数 blood -= pq.poll(); // 加回之前扣除最多的血量 } } blood += nums[i]; // 更新小扣的血量 } return last; // 返回调整的次数 } }
7.二叉树的堂兄弟节点 II
题目链接:2641. 二叉树的堂兄弟节点 II
给你一棵二叉树的根 root ,请你将每个节点的值替换成该节点的所有 堂兄弟节点值的和 。
如果两个节点在树中有相同的深度且它们的父节点不同,那么它们互为 堂兄弟 。
请你返回修改值之后,树的根 root 。
注意,一个节点的深度指的是从树根节点到这个节点经过的边数。
示例 1:
输入:root = [5,4,9,1,10,null,7]
输出:[0,0,0,7,7,null,11]
解释:上图展示了初始的二叉树和修改每个节点的值之后的二叉树。
- 值为 5 的节点没有堂兄弟,所以值修改为 0 。
- 值为 4 的节点没有堂兄弟,所以值修改为 0 。
- 值为 9 的节点没有堂兄弟,所以值修改为 0 。
- 值为 1 的节点有一个堂兄弟,值为 7 ,所以值修改为 7 。
- 值为 10 的节点有一个堂兄弟,值为 7 ,所以值修改为 7 。
- 值为 7 的节点有两个堂兄弟,值分别为 1 和 10 ,所以值修改为 11 。
示例 2:
输入:root = [3,1,2]
输出:[0,0,0]
解释:上图展示了初始的二叉树和修改每个节点的值之后的二叉树。
- 值为 3 的节点没有堂兄弟,所以值修改为 0 。
- 值为 1 的节点没有堂兄弟,所以值修改为 0 。
- 值为 2 的节点没有堂兄弟,所以值修改为 0 。
提示:
树中节点数目的范围是 [1, 105] 。
1 <= Node.val <= 104
题解:
方法:BFS(广度优先搜索)遍历二叉树
使用BFS(广度优先搜索)进行了两次遍历二叉树:
- 初始化根节点值为0: 将根节点的值设置为0。
- 第一次 BFS 遍历: 使用 BFS 遍历二叉树,计算出每一层的节点值之和
nextLevelSum
,同时将当前层的所有节点加入队列q
中。 - 第二次 BFS 遍历: 再次遍历每个节点,计算其子节点值之和,并用
nextLevelSum
减去子节点值之和来更新子节点的值。 - 重复以上步骤直至队列为空: 重复进行第一次和第二次 BFS 遍历,直到队列为空,表示已经遍历完所有层级的节点。
- 返回根节点: 返回经过替换处理后的根节点。
/** * 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; * } * } */ import java.util.ArrayList; import java.util.List; class Solution { public TreeNode replaceValueInTree(TreeNode root) { root.val = 0; List<TreeNode> q = List.of(root); while (!q.isEmpty()) { List<TreeNode> tmp = q; q = new ArrayList<>(); // 计算下一层的节点值之和 int nextLevelSum = 0; for (TreeNode node : tmp) { if (node.left != null) { q.add(node.left); nextLevelSum += node.left.val; } if (node.right != null) { q.add(node.right); nextLevelSum += node.right.val; } } // 再次遍历,更新下一层的节点值 for (TreeNode node : tmp) { // 计算当前节点子节点的值之和 int childrenSum = (node.left != null ? node.left.val : 0) + (node.right != null ? node.right.val : 0); // 更新子节点的值为下一层节点值之和减去子节点值之和 if (node.left != null) node.left.val = nextLevelSum - childrenSum; if (node.right != null) node.right.val = nextLevelSum - childrenSum; } } return root; } }
8.二叉树的堂兄弟节点
题目链接:993. 二叉树的堂兄弟节点
在二叉树中,根节点位于深度 0 处,每个深度为 k 的节点的子节点位于深度 k+1 处。
如果二叉树的两个节点深度相同,但 父节点不同 ,则它们是一对堂兄弟节点。
我们给出了具有唯一值的二叉树的根节点 root ,以及树中两个不同节点的值 x 和 y 。
只有与值 x 和 y 对应的节点是堂兄弟节点时,才返回 true 。否则,返回 false。
示例 1:
输入:root = [1,2,3,4], x = 4, y = 3
输出:false
示例 2:
输入:root = [1,2,3,null,4,null,5], x = 5, y = 4
输出:true
示例 3:
输入:root = [1,2,3,null,4], x = 2, y = 3
输出:false
提示:
二叉树的节点数介于 2 到 100 之间。
每个节点的值都是唯一的、范围为 1 到 100 的整数。
题解:
方法:DFS
希望得到某个节点的父节点&所在深度,:
/** * 查找 t 的「父节点值」&「所在深度」 * @param root 当前搜索到的节点 * @param fa root 的父节点 * @param depth 当前深度 * @param t 搜索目标值 * @return [fa.val, depth] */ int[] dfs(TreeNode root, TreeNode fa, int depth, int t);
之后按照遍历的逻辑处理即可。
需要注意的时,我们需要区分出搜索不到和**搜索对象为 root(没有 fa 父节点)**两种情况。
我们约定使用 −1 代指没有找到目标值 t,使用 0 代表找到了目标值 t,但其不存在父节点。
/** * 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 isCousins(TreeNode root, int x, int y) { int[] xi = dfs(root, null, 0, x); int[] yi = dfs(root, null, 0, y); return xi[1] == yi[1] && xi[0] != yi[0]; } int[] dfs(TreeNode root, TreeNode fa, int depth, int t) { if (root == null) return new int[]{-1, -1}; // 使用 -1 代表为搜索不到 t if (root.val == t) { return new int[]{fa != null ? fa.val : 1, depth}; // 使用 1 代表搜索值 t 为 root } int[] l = dfs(root.left, root, depth + 1, t); if (l[0] != -1) return l; return dfs(root.right, root, depth + 1, t); } }
方法:BFS
class Solution { public boolean isCousins(TreeNode root, int x, int y) { int[] xi = bfs(root, x); int[] yi = bfs(root, y); return xi[1] == yi[1] && xi[0] != yi[0]; } int[] bfs(TreeNode root, int t) { Deque<Object[]> d = new ArrayDeque<>(); // 存储值为 [cur, fa, depth] d.addLast(new Object[]{root, null, 0}); while (!d.isEmpty()) { int size = d.size(); while (size-- > 0) { Object[] poll = d.pollFirst(); TreeNode cur = (TreeNode)poll[0], fa = (TreeNode)poll[1]; int depth = (Integer)poll[2]; if (cur.val == t) return new int[]{fa != null ? fa.val : 0, depth}; if (cur.left != null) d.addLast(new Object[]{cur.left, cur, depth + 1}); if (cur.right != null) d.addLast(new Object[]{cur.right, cur, depth + 1}); } } return new int[]{-1, -1}; } }
9.二叉树的最近公共祖先
题目链接:236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
题解:
方法:DFS
查找二叉树中指定节点 p 和 q 的最近公共祖先节点。通过递归遍历左右子树,找到 p 和 q 的最近公共祖先节点。
这个递归解法通过深度优先搜索(DFS)来查找二叉树中节点 p 和节点 q 的最近公共祖先。
递归解析:
- 递归终止条件: 在递归的过程中,首先检查当前节点是否为空,或者当前节点是否为节点 p 或节点 q。如果是的话,直接返回当前节点,因为当前节点就是最近公共祖先。
- 递归向左子树查找: 若当前节点不是节点 p 或节点 q,则递归调用函数
lowestCommonAncestor
在左子树中查找节点 p 和节点 q 的最近公共祖先。递归调用的结果存储在变量left
中。 - 递归向右子树查找: 同样地,递归调用函数
lowestCommonAncestor
在右子树中查找节点 p 和节点 q 的最近公共祖先,结果存储在变量right
中。 - 判断最近公共祖先:最后,根据递归调用的结果
left
和right
,判断以下情况:
- 如果
left
和right
都不为空,说明节点 p 和节点 q 分别位于当前节点的左右子树中,因此当前节点就是最近公共祖先,直接返回当前节点。 - 如果
left
为空而right
不为空,说明节点 p 和节点 q 同时位于右子树中,且右子树中有它们的最近公共祖先,因此返回right
。 - 如果
left
不为空而right
为空,说明节点 p 和节点 q 同时位于左子树中,且左子树中有它们的最近公共祖先,因此返回left
。 - 如果
left
和right
都为空,说明左右子树中都不包含节点 p 和节点 q,返回null
。
- 返回结果: 最终返回最近公共祖先节点。
这个递归解法的时间复杂度为 O(N),其中 N 是二叉树中节点的数量,因为每个节点最多被访问一次。
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ class Solution { /** * 找到二叉树中指定节点 p 和 q 的最近公共祖先节点。 * @param root 根节点 * @param p 节点 p * @param q 节点 q * @return 最近公共祖先节点 */ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // 如果根节点为空,或者根节点为 p 或 q,直接返回根节点 if (root == null || root == p || root == q) { return root; } // 递归在左子树中寻找 p 和 q 的最近公共祖先节点 TreeNode left = lowestCommonAncestor(root.left, p, q); // 递归在右子树中寻找 p 和 q 的最近公共祖先节点 TreeNode right = lowestCommonAncestor(root.right, p, q); // 如果左子树中未找到 p 和 q 的公共祖先,则返回右子树的结果 if (left == null) { return right; } // 如果右子树中未找到 p 和 q 的公共祖先,则返回左子树的结果 if (right == null) { return left; } // 如果左右子树都找到了 p 和 q 的公共祖先,则当前节点为最近公共祖先节点 return root; } }
4种情况的展开写法如下:
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root == null || root == p || root == q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if(left == null && right == null) return null; // 4.如果 `left` 和 `right` 都为空 if(left == null) return right; // 2.如果 `left` 为空而 `right` 不为空 if(right == null) return left; // 3.如果 `left` 不为空而 `right` 为空 return root; // 1. if(left != null and right != null)如果 `left` 和 `right` 都不为空 } }
10. 二叉树的中序遍历
题目链接:94. 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
题解:
方法一:递归遍历
先递归左子树,再访问根节点,接着递归右子树。
/** * 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; * } * } */ import java.util.*; class Solution { private List<Integer> ans = new ArrayList<>(); // 用于存放中序遍历结果的列表 /** * 执行中序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 中序遍历结果列表 */ public List<Integer> inorderTraversal(TreeNode root) { dfs(root); // 调用深度优先搜索函数进行中序遍历 return ans; // 返回中序遍历结果列表 } /** * 深度优先搜索函数,递归遍历二叉树,并将节点值存入结果列表。 * @param root 当前处理的节点 */ private void dfs(TreeNode root) { if (root == null) { // 递归终止条件:当前节点为空 return; } dfs(root.left); // 递归遍历左子树 ans.add(root.val); // 将当前节点值加入结果列表 dfs(root.right); // 递归遍历右子树 } }
时间复杂度 O(n),空间复杂度 O(n)。其中 n 是二叉树的节点数,空间复杂度主要取决于递归调用的栈空间。
方法二:栈实现非递归遍历
二叉树的中序遍历,使用了迭代的方式,并且使用了辅助栈来模拟递归过程。
非递归的思路如下:
- 定义一个栈 stk
- 将树的左节点依次入栈
- 左节点为空时,弹出栈顶元素并处理
- 重复 2~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; * } * } */ import java.util.*; class Solution { /** * 执行中序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 中序遍历结果列表 */ public List<Integer> inorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); // 用于存放中序遍历结果的列表 Deque<TreeNode> stk = new ArrayDeque<>(); // 辅助栈,用于遍历过程中保存节点 // 遍历二叉树 while (root != null || !stk.isEmpty()) { if (root != null) { stk.push(root); // 将当前节点入栈 root = root.left; // 移动到左子节点 } else { root = stk.pop(); // 栈顶节点出栈 ans.add(root.val); // 访问栈顶节点的值,因为是中序遍历,所以此时是左子树的最左侧节点 root = root.right; // 移动到右子节点 } } return ans; // 返回中序遍历结果列表 } }
方法三:Morris 实现中序遍历
Morris 遍历无需使用栈,空间复杂度为 O(1)。核心思想是:
遍历二叉树节点,
- 若当前节点 root 的左子树为空,将当前节点值添加至结果列表 ans 中,并将当前节点更新为 root.right
- 若当前节点 root 的左子树不为空,找到左子树的最右节点 prev(也即是 root 节点在中序遍历下的前驱节点):
- 若前驱节点 prev 的右子树为空,将前驱节点的右子树指向当前节点 root,并将当前节点更新为 root.left。
- 若前驱节点 prev 的右子树不为空,将当前节点值添加至结果列表 ans 中,然后将前驱节点右子树指向空(即解除 prev 与 root 的指向关系),并将当前节点更新为 root.right。
- 循环以上步骤,直至二叉树节点为空,遍历结束。
/** * 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; * } * } */ import java.util.*; class Solution { /** * 执行中序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 中序遍历结果列表 */ public List<Integer> inorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); // 用于存放中序遍历结果的列表 // 当根节点不为空时,执行遍历 while (root != null) { if (root.left == null) { // 如果当前节点的左子节点为空 ans.add(root.val); // 将当前节点的值加入结果列表 root = root.right; // 移动到当前节点的右子节点 } else { // 如果当前节点的左子节点不为空 TreeNode prev = root.left; // 找到当前节点的左子树的最右下角节点 while (prev.right != null && prev.right != root) { prev = prev.right; } if (prev.right == null) { // 如果最右下角节点的右子节点为空 prev.right = root; // 将最右下角节点的右子节点指向当前节点 root = root.left; // 移动到当前节点的左子节点 } else { // 如果最右下角节点的右子节点不为空 ans.add(root.val); // 将当前节点的值加入结果列表 prev.right = null; // 恢复最右下角节点的右子节点为null root = root.right; // 移动到当前节点的右子节点 } } } return ans; // 返回中序遍历结果列表 } }
11. 二叉树的前序遍历
题目链接:144. 二叉树的前序遍历
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[1,2]
示例 5:
输入:root = [1,null,2]
输出:[1,2]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶:递归算法很简单,你可以通过迭代算法完成吗?
题解:
方法一:递归遍历
我们先访问根节点,然后递归左子树和右子树。
/** * 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; * } * } */ import java.util.*; class Solution { private List<Integer> ans = new ArrayList<>(); // 用于存放前序遍历结果的列表 /** * 执行前序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 前序遍历结果列表 */ public List<Integer> preorderTraversal(TreeNode root) { dfs(root); // 调用深度优先搜索函数进行前序遍历 return ans; // 返回前序遍历结果列表 } /** * 深度优先搜索函数,递归遍历二叉树,并将节点值存入结果列表。 * @param root 当前处理的节点 */ private void dfs(TreeNode root) { if (root == null) { // 递归终止条件:当前节点为空 return; } ans.add(root.val); // 将当前节点值加入结果列表 dfs(root.left); // 递归遍历左子树 dfs(root.right); // 递归遍历右子树 } }
方法二:栈实现非递归遍历
使用了迭代的方式,并且利用了辅助栈来模拟递归过程,实现了二叉树的前序遍历。
栈实现非递归遍历的思路如下:
- 定义一个栈 stk,先将根节点压入栈
- 若栈不为空,每次从栈中弹出一个节点
- 处理该节点
- 先把节点右孩子压入栈,接着把节点左孩子压入栈(如果有孩子节点)
- 重复 2-4
- 返回结果
/** * 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; * } * } */ import java.util.*; class Solution { /** * 执行前序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 前序遍历结果列表 */ public List<Integer> preorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); // 用于存放前序遍历结果的列表 if (root == null) { // 如果根节点为空,直接返回空列表 return ans; } Deque<TreeNode> stk = new ArrayDeque<>(); // 辅助栈,用于遍历过程中保存节点 // 将根节点入栈 stk.push(root); // 开始遍历,直到栈为空 while (!stk.isEmpty()) { TreeNode node = stk.pop(); // 弹出栈顶节点 ans.add(node.val); // 将当前节点值加入结果列表 if (node.right != null) { // 如果当前节点有右子节点,将右子节点入栈 stk.push(node.right); } if (node.left != null) { // 如果当前节点有左子节点,将左子节点入栈 stk.push(node.left); } } return ans; // 返回前序遍历结果列表 } }
方法三:Morris 实现前序遍历
利用 Morris Traversal 的思想实现了二叉树的前序遍历,其时间复杂度为 O(n),其中 n 是二叉树中的节点数。Morris Traversal 算法的优点是空间复杂度为 O(1),不需要额外的栈空间。核心思想是:
遍历二叉树节点,
- 若当前节点 root 的左子树为空,将当前节点值添加至结果列表 ansansans 中,并将当前节点更新为 root.right
- 若当前节点 root 的左子树不为空,找到左子树的最右节点 pre(也即是 root 节点在中序遍历下的前驱节点):
- 若前驱节点 pre 的右子树为空,将当前节点值添加至结果列表 ansansans 中,然后将前驱节点的右子树指向当前节点 root,并将当前节点更新为 root.left。
- 若前驱节点 pre 的右子树不为空,将前驱节点右子树指向空(即解除 pre 与 root 的指向关系),并将当前节点更新为 root.right。
- 循环以上步骤,直至二叉树节点为空,遍历结束。
/** * 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; * } * } */ import java.util.ArrayList; import java.util.List; class Solution { /** * 执行前序遍历,返回遍历结果列表。 * @param root 二叉树的根节点 * @return 前序遍历结果列表 */ public List<Integer> preorderTraversal(TreeNode root) { List<Integer> ans = new ArrayList<>(); // 用于存放前序遍历结果的列表 while (root != null) { if (root.left == null) { // 如果当前节点的左子节点为空 ans.add(root.val); // 将当前节点的值加入结果列表 root = root.right; // 移动到当前节点的右子节点 } else { // 如果当前节点的左子节点不为空 TreeNode prev = root.left; // 找到当前节点的左子树的最右下角节点 while (prev.right != null && prev.right != root) { prev = prev.right; // 寻找当前节点的左子树的最右下角节点 } if (prev.right == null) { // 如果最右下角节点的右子节点为空 ans.add(root.val); // 将当前节点的值加入结果列表 prev.right = root; // 将最右下角节点的右子节点指向当前节点,建立线索 root = root.left; // 移动到当前节点的左子节点 } else { // 如果最右下角节点的右子节点不为空 prev.right = null; // 恢复最右下角节点的右子节点为空,断开线索 root = root.right; // 移动到当前节点的右子节点 } } } return ans; // 返回前序遍历结果列表 } }
下接:【题解】—— LeetCode一周小结7