数据结构——二叉树的修改与构造
- 一、修改二叉树
- 226. 翻转二叉树
- 1.前/后序递归
- 2.广度优先搜索迭代
- 3.拓展:修改中序遍历 / 中序统一迭代写法
- 114. 二叉树展开为链表
- 二、构造二叉树
- 106. 从中序与后序遍历序列构造二叉树
- 递归思路
- 105. 从前序与中序遍历序列构造二叉树
- 递归思路
- 889. 根据前序和后序遍历构造二叉树
- 思路
- 654. 最大二叉树
- 思路
- 617. 合并二叉树
- 1.递归
- 2.迭代
一、修改二叉树
226. 翻转二叉树
226. 翻转二叉树
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!
使用层序遍历依然是可以的。只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的
1.前/后序递归
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
invertTree(root.left);
invertTree(root.right);
swapTree(root); //前后序都可
return root;
}
public void swapTree(TreeNode root){
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
2.广度优先搜索迭代
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {return null;}
ArrayDeque<TreeNode> deque = new ArrayDeque<>();
deque.offer(root);
while (!deque.isEmpty()) {
int size = deque.size();
while (size-- > 0) {
TreeNode node = deque.poll();
swapTree(node);
if (node.left != null) {deque.offer(node.left);}
if (node.right != null) {deque.offer(node.right);}
}
}
return root;
}
public void swapTree(TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
3.拓展:修改中序遍历 / 中序统一迭代写法
修改中序遍历,避免节点左右孩子翻转两次的情况
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
invertTree(root.left);
swapTree(root);
invertTree(root.left);
return root;
}
public void swapTree(TreeNode root){
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
中序统一迭代写法用栈来遍历,而不是靠指针来遍历,避免了递归法中翻转了两次的情况
class Solution {
public TreeNode invertTree(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
if (root != null) {
stack.push(root);
}
while (!stack.isEmpty()) {
TreeNode node = stack.peek();
if (node!=null) {
stack.pop();
if (node.right!=null) stack.push(node.right); // 右
stack.push(node); // 中
stack.push(null);
if (node.left!=null) stack.push(node.left); // 左
} else {
stack.pop();
node = stack.pop();
swapTree(node);
}
}
return root;
}
public void swapTree(TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
114. 二叉树展开为链表
114. 二叉树展开为链表
class Solution {
public void flatten(TreeNode root) {
if (root == null) return;
//分别处理左右子树
flatten(root.left);
flatten(root.right);
//后序,保证左右指针改变指向不会使孩子丢失
TreeNode left = root.left;
TreeNode right = root.right;
//左指针置为null,右指针指向原左子树
root.left = null;
root.right = left;
//将原本的右子树放在当前右子树的末端
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
二、构造二叉树
106. 从中序与后序遍历序列构造二叉树
106. 从中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
注意: 你可以假设树中没有重复的元素。
例如,给出中序遍历 inorder = [9,3,15,20,7] 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:
递归思路
根据两个顺序构造一个唯一的二叉树,就是以后序数组的最后一个元素为切割点,将中序数组切分,根据中序数组分割情况,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。是一种左右子树分治的方法。
-
第一步:如果数组大小为零的话,说明是空节点了。
-
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
第五步:切割后序数组,切成后序左数组和后序右数组
-
第六步:递归处理左区间和右区间
1、如何切割
中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割。
对于后序数组,首先要舍弃末尾元素,因为这个元素就是中间节点,已经用过了。
后序数组没有明确的切割元素来进行左右切割,但注意中序数组大小一定是和后序数组的大小相同的。中序数组已经切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。
中序和后序数组都切割完成后,就可以进行递归了。
2、边界值的计算
注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。
这里使用左闭右开。
左子树-中序数组:中序数组开头 到 找到的节点元素位置
左子树-后序数组:后序数组开头 到 postLeft + (rootIndex - inLeft) (后序数组的起始位置加上左子树长度就是后序数组结束位置了(开区间),左子树的长度 = 根节点索引-左子树)
右子树-中序数组 找到的节点元素位置+1 到 中序数组末尾
右子树-后序数组 postLeft + (rootIndex - inLeft) 到 后序数组倒数第二个元素位置
找到的节点位置rootIndex不进入下次递归
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
return buildTree1(inorder, 0, inorder.length, postorder, 0, postorder.length);
}
public TreeNode buildTree1(int[] inorder, int inLeft, int inRight,
int[] postorder, int postLeft, int postRight) {
// 没有元素了
if (inRight - inLeft < 1) {
return null;
}
// 只有一个元素
if (inRight - inLeft == 1) {
return new TreeNode(inorder[inLeft]);
}
// 后序数组postorder里最后一个即为根结点
int rootVal = postorder[postRight - 1];
TreeNode root = new TreeNode(rootVal);
int rootIndex = 0;//记录下标
// 根据根结点的值找到该值在中序数组inorder里的位置
for (int i = inLeft; i < inRight; i++) {
if (inorder[i] == rootVal) {
rootIndex = i;
break;
}
}
// 根据rootIndex划分左右子树
//左中序,左后序
root.left = buildTree1(inorder, inLeft, rootIndex,
postorder, postLeft, postLeft + (rootIndex - inLeft));
//右中序,右后序
root.right = buildTree1(inorder, rootIndex + 1, inRight,
postorder, postLeft + (rootIndex - inLeft), postRight - 1);
return root;
}
}
105. 从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
例如,给出
前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:
递归思路
与106. 从中序与后序遍历序列构造二叉树思路相同,变为以前序数组的第一个元素为切割点,将中序数组切分,根据中序数组分割情况,切前序数组。
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return buildTree2(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
TreeNode buildTree2(int[] preorder, int preLeft, int preRight,
int[] inorder, int inLeft, int inRight) {
// 递归终止条件
if (inLeft > inRight || preLeft > preRight) return null;
// val 为前序遍历第一个的值,也即是根节点的值
// idx 为根据根节点的值来找中序遍历的下标
int idx = inLeft, val = preorder[preLeft];
TreeNode root = new TreeNode(val);
for (int i = inLeft; i <= inRight; i++) {
if (inorder[i] == val) {
idx = i;
break;
}
}
// 根据 idx 来递归找左右子树
root.left = buildTree2(preorder, preLeft + 1, preLeft + (idx - inLeft),
inorder, inLeft, idx - 1);
root.right = buildTree2(preorder, preLeft + (idx - inLeft) + 1, preRight,
inorder, idx + 1, inRight);
return root;
}
}
PS:只知道前序和后序遍历序列无法确定唯一的一颗二叉树,因为没有中序遍历就无法确定左右部分,也就无法分割。
如前序遍历是[1 2 3], 后序遍历是[3 2 1],可能有如下四种情况:
889. 根据前序和后序遍历构造二叉树
889. 根据前序和后序遍历构造二叉树
给定两个整数数组,preorder 和 postorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。
如果存在多个答案,您可以返回其中 任何 一个。
思路
通过前序中序,或者后序中序遍历结果可以确定一棵原始二叉树,但是通过前序后序遍历结果无法确定原始二叉树。
前两道题,可以通过前序或者后序遍历结果找到根节点,然后根据中序遍历结果确定左右子树。这道题,你可以确定根节点,但是无法确切的知道左右子树有哪些节点。
首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。
然后把前序遍历结果的第二个元素作为左子树的根节点的值。
在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。
class Solution {
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
int len = preorder.length;
if (len == 0) return null;
TreeNode node = new TreeNode(preorder[0]);
if (len == 1) return node;
int left = 0;
for (int i = 0; i < len; i++) {
if (postorder[i] == preorder[1])
left = i+1;
}
// 递归构造左右子树
// 根据左子树的根节点索引和元素个数推导左右子树的索引边界
node.left = constructFromPrePost(Arrays.copyOfRange(preorder,1,left+1),
Arrays.copyOfRange(postorder,0,left));
node.right = constructFromPrePost(Arrays.copyOfRange(preorder,left+1,len),
Arrays.copyOfRange(postorder,left,len-1));
return node;
}
}
654. 最大二叉树
654. 最大二叉树
给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
二叉树的根是数组中的最大元素。
左子树是通过数组中最大值左边部分构造出的最大二叉树。
右子树是通过数组中最大值右边部分构造出的最大二叉树。
通过给定的数组构建最大二叉树,并且输出这个树的根节点。
示例 :
提示:
给定的数组的大小在 [1, 1000] 之间。
思路
代码随想录动图:
构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。
- 递归函数的参数和返回值:参数就是传入的是存放元素的数组,返回该数组构造的二叉树的头结点
- 终止条件:当递归遍历的时候,如果传入的数组大小为1,说明遍历到了叶子节点了。那么应该定义一个新的节点,并把这个数组的数值赋给新的节点,然后返回这个节点。 这表示一个数组大小是1的时候,构造了一个新的节点,并返回。
- 单层递归的逻辑:
- 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。
- 最大值所在的下标左区间 构造左子树
如果不让空节点(空指针)进入递归,就要加判断maxValueIndex > 0,因为要保证左区间至少有一个数值。 - 最大值所在的下标右区间 构造右子树
如果不让空节点(空指针)进入递归,要加判断maxValueIndex < (nums.size() - 1),确保右区间至少有一个数值。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return constructMaximumBinaryTree1(nums, 0, nums.length);
}
public TreeNode constructMaximumBinaryTree1(int[] nums, int leftIndex, int rightIndex) {
if (rightIndex - leftIndex < 1) {// 没有元素了
return null;
}
if (rightIndex - leftIndex == 1) {// 只有一个元素
return new TreeNode(nums[leftIndex]);
}
int maxIndex = leftIndex;// 最大值所在位置
int maxVal = nums[maxIndex];// 最大值
for (int i = leftIndex + 1; i < rightIndex; i++) {
if (nums[i] > maxVal){
maxVal = nums[i];
maxIndex = i;
}
}
TreeNode root = new TreeNode(maxVal);
// 根据maxIndex划分左右子树
root.left = constructMaximumBinaryTree1(nums, leftIndex, maxIndex);
root.right = constructMaximumBinaryTree1(nums, maxIndex + 1, rightIndex);
return root;
}
}
617. 合并二叉树
617. 合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
注意: 合并必须从两个树的根节点开始。
示例 1:
1.递归
合并需要同时遍历两个二叉树,和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。
本题前中后序遍历都可以使用
- 递归函数的参数和返回值:要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。
- 终止条件:传入了两个树,就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就是 t2 了(如果t2也为NULL也无所谓,合并之后就是NULL);反过来如果t2 == NULL,那么两个数合并就是t1。
- 单层递归的逻辑:重复利用t1树,将t1作为合并之后树的根节点。单层递归中,把两棵树的元素加到一起。接下来t1的左子树是合并t1左子树t2左子树之后的左子树;t1的右子树是合并t1右子树t2右子树之后的右子树。
也可以重新定义新的树用于存储结果。
前序:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null) return root2;// 如果t1为空,合并之后就是t2
if (root2==null) return root1;// 如果t2为空,合并之后就应该是t1
root1.val+=root2.val; // 中
root1.left = mergeTrees(root1.left,root2.left); // 左
root1.right = mergeTrees(root1.right,root2.right); // 右
return root1;
}
}
中序:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null) return root2;// 如果t1为空,合并之后就是t2
if (root2==null) return root1;// 如果t2为空,合并之后就应该是t1
root1.left = mergeTrees(root1.left,root2.left); // 左
root1.val+=root2.val; // 中
root1.right = mergeTrees(root1.right,root2.right); // 右
return root1;
}
}
后序:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null) return root2;// 如果t1为空,合并之后就是t2
if (root2==null) return root1;// 如果t2为空,合并之后就应该是t1
root1.left = mergeTrees(root1.left,root2.left); // 左
root1.right = mergeTrees(root1.right,root2.right); // 右
root1.val+=root2.val; // 中
return root1;
}
}
2.迭代
使用栈模拟层序遍历:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null) return root2;// 如果t1为空,合并之后就是t2
if (root2==null) return root1;// 如果t2为空,合并之后就应该是t1
Stack<TreeNode> stack = new Stack<>();
stack.push(root2);
stack.push(root1);
while (!stack.isEmpty()) {
TreeNode node1 = stack.pop();
TreeNode node2 = stack.pop();
node1.val += node2.val;
if (node2.right != null && node1.right != null) {
stack.push(node2.right);
stack.push(node1.right);
} else {
if (node1.right == null) {
node1.right = node2.right;
}
}
if (node2.left != null && node1.left != null) {
stack.push(node2.left);
stack.push(node1.left);
} else {
if (node1.left == null) {
node1.left = node2.left;
}
}
}
return root1;
}
}
使用队列模拟层序遍历:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1==null) return root2;// 如果t1为空,合并之后就是t2
if (root2==null) return root1;// 如果t2为空,合并之后就应该是t1
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root1);
queue.offer(root2);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
// 此时两个节点一定不为空,val相加
node1.val = node1.val + node2.val;
// 如果两棵树左节点都不为空,加入队列
if (node1.left != null && node2.left != null) {
queue.offer(node1.left);
queue.offer(node2.left);
}
// 如果两棵树右节点都不为空,加入队列
if (node1.right != null && node2.right != null) {
queue.offer(node1.right);
queue.offer(node2.right);
}
// 若node1的左节点为空,直接赋值
if (node1.left == null && node2.left != null) {
node1.left = node2.left;
}
// 若node2的左节点为空,直接赋值
if (node1.right == null && node2.right != null) {
node1.right = node2.right;
}
}
return root1;
}
}