二叉树题目主要是对深度优先遍历、广度优先遍历的应用。
二叉树的中序遍历
描述
给定一个二叉树的根节点 root ,返回它的 中序 遍历
样例
1
/ \
null 2
/ \
3 null
输入:root = [1,null,2,3]
输出:[1,3,2]
思路
迭代图解
实现
递归
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list=new ArrayList<>();
if (root!=null){
//递归左子树
List<Integer> left=inorderTraversal(root.left);
list.addAll(left);
//根
list.add(root.val);
//递归右子树
List<Integer> right=inorderTraversal(root.right);
list.addAll(right);
}
return list;
}
}
迭代
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();
TreeNode cur=root;
while (cur!=null || !stack.isEmpty()){
//先将当前节点的最左边一条链全部加入stack中
//即,先遍历左子树
while (cur!=null){
stack.push(cur);
cur=cur.left;
}
//弹出栈顶元素
//处理根节点
cur=stack.peek();
stack.pop();
res.add(cur.val);
//将右子树加入栈中
//遍历右子树
cur=cur.right;
}
return res;
}
}
验证二叉搜索树
描述
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
二叉搜索树特点:
- node 的左子树中的所有节点的值 < node的值。
- node 的右子树中的所有节点的值 > node的值。
- 所有左子树和右子树自身必须也是二叉搜索树。
样例
输入:
2
/ \
1 3
输出: true
思路
基于二叉搜索树的特点,以 root 为根节点,判断子树中所有节点的值是否都在满足二叉搜索树条件的 [min,max]的范围内(闭区间)。如果 root.val 不在满足条件的 [min,max]内,说明不满足二叉搜索树性质直接返回。若根节点满足,还要继续递归检查它的左右子树是否满足条件,在左、右子树也都满足的情况下,才能证明它是一棵二叉搜索树。
实现
public class Main {
public boolean isValidBST(TreeNode root) {
if (root==null){
return true;
}
//根节点本身应该是在(-∞,+∞)范围中
return dfs(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
private boolean dfs(TreeNode root, long min, long max) {
//递归出口
if (root==null) {
return true;
}
//2.root.val不在给定的[min,max]范围内,则说明不满足二叉搜索树性质,直接返回。
if (root.val<min || root.val>max){
return false;
}
//递归判断root的左孩子是否在[min,root.val-1],右孩子[root.val+ 1,max]范围内;
//注意这里用的是闭区间
return dfs(root.left,min,root.val- 1L)
&& dfs(root.right,root.val+ 1L,max);
}
}
还可以利用二叉搜索树,中序遍历有序的特点,进行解题。
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
//中序遍历:左根右
//判断左子树,不满足直接返回,满足则继续判断根
if (!isValidBST(root.left)) {
return false;
}
// 当前节点小于等于中序遍历前续节点,则不满足条件直接返回。
if (root.val <= pre) {
return false;
}
//将当前节点值赋给pre,作为下一个节点的判断依据
pre = root.val;
// 判断右子树
return isValidBST(root.right);
}
}
(这个是leetcode大佬的题解,我自己写的虽然通过了,但真是又菜又长…)
对称二叉树
描述
给定一个二叉树,检查它是否是镜像对称的。
样例
1
/ \
2 2
/ \ / \
3 4 4 3
输入:[1,2,2,3,4,4,3]
输出:true
原题链接
思路
实现
递归
class Solution {
public boolean isSymmetric(TreeNode root) {
return check(root.left,root.right);
}
public boolean check(TreeNode p,TreeNode q){
if(p==null && q==null){
return true;
}
if(p==null || q==null){
return false;
}
return p.val==q.val
&& check(p.left,q.right)
&& check(p.right,q.left);
}
}
迭代
class Solution{
public boolean isSymmetric(TreeNode root) {
if(root==null){
return true;
}
//同时从最左、最右的两条链进行遍历
Stack<TreeNode> left=new Stack<>();
Stack<TreeNode> right=new Stack<>();
TreeNode cl=root.left;
TreeNode cr=root.right;
while (cl!=null|| !left.isEmpty()||cr!=null || !right.isEmpty()){
while (cl!=null && cr!=null){
//分别将最左、最右两条链的节点
left.push(cl);
right.push(cr);
cl=cl.left;
cr=cr.right;
}
if (cl!=null || cr!=null){
return false;
}
//处理根节点
cl=left.peek();
left.pop();
cr=right.peek();
right.pop();
if (cl.val!=cr.val){
return false;
}
//将左边的当前节点的右子树压入栈中
cl=cl.right;
//将右边的当前节点的左子树压入栈中
cr=cr.left;
}
return true;
}
}
从前序与中序遍历序列构造二叉树
描述
给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
样例
3
/ \
9 20
/ \
15 7
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
思路
实现
public class Main{
//存储中序遍历节点的值和下标
public Map<Integer,Integer> map=new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
int len=preorder.length;
for (int i = 0; i < len; i++) {
map.put(inorder[i],i);
}
return dfs(preorder,inorder,0,len-1,0,len-1);
}
private TreeNode dfs(int[] preorder, int[] inorder,
int pl, int pr, int il, int ir) {
if(pl>pr){
return null;
}
//获取根节点的值
int v=preorder[pl];
//根节点在inorder中的下标
int k=map.get(v);
//记录左子树的长度
int len=k-il;
TreeNode root=new TreeNode(v);
//递归处理左右子树
root.left=dfs(preorder,inorder,pl+1,pl+len,il,k-1);
root.right=dfs(preorder,inorder,pl+1+len,pr,k+1,ir);
return root;
}
}
二叉树的层序遍历
描述
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
样例
3
/ \
9 20
/ \
15 7
输入:[3,9,20,null,null,15,7]
输出:[3,[9,20],[15,7]]
思路
先将根节点加入队列,记录本层节点个数
根节点出队列,判断根节点有无左右孩子,有则将左右孩子入队,无则不作入队处理,继续去处理队列中的下一个节点。
重复上述操作,直到队列为空,即树遍历结束。
在节点出队列上,将节点对应的值存储在 depth(记录本层遍历结果)中,
在本层遍历完成后,将depth的结果添加到 res(结果集)中去。
实现
public class Main {
public static List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if (root==null){
return res;
}
Deque<TreeNode> path=new ArrayDeque<>();
path.addLast(root);
while (!path.isEmpty()){
//记录本层遍历结果
List<Integer> depth=new ArrayList<>();
//记录本层节点个数
int size=path.size();
while (size-->0) {
//将根节点弹出,存储节点值
TreeNode node=path.pop();
depth.add(node.val);
//将根节点的左右孩子加入队列
if (node.left != null) {
path.addLast(node.left);
}
if (node.right != null) {
path.addLast(node.right);
}
}
//保存本层的遍历结果
res.add(depth);
}
return res;
}
}
二叉树的最近公共祖先
描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
样例
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
思路
1.如果根节点root 恰好是p或q,说明 p或q 其中一个 在root 的左子树或右子树中,则root 是p或q的最近公共祖先。
root 不是p或q的情况下,递归处理左子树 left 和右子树 right :
1.若 left 和 right 都为空 ,则 root的左 、右子树中都不包含 p,q 返回 null;
2.若 left 和 right 都不为空 ,则 p、q 分别在 root 的左、右子树中,说明 root 为最近公共祖先。
3.若 left 为空,right 不为空 ,则 p、q 都不在左子树中。
p,q 其中一个在 root 的 右子树 中,说明right 是 p或q,则right 是最近公共祖先
p,q 都在 root 的 右子树中,说明p、q都在right的子树中,则right 是最近公共祖先
实现
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//1.空树或p或q为空
if (root==null || p==null || q==null){
return null;
}
//2.如果p或q==root
if (root==p || root==q) {
return root;
}
//3. p、q在root的子树中
root.left=lowestCommonAncestor(root.left,p,q);
root.right=lowestCommonAncestor(root.right,p,q);
//3.1左子树为空,又不是根,则p、q都在右子树中,最近公共祖先一定是根节点的右孩子
if (root.left==null){
return root.right;
}
//3.2同理,p、q都在左子树中,最近公共祖先一定是根节点的左孩子
if (root.right==null){
return root.left;
}
//3.3 p、q分别在根节点的左右子树中,则根节点是它们的最近公共祖先
return root;
}
}
二叉树的直径
描述
给定一棵二叉树,你需要计算它的直径长度。
一棵二叉树的直径长度是任意两个结点路径长度中的最大值。
TIP:这条路径可能穿过也可能不穿过根结点。
样例
1
/ \
2 3
/ \
4 5
输入:[1,2,3,4,5]
输出:3
解释:它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]
思路
枚举最高点。
以每一个节点作为最高点,求经过该节点的最长路径,这些结果的最大值即为二叉树的直径。
以 node 为最高点,经过node的最长路径=node左子树的最大深度+node右子树的最大深度。
直径=所有最高点的最长路径的最大值。
但不能直接以树的根节点,求左、右子树深度最大值之和作为直径,有可能最长路径就没经过根节点呢?
例如:
实现
public class Main {
public int res=0;
public int diameterOfBinaryTree(TreeNode root) {
if(root==null){
return 0;
}
dfs(root);
return res;
}
//当前节点的最长路径=node左子树最大深度+node右子树最大深度
private int dfs(TreeNode node) {
if (node==null){
return 0;
}
int left=dfs(node.left);
int right=dfs(node.right);
//记录每次求得的最长路径,更新最大值
res=Math.max(res,left+right);
//当前经过node的深度最大值
return Math.max(left, right)+1;
}
}
二叉树中的最大路径和
描述
给你一个二叉树的根节点 root ,返回其 最大路径和 。
路径 :一条从树中任意节点出发,沿父节点–>子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
样例
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
思路
和上一题的思路一样,还是枚举最高点,以每一个节点作为最高点,求经过该节点的最长路径上的权重和。
实现
public class Main {
int res=Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if(root==null){
return 0;
}
dfs(root);
return res;
}
private int dfs(TreeNode root) {
if (root==null){
return 0;
}
//做一点优化
// int left=dfs(root.left);
// int right=dfs(root.right);
//如果经过当前节点的最大权重<0,没有参考意义,剪枝
//只有经过该节点的最大权重>0时,才会选用当前节点
int left=Math.max(dfs(root.left),0);
int right=Math.max(dfs(root.right),0);
//更新最大值
//经过当前节点的最大权重和=左子树的最大权重和+右子树的最大权重和+当前节点的值
res=Math.max(res,left+right+root.val);
return root.val+Math.max(left,right);
}
}
二叉搜索树迭代器
描述
实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器。
- BSTIterator(TreeNode root) :初始化 BSTIterator 类的一个对象。
- boolean hasNext() :如果有下一个节点,则返回 true ;否则返回 false 。
- int next():返回下一个节点。
样例
输入:["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出:[null, 3, 7, true, 9, true, 15, true, 20, false]
解释:
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // 返回 3
bSTIterator.next(); // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 20
bSTIterator.hasNext(); // 返回 False
思路
实质:用栈模拟中序遍历的过程,这里是将遍历过程拆分成了及部分。
BSTIterator(TreeNode root):初始化、创建节点。将树中最左侧的一条链全部压入栈中。
next():输出下一个节点,也就是输出栈顶的节点了。
注意:在栈顶的节点弹出栈的同时,要将该节点的右孩子压入栈中,再次将右孩子的子树中所有的左边的节点也压入栈中。
hasNext():判断有无下一个节点,则就是看栈是否为空了,栈若不为空,说明栈中还有节点,则必然存在下一个节点。
实现
public class BSTIterator {
Stack<TreeNode> stack=new Stack<>();
//初始化,将所有左子树所有节点存储到stack中
public BSTIterator(TreeNode root) {
while (root!=null){
stack.push(root);
root=root.left;
}
}
//输出下一个节点
public int next() {
//下一个节点,即栈顶元素
//弹出栈顶元素,将栈顶节点的右孩子压入栈中
TreeNode node=stack.pop();
int v=node.val;
if (node.right!=null) {
TreeNode p = node.right;
while (p != null) {
//先将右孩子及其左子树全部压入栈中
stack.push(p);
p = p.left;
}
}
return v;
}
//判断有无下一个节点
public boolean hasNext() {
//如果栈不为空,则必然有下一个节点
return !stack.isEmpty();
}
}
二叉树的序列化与反序列化
描述
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定序列 / 反序列化算法执行逻辑,只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
样例
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
思路
通俗一点,这道题的意思可以理解为:
serialize(TreeNode root) :将TreeNode对象 转换 为String格式
deserialize(String data):将String格式 恢复 为TreeNode对象
方法:前序遍历+递归
问题:
为啥用前序遍历?
当你对一棵树一无所知的时候,先找它的根,顺着根再去找子孙们,这样处理会相对容易些,而前序遍历更容易定位到根节点。
仅通过前序遍历是无法唯一确定一个二叉树,序列化时不影响,反序列化时怎么办?
1.为什么:首先明确为什么不能唯一确定,因为遍历的结果集中我们只能观察到实际存在的节点,但空节点的位置模糊了。在重建时,可以根据结果,构建多种不同的树。
–
2.怎么办:可以在序列化时,将null的部分用其他元素来标记,保证树的结构唯一。反序列化的时候,碰到标记就知道这里原本是什么,应该怎么去复原了。
实现
public class Codec {
public String serialize(TreeNode root) {
//结果集
StringBuilder res=new StringBuilder();
if (root==null){
return res.toString();
}
TreeNode node=root;
//前序遍历
preOrder(root,res);
return res.toString();
}
private void preOrder(TreeNode root, StringBuilder res) {
//如果为空,用”#“替换,方便之后做反序列化
//如果直接return,前序遍历无法唯一确定一棵二叉树,但标记了null后,树的结构就唯一了
if (root==null){
res.append("#,");
return;
}
//”节点“之间用","分隔
res.append(root.val).append(",");
//递归处理左右子树
preOrder(root.left,res);
preOrder(root.right,res);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
int len=data.length();
if (len==0){
return null;
}
String[] str=data.split(",");
Queue<String> queue=new LinkedList<>(Arrays.asList(str));
return dfs(queue);
}
private TreeNode dfs(Queue<String> queue) {
String s=queue.poll();
//如果此处为"#",则代表为空
if (s.equals("#")){
return null;
}
//否则,创建节点,关联左右孩子
Integer v=Integer.parseInt(s);
TreeNode root=new TreeNode(v);
//递归处理左右子树
root.left=dfs(queue);
root.right=dfs(queue);
return root;
}
}