题目 | 难度 | 备注 |
---|---|---|
2471. 逐层排序二叉树所需的最少操作数目 | 1635 | BFS + 置换环 + 离散化 |
2641. 二叉树的堂兄弟节点 II | 1677 | BFS |
周赛二叉树问题
2471. 逐层排序二叉树所需的最少操作数目
难度中等27
给你一个 值互不相同 的二叉树的根节点 root
。
在一步操作中,你可以选择 同一层 上任意两个节点,交换这两个节点的值。
返回每一层按 严格递增顺序 排序所需的最少操作数目。
节点的 层数 是该节点和根节点之间的路径的边数。
示例 1 :
输入:root = [1,4,3,7,6,8,5,null,null,null,null,9,null,10]
输出:3
解释:
- 交换 4 和 3 。第 2 层变为 [3,4] 。
- 交换 7 和 5 。第 3 层变为 [5,6,8,7] 。
- 交换 8 和 7 。第 3 层变为 [5,6,7,8] 。
共计用了 3 步操作,所以返回 3 。
可以证明 3 是需要的最少操作数目。
示例 2 :
输入:root = [1,3,2,7,6,5,4]
输出:3
解释:
- 交换 3 和 2 。第 2 层变为 [2,3] 。
- 交换 7 和 4 。第 3 层变为 [4,6,5,7] 。
- 交换 6 和 5 。第 3 层变为 [4,5,6,7] 。
共计用了 3 步操作,所以返回 3 。
可以证明 3 是需要的最少操作数目。
提示:
- 树中节点的数目在范围
[1, 105]
。 1 <= Node.val <= 105
- 树中的所有值 互不相同 。
题解:https://leetcode.cn/problems/minimum-number-of-operations-to-sort-a-binary-tree-by-level/solution/by-liu-wan-qing-zjlj/
以计算使得 [7, 6, 8, 5] 严格递增需要的最小交换次数为例,画出置换环算法图解如下:
经典问题:置换环求解数组排序需要的最小交换次数
置换环的思想为 : 对每个节点,将其指向其排序后应该放到的位置,直到首位相接形成了一个环。
具体实现思想:
- 使用 Map 哈希表记录每个节点值以及其应该放到的位置
- 从头到尾遍历初始数组,使用
flag[]
数组标记当前元素是否已经参与过(即已经被加入环中),对已经参与过的数组则不再需要遍历。每次成环结束,记录成环个数 loop。 - 最终最小交换次数为 :数组长度 - 成环个数(
nums.size() - loop
)
置换环算法一般情况代码如下:
// 返回使得 nums 递增需要的最小交换元素次数
public int minChanges(int[] nums){
int[] copy = Arrays.copyOf(nums, nums.length);
Arrays.sort(copy); // 排序,获得有序的nums数组copy
// 离散化:记录元素原本应该出现的位置 i
HashMap<Integer, Integer> map = new HashMap<>();
for(int i=0; i<copy.length; i++){
map.put(copy[i], i);
}
boolean[] flag = new boolean[nums.length]; // 用于标记 nums[i] 是否已经被加入环中
int loop = 0; // 环的个数
for(int i=0; i<nums.length; i++){
if(!flag[i]){
int j = i;
while(!flag[j]){ // 画环
int index = map.get(nums[j]); // 当前节点指向的实际存放位置,画环过程
flag[j] = true; // 将 j 加入环中
j = index; // 将当前节点移动到环上下个节点
}
loop++; // 环数递增
}
}
return nums.length - loop; // 最小交换次数为 : 数组长度 - 环数
}
代码实现
class Solution {
public int minimumOperations(TreeNode root) {
int res = 0;
if(root == null) return res;
Deque<TreeNode> dq = new ArrayDeque<>();
dq.add(root);
while(!dq.isEmpty()){
List<Integer> levellist = new ArrayList<>();
int size = dq.size();
while(size-- > 0){
TreeNode node = dq.poll();
levellist.add(node.val);
if(node.left != null) dq.add(node.left);
if(node.right != null) dq.add(node.right);
}
// 计算该数组排序需要的最小交换次数:置换环
int loop = 0;
List<Integer> tmp = new ArrayList<>(levellist);
Collections.sort(levellist);
boolean[] flag = new boolean[tmp.size()];
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < tmp.size(); i++){
map.put(levellist.get(i), i);
}
for(int i = 0; i < tmp.size(); i++){
if(!flag[i]){
int j = i;
while(!flag[j]){
int index = map.get(tmp.get(j));
flag[j] = true;
j = index;
}
loop++;
}
}
res += (tmp.size() - loop);
}
return res;
}
}
2641. 二叉树的堂兄弟节点 II
难度中等0
给你一棵二叉树的根 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遍历
https://leetcode.cn/problems/cousins-in-binary-tree-ii/solution/bfssuan-liang-ci-pythonjavacgo-by-endles-b72a/
站在父节点的视角,去看下一层节点的取值:(站在父节点的位置上解决子节点的问题)!!!启发点:对于一个节点 x 来说,它的所有堂兄弟节点值的和,等价于 x 这层的所有节点值之和,减去 x 及其兄弟节点的值之和
BFS
层序遍历二叉树,对于每一层:
首先,遍历当前层的每个节点,通过节点的左右儿子,计算下层的节点值之和 nextLevelSum
;
然后,再次遍历当前层的每个节点 x,计算 x 的左右儿子的节点值之和 childrenSum
,更新 x 的左右儿子的节点值为nextLevelSum
- childrenSum
。
class Solution {
public TreeNode replaceValueInTree(TreeNode root) {
root.val = 0;
// 使用List和临时变量tmp来模拟队列(Deque没有get方法)
List<TreeNode> q = new ArrayList<>();
q.add(root);
while(!q.isEmpty()){
// 用临时遍历保存本层节点,后面需要计算本层节点x左右儿子的节点值之和
List<TreeNode> tmp = q;
q = new ArrayList<>();
int nextLevelSum = 0; // 下一层的节点之和
// 获取下层节点和的同时进行BFS操作
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 childSum = (node.left != null ? node.left.val : 0) +
(node.right != null ? node.right.val : 0);
if(node.left != null) node.left.val = nextLevelSum - childSum;
if(node.right != null) node.right.val = nextLevelSum - childSum;
}
}
return root;
}
}
其他
1080. 根到叶路径上的不足节点
难度中等126
给你二叉树的根节点 root
和一个整数 limit
,请你同时删除树中所有 不足节点 ,并返回最终二叉树的根节点。
假如通过节点 node
的每种可能的 “根-叶” 路径上值的总和全都小于给定的 limit
,则该节点被称之为 不足节点 ,需要被删除。
叶子节点,就是没有子节点的节点。
示例 1:
输入:root = [1,2,3,4,-99,-99,7,8,9,-99,-99,12,13,-99,14], limit = 1
输出:[1,2,3,4,null,null,7,8,9,null,14]
示例 2:
输入:root = [5,4,8,11,null,17,4,7,1,null,null,5,3], limit = 22
输出:[5,4,8,11,null,17,4,7,null,null,null,5]
示例 3:
输入:root = [1,2,-3,-5,null,4,null], limit = -1
输出:[1,null,-3,4]
提示:
- 树中节点数目在范围
[1, 5000]
内 -105 <= Node.val <= 105
-109 <= limit <= 109
递归
https://leetcode.cn/problems/insufficient-nodes-in-root-to-leaf-paths/solution/python3-di-gui-xiang-jie-1080-gen-dao-xi-cc4a/
递归的过程包含两部分,一部分是向下走的“递下去”的过程,另一部分是从终点想回走的“归上来”的过程。
在你的递归函数中:
调用下一个递归函数之前,都是在为“递下去”做准备,即在“递下去”之前执行;
而在调用递归函数之后,此时操作的所有代码均为“归上来”之后执行。
以上两点是递归最重要的两部分,只有理解了这两部分,在写代码的时候才能想清楚,才能知道某些操作应该放到什么位置。
备注:递归只有在处理(递下去)的问题时,才可以转化为迭代。处理(归上来)问题时,是无法转化为迭代的。
思路
理解了上面的递归,再来看这个题目就会容易很多了。
首先我们可以开一个Map,记录每个节点对应的经过该节点的所有路径和中的最大值。
因为我们是从上面根节点出发,所以传递路径和的过程应该放在“递下去”的过程中。
而如果我们只是向下传递路径和,那么只有叶子结点才是这条路径上的路径和,而其他非叶子节点都是不完整的路径和。
所以,我们还需要在递归执行到底下,准备返回时,将叶子节点的值传上来,使得每个非叶子节点的路径和得以完整。这时候我们再去选一个左右节点传上来的路径和的最大值即可。
当然,上一段的将叶子节点的值传上来这一操作是放在归上来的位置,这显而易见。
当我们归上来到该节点时,那么就证明递归已经从下面上来了,准备继续向上走了。那此时该节点左右子树的所有值都是已经计算完成的。这时候就可以根据题意判断删除「不足节点」了。
总结一下,在一个递归函数中,顺序如下:
- 向下传递该节点的值(前缀和思想);
- 执行递归函数;
- 向上传递叶子节点的路径和,并取左右子树中路径和最大的那个;
- 根据题意判断删除「不足节点」;
class Solution {
Map<TreeNode, Integer> vals;
int limit;
public TreeNode sufficientSubset(TreeNode root, int limit) {
this.limit = limit;
TreeNode dummy = new TreeNode(0, root, null);
// 记录每个节点对应的经过该节点的所有路径和中的最大值
vals = new HashMap<>();
dfs(dummy);
return dummy.left;
}
public int dfs(TreeNode node){
// 判断边界
if(node == null) return Integer.MIN_VALUE;
// 向下递归“递下去”,将自身值传递下去
vals.put(node, vals.getOrDefault(node, 0) + node.val);
if(node.left != null)
vals.put(node.left, vals.getOrDefault(node.left, 0) + vals.get(node));
if(node.right != null)
vals.put(node.right, vals.getOrDefault(node.right, 0) + vals.get(node));
// 只要当前节点不是叶子节点,vals[node]就是一个不完全的路径和
// 需要从递归“归上来”的值中去选最大的
if(node.left != null || node.right != null)
vals.put(node, Math.max(dfs(node.left), dfs(node.right)));
// 走到这里证明已经从下面归上来走到这里了,下面的所有值都已计算完成
// 删除该删除的子节点即可
if(node.left != null && vals.get(node.left) < limit)
node.left = null;
if(node.right != null && vals.get(node.right) < limit)
node.right = null;
return vals.get(node);
}
}
简洁DFS
class Solution {
public TreeNode sufficientSubset(TreeNode root, int limit) {
limit -= root.val;
if(root.left == root.right) // root是叶子
// 如果 limit > 0 说明从根到叶子的路径和小于 limit,删除叶子,否则不删除
return limit > 0 ? null : root;
if(root.left != null) root.left = sufficientSubset(root.left, limit);
if(root.right != null) root.right = sufficientSubset(root.right, limit);
// 如果儿子都被删除,就删 root,否则不删 root
return root.left == null && root.right == null ? null : root;
}
}