递归算法的关键
**首先:明确函数定义,相信这个定义,不要**跳进递归。
**其次:**搞明白root节点自己要做什么,然后根据题目要求选择前中后序递归框架。
**最后:**思考出每个节点需要做什么,刷题。
习题1:翻转二叉树
首先,明确函数定义
即,交换根节点的左右节点位置,返回翻转后的节点。
其次,明确root自己做什么
他要交换它的两个子节点,故选择前序遍历
再者,明确子节点都要做什么
让每个子节点都去翻转他们的左右子节点。
最后,明确边界条件
当root == null时,证明执行到了叶子节点,终止递归,直接返回。
TreeNode invertTree(TreeNode root) {
//判断root是否为空,为空直接返回。
if (root == null) {
return root;
}
//选择前序遍历
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
//左右节点都要干什么,他们要交换他们的左右节点
invertTree(root.left);
invertTree(root.right);
//返回翻转后的树,根节点
return root;
}
习题2:填充每个二叉树的右侧指针
二叉树问题的难点就在于,如何把题目的要求细化成每个节点要做的事情。
试探思路1:
像上一题一样,将任务分给一个节点,让他的左子节点指向右子节点,右子节点指向空,但是我们很快就会发现一个问题,如果两个相邻子节点并不属于一个双亲结点,那么他们就无法相连。这也是递归的难点,如何细化每个节点要做的事情。
解决思路:
细化节点任务,让两个节点承担任务。
明确root自己做什么
root 自己将自己的左子节点和右子节点连接起来。
左子节点和右子节点都要将他们的子节点连接起来,并且,也要将不属于一个根节点的同层节点连接起来。
明确子节点要做什么
子节点做和父节点一样的事。
Node connect(Node root) {
//临界条件,若root = null 返回root
if (root == null) {
return root;
}
connectTwoNode(root.left,root.right);
return root;
}
void connectTwoNode(Node node1,Node node2) {
//只要node1 和 node2 有一个是空的,不需要再指向,直接保持原状指向null即可。
if (node1 == null || node2 == null) {
}
//前序遍历,将node1 指向 node2
node1.next = ndoe2;
//递归将 node1 的 左子节点指向它的右子节点
connectTwoNode(node1.left,node2.left);
//将 node2 的 左子节点指向有字节点
connectTwoNode(node2.left,node2.right);
//将 node1 的右子节点指向 node2 的左子节点,解决不同根节点的问题。
connectTwoNode(node1.right,node2.left);
}
习题3:将二叉树展开为链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ostsoe81-1636643625697)(C:\Users\axi\AppData\Roaming\Typora\typora-user-images\image-20211107181324327.png)]
解题思路:
首先,明确递归函数定义
目标,将它的左右子树转化为一条链表,让左子树代替根节点的右子树,将原右子树链到新右子树的后面。
传入一个节点,函数里对这个节点的左右子树进行链表化。所以采用后序遍历。
明确root自己做什么
将它的左右子节点向上文所说的那样进行合并。
明确边界条件
root == null 返回root
左子树为null 直接返回右子树
右子树为null 直接链入左子树
public void flatten(TreeNode root) {
if (root == null) {
return;
}
//后序遍历,先将根节点的左右子树链表化
flatten(root.left);
faltten(root.right);
//记录链表化的左右子树
TreeNode left = root.left;
TreeNode right = root.right;
//将左节点置空
root.left = null;
//用左子树替换掉右子树
root.right = left;
TreeNode p = root;
//将左子树作为根节点的右子树,将右子树链到原左子树的后面
while (p.right != null) {
p = p.right;
}
p.right = right;
}
总结:
递归算法的关键要明确函数的定义,相信这个定义,而不要跳进递归细节。
写二叉树的算法题,都是基于递归框架的,我们先要搞清楚 root
节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。
二叉树题目的难点在于如何通过题目的要求思考出每一个节点需要做什么,这个只能通过多刷题进行练习了。
如果本文讲的三道题对你有一些启发,请三连,数据好的话东哥下次再来一波手把手刷题文,你会发现二叉树的题真的是越刷越顺手,欲罢不能,恨不得一口气把二叉树的题刷通。
原文链接:https://labuladong.gitee.io/algo/2/18/21/
习题4:构造最大二叉树
明确思路:
我们通过遍历数组中的最大值,来确定最大元素在数组中的下标位置,根据下标位置,得到它的左右子树,然后递归左右子树得到左右根节点。
明确递归函数的作用及返回值
递归函数作用,在给定数组区间找到最大的元素位置,并递归找到它的左右子树,返回以最大元素构造的根节点。
TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums, 0, nums.length - 1);
}
TreeNode build(int[] nums,int left,int right) {
//base case 若左 > 右 越界
if (left > right) {
return null;
}
//定义maxval 和 index 记录给定范围数组的最大元素的值和下标。
int maxVal = Integer.MIN_VALUE;
int index = -1;
for (int i = left ; i <= right ; i++) {
if (nums[i] > maxVal) {
index = i;
maxVal = nums[i];
}
}
//根据最大值创建根节点的值。
TreeNode root = new TreeNode(maxVal);
//递归构造左子树以及右子树
root.left = build(nums,left,index-1);
root.right = build(nums,index+1,right);
//返回root
return root;
}
习题5:通过前序中序构造二叉树
解题思路:
我们要想办法构造头节点的值,将头节点构造出来,之后递归构造它的左右子树。
首先,明确先序和中序遍历的原理。
先序遍历:根节点始终是数组第一个元素。然后是左子树和右子树部分。
后续遍历:根节点在数组中部,前面是左子树,后面是右子树。
二者具有相互利用关系,若想回复二叉树,二者缺一不可。
注:leftsize是左子树的大小,由于preStart+leftSize是包含preStart的,所以相加后的size是比原size+1的,故他会向后多一位,因为前序遍历首位是root,所以取到size+1正好取到了原lefSize的区域。
对比一下后序遍历,由于尾才是root,前面就相当于多取了一位,要减掉才行。
整个程序采用先序遍历。
步骤:
- 先根据先序遍历得到root的位置和值
- 根据root的值在中序中查找相应位置,记录index
- 计算得出左子树所占区域大小
由于左右子节点的确定都是由前序与中序共同完成的,我们就要向它们传入左右子树的前序和中序遍历所在的范围。
class Solution {
//明确,要想找到根节点的左右两子树进行递归找根节点,需要先序中序配合。
//首先描述以下先序遍历与中序遍历的根节点以及左右子树的位置
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder,0,preorder.length - 1,inorder,0,inorder.length - 1);
}
public TreeNode build(int[] preorder,int preStart,int preEnd,int[] inorder,int inStart,int inEnd) {
if (preStart > preEnd) {
return null;
}
//首先,找到根节点,根节点在先序遍历的位置就是0索引处
int rootVal = preorder[preStart];
int index = -1;
//找到根节点在中序遍历的位置
for (int i = inStart; i <= inEnd ; i++) {
if (inorder[i] == rootVal){
index = i;
break;
}
}
//记录左子树在中序遍历数组中的长度,因为不包括根节点,所以不+1
int leftSize = index - inStart;
//构造root节点
TreeNode root = new TreeNode(rootVal);
//递归构造root节点的左右子树
root.left = build(preorder,preStart+1,preStart + leftSize,inorder,inStart,index-1);
root.right = build(preorder,preStart + leftSize + 1,preEnd,inorder,index + 1,inEnd);
return root;
}
}
习题6:通过后序遍历构造二叉树
//还是相互牵制的关系,我们可以由后序遍历获得根节点的右节点的值,通过这个值
//我们就可以确定中序遍历中右节点的位置,也就能确定左节点的值了。
public TreeNode buildTree(int[] inorder, int[] postorder) {
return build(inorder,0,inorder.length - 1,postorder,0,postorder.length - 1);
}
public TreeNode build(int[] inorder,int inBegin,int inEnd,int[] postorder,int pBegin,int pEnd) {
//base case
if (inBegin > inEnd)
return null;
//后序遍历的数组尾就是rootval
int rootVal = postorder[pEnd];
int index = 0;
//从中序遍历中找到数组下标位置
for (int i = inBegin ; i <= inEnd ; i++) {
if (inorder[i] == rootVal) {
index = i;
break;
}
}
//计算不包含index的纯leftSize
int leftSize = index - inBegin;
TreeNode root = new TreeNode(rootVal);
//递归构造根节点的左子树,后序遍历开始是pBegin,pBegin+leftSzie就多了一位,要减掉
root.left = build(inorder,inBegin,index - 1,postorder,pBegin,pBegin + leftSize - 1);
root.right = build(inorder,index + 1,inEnd,postorder,pBegin + leftSize,pEnd - 1);
return root;
}
习题7:二叉树的序列化与反序列化
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
public class bianli {
// 把一棵二叉树序列化成字符串
public String serialize(TreeNode root) {}
// 把字符串反序列化成二叉树
public TreeNode deserialize(String data) {}
}
用第一个方法将二叉树序列化,第二个反序列化。
究其根源,所谓序列化,不过是把结构化数据打平,也就是二叉树的遍历操作。
我们用#代替null值。
1、前序遍历解法
首先:通过前序遍历将二叉树打平。
String NULL = "#";//用#代替nullString SEP = ",";StringBuilder sb = new StringBuilder();//用于存储序列化的字符串public String serialize(TreeNode root) { //若根节点为空,将空值存入,存入, if (root == null) { sb.append(NULL); sb.append(SEP); return null; } //不为空,将当前节点加入字符串中,,加入 sb.append(root.val); sb.append(SEP); //递归序列化它的左右子树 serialize(root.left); serialize(root.right); return sb.toString();}
其次:进行二叉树还原。
我们如果仅靠前序遍历是无法还原二叉树的,因为不能确定空值的位置,但是我们确定了空值的位置,就可以仅靠前序遍历还原二叉树了。
public TreeNode deserialize(String data) { //使用链表存储节点 LinkedList<String> nodes = new LinkedList<>(); //base case if (data == null) { return null; } //根据,切分字符串 for (String node : data.split(",")) { nodes.add(node); } TreeNode root = deserialize(nodes); return root;}public TreeNode deserialize(LinkedList<String> nodes) { //base case 若链表为空,构造结束 if (nodes.isEmpty()) { return null; } //由于,前序遍历的第一个节点就是一个子树的根节点,我们不妨拿到它的第一个节点,并移除,这样递归的时候永远拿到的都是某个子树的根节点了。 String first = nodes.removeFirst(); //base case 若节点为null,返回null if (first.equals(NULL)) { return null; } //构造根节点 TreeNode root = new TreeNode(Integer.parseInt(first)); //递归构造它的左右子节点 root.left = deserialize(nodes); root.right = deserialize(nodes); return root;}
2、后序遍历解法
后序遍历和前序遍历的区别如下图,前序遍历是先构造左子树,然后右子树,这是因为前序遍历根节点在最左侧,后序遍历根节点在右侧,所以从右开始,先构造右子树。
public class Codec { String NULL = "#"; String SEP = ","; StringBuilder sb = new StringBuilder(); // 后序遍历序列化二叉树 public String serialize(TreeNode root) { if (root == null) { sb.append(NULL); sb.append(SEP); return null; } serialize(root.left); serialize(root.right); sb.append(root.val); sb.append(SEP); return sb.toString(); } public TreeNode deserialize(String data) { LinkedList<String> nodes = new LinkedList<>(); if (data == null) { return null; } for (String node : data.split(",")) { nodes.offer(node); } return build(nodes); } public TreeNode build(LinkedList<String> nodes) { if (nodes.isEmpty()) { return null; } String last = nodes.removeLast(); if (last.equals(NULL)) { return null; } TreeNode root = new TreeNode(Integer.parseInt(last)); root.right = build(nodes); root.left = build(nodes); return root; }}
习题8:二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
解题思路:
首先:明确root节点需要做什么。
root 节点首先需要知道它的左右子节点是否包含p和q(后序遍历)
其次:明确后序遍历的功能
- 若root的左子树中包含p或q,而右子树不包含,那么就证明,两个节点都是在左子树上,返回第一次遇到的节点,那个就是他们的公共祖先。
- 反之亦然。
- 若都不为空时,说明pq在root的异侧,返回root即可。
因为是后序遍历,所以最先访问到的节点是叶子节点,故第一次满足条件的root就是最近的祖先。
最后:明确base case
- root == null 直接返回null
- root == p 或 root == q根据题目,p 和 q都在根节点为root的二叉树中,那么root就是他们的最近公共祖先。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { //若 root 为null,到达叶子节点,直接返回null //若 root == p/q p或q就是他们的最近公共祖先。 if (root == null || root == p || root == q) { return root; } //进行后序遍历,先明确左右子树的状态才能知道根节点要做什么 TreeNode left = lowestCommonAncestor(root.left,p,q); TreeNode right = lowestCommonAncestor(root.right,p,q); //若左子树为空,也就是没有p 和 q,那就证明节点全在右子树上,右子树先找到的那个节点就是祖 先。反之亦然。 if (left == null) { return right; } else if (right == null) { return left; } //都为空,证明在异侧,返回root即可。 else { return root; }}
习题9:二叉树剪枝
给你二叉树的根结点 root ,此外树的每个结点的值要么是 0 ,要么是 1 。
返回移除了所有不包含 1 的子树的原二叉树。
节点 node 的子树为 node 本身加上所有 node 的后代。
解题思路:
明确root节点要做什么
root节点根据它的左右子节点是否包含1和自己的值是否为0做决定。(后序遍历)
public TreeNode pruneTree(TreeNode root) { //采用后序遍历,只有知道左右子树是否包含1,才知道这棵树是否包含1。 if (root == null) { return null; } //递归赋值root的左右子树 root.left = pruneTree(root.left); root.right = pruneTree(root.right); //若左子树或右子树为null并且该节点的值也为0,那就说明包含该节点在内的子树不包含值1的节点, 置空。 if (root.left == null && root.right == null && root.val == 0) { return null; } return root;}
习题10:从上到下打印二叉树
考察二叉树层序遍历
class Solution { public List<List<Integer>> levelOrder(TreeNode root) { //base case if (root == null) { return new ArrayList(); } //用一个队列装根节点 Queue<TreeNode> queue = new ArrayDeque<>(); //装结果 List<List<Integer>> print = new ArrayList<>(); queue.offer(root);//先让根节点入队 while (!queue.isEmpty()) { //若队列为空,打印完毕。 int count = queue.size(); //记录每层有多少个节点。 List<Integer> prints = new ArrayList<>(); //盛装每层的打印的节点。 //进行循环,遍历每层的节点。 while (count > 0) { TreeNode node = queue.poll(); //节点出队 prints.add(node.val); //将当前节点加入打印列表 if (node.left != null) {//若 左子树不为空,将其入队 queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } count --; //减减 } print.add(prints); } return print; }}
习题11:对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1 / \ 2 2 / \ / \3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1 / \ 2 2 \ \ 3 3
1、递归实现
首先,明确根节点做什么。
根节点和自己是对称的,这棵树想要对称,就需要根节点的左右子树对称,而决定根节点的左右子树对称的条件是,左子树的左子节点和右子树的右子节点,左子树的右子节点和右子树的左子节点对称。
其次,明确base case
-
根节点为null时,该树为对称的。
-
左子节点为null 而右子节点不为null,反过来,这棵树都不是对称的。
-
根节点的左右子节点必须对称。
左右节点对称 && 左节点的左节点与右节点的右节点对称 && 左节点的右节点与右节点的左节点对称
public boolean isSymmetric(TreeNode root) { //base case if (root == null) return true; return recur(root.left,root.right);}public boolean recur(TreeNode node1,TreeNode node2) { //base case if (node1 == null && node2 == null) { return true; } if (node1 == null) { if (node2 != null) return false; } if (node2 == null) { if (node1 != null) return false; } return (node1.val == node2.val) && recur(node1.left,node2.right) && recur(node1.right,node2.left);}
2、迭代实现
我们迭代,一般引入一个队列,这是把递归转为迭代的常用方法。
首先,我们将root节点入队两次,每次迭代出队左右子节点,判断它们是否都为空,若否,一个为空一个不为空,不对称,值不同,不对称。
我们将它们的左右子节点分开存储,左子节点的左子树和右子节点右子树存一起,左子节点的右子树和右子节点的左子树存一起,这样做为了判断是否对称。
循环结束,对称。
public boolean isSymmetric(TreeNode root) { Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); queue.offer(root); while (!queue.isEmpty()) { TreeNode left = queue.poll(); TreeNode right = queue.poll(); if (left==null && right ==null) continue; if ((left == null || right == null) || left.val != right.val) { return false; } queue.offer(left.left); queue.offer(right.right); queue.offer(left.right); queue.offer(right.left); } return true;}