目录
理论基础
二叉树
种类
普通二叉树:每个节点最多有两个子节点(左子节点和右子节点)。
满二叉树:所有叶子节点都在同一层,每个非叶子节点都有两个子节点。
完全二叉树:除最后一层外,其他每层都是满的,最后一层的节点从左到右排列。
二叉搜索树:对于每个节点,左子树的所有节点值小于该节点,右子树的所有节点值大于该节点。
遍历
前序:根 -> 左子树 -> 右子树
中序:左子树 -> 根 -> 右子树
后序:左子树 -> 右子树 -> 根
层序:按层逐层遍历,从上到下,从左到右。
定义
递归定义:树是一个根节点及其左、右子树。
非递归定义:树由节点和边组成,每个节点有左右子节点,边连接父子节点
遍历法
DFS
DFS递归
1.确定递归函数的参数和返回值
2.确定终止条件(防止栈溢出)
3.确定单层递归的逻辑
BFS
BFS使用队列
把每个还没有搜索到的点依次放入队列
然后再弹出队列的头部元素当做当前遍历点
1.不需要确定当前遍历到了哪一层
while queue 不空:
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未访问过:
queue.push(该节点)
2.需要确定当前遍历到了哪一层
增加level:遍历到二叉树中的哪一层
增加size:队列中的元素数/当前层的所有元素
level = 0
while queue 不空:
size = queue.size()
while (size --) {
//循环直到当前层的所有节点都处理完毕
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
queue.push(该节点)
}
level ++;
BST
每个节点的左子树所有节点值都小于该节点,右子树所有节点值都大于该节点
且左、右子树也分别为二叉搜索树
特点:
中序遍历下,输出的二叉搜索树节点的数值是有序序列
常见题目
基础
-
是否对称:
- 递归:后序,比较根节点的左子树和右子树是否互为镜像。
- 迭代:使用队列或栈同时存储两个节点,逐步比较。
-
求最大深度:
- 递归:后序遍历,通过递归计算左右子树的最大高度。
- 迭代:层序遍历,记录遍历的层数。
-
求最小深度:
- 递归:后序遍历,计算左右子树的最小高度(注意区分叶子节点)。
- 迭代:层序遍历,第一个遇到叶子节点时返回。
-
求节点数量:
- 递归:后序遍历,通过递归函数的返回值计算节点数量。
- 迭代:层序遍历,逐个统计节点数量。
-
是否平衡:
- 递归:后序遍历,递归过程中计算左右子树高度差是否满足平衡条件。
- 迭代:不推荐,效率较低。
-
找所有路径:
- 递归:前序遍历,结合回溯记录从根到叶子的所有路径。
- 迭代:使用栈模拟递归,同时存储路径信息。
-
求左叶子之和:
- 递归:后序遍历,三层约束条件判断是否是左叶子。
- 迭代:模拟后序遍历,判断左叶子节点。
-
求左下角的值:
- 递归:优先搜索左孩子,找到深度最大的叶子节点。
- 迭代:层序遍历,最后一行最左边的节点即为结果。
-
求路径总和:
- 递归:遍历整棵树,检查路径总和是否等于目标值。
- 迭代:栈中保存节点指针及对应路径的累加和。
修改与构造
-
翻转二叉树:
- 递归:前序遍历,交换左右子节点。
- 迭代:使用栈模拟前序遍历交换子节点。
-
构造二叉树:
- 递归:前序遍历,找分割点构建左右子树。
- 迭代:复杂,不推荐。
-
构造最大的二叉树:
- 递归:前序遍历,使用数组最大值分割左右子树。
- 迭代:较复杂,不推荐。
-
合并两个二叉树:
- 递归:前序遍历,同时操作两个树的节点。
- 迭代:使用队列进行合并,类似层序遍历。
属性
-
搜索节点:
- 递归:根据节点值有方向地递归查找。
- 迭代:同样根据有序性,逐步向下查找。
-
是否为二叉搜索树:
- 递归:中序遍历,检查节点值是否递增。
- 迭代:模拟中序遍历,同样判断序列。
-
求最小绝对差:
- 递归:中序遍历,双指针比较节点间差值。
- 迭代:模拟中序遍历,逐步比较。
-
求众数:
- 递归:中序遍历,统计出现频率,找到众数。
- 迭代:模拟中序遍历,统计节点值出现次数。
-
转换为累加树:
- 递归:中序遍历,通过累加计算。
- 迭代:同样模拟中序遍历,进行累加。
公共祖先问题
-
普通二叉树的公共祖先:
- 递归:后序遍历,回溯找到左、右子树出现目标节点的地方。
- 迭代:不推荐模拟回溯,较复杂。
-
二叉搜索树的公共祖先:
- 递归:通过节点值找到目标区间内的祖先节点。
- 迭代:按顺序遍历二叉搜索树。
BST修改与构造
-
插入节点:
- 递归:顺序无所谓,递归找到合适位置插入节点。
- 迭代:按顺序遍历,记录插入父节点。
-
删除节点:
- 递归:前序遍历,处理删除非叶子节点的复杂情况。
- 迭代:较复杂,按顺序遍历处理。
-
修剪二叉搜索树:
- 递归:前序遍历,根据返回值修剪节点。
- 迭代:按顺序遍历修剪,较复杂。
-
构造二叉搜索树:
- 递归:前序遍历,使用数组中间节点构建树。
- 迭代:通过队列模拟构造,较复杂。
94.中序
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
延伸:前中后遍历 递归法
// 前序遍历LC144_二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}
public void preorder(TreeNode root, List<Integer> result) {
if (root == null) return;
result.add(root.val);
preorder(root.left, result);
preorder(root.right, result);
}
}
// 中序遍历LC94_二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
void inorder(TreeNode root, List<Integer> list) {
if (root == null) return;
inorder(root.left, list);
list.add(root.val);
inorder(root.right, list);
}
}
// 后序遍历LC145_二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postorder(root, res);
return res;
}
void postorder(TreeNode root, List<Integer> list) {
if (root == null) return;
postorder(root.left, list);
postorder(root.right, list);
list.add(root.val);
}
}
104.最大深度
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
提示:
树中节点的数量在 [0, 104] 区间内。
-100 <= Node.val <= 100
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数
前序求深度,后序求高度(为什么想一下遍历顺序就明白了)
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
//先求左子树的深度,再求右子树的深度,最后取左右深度最大的数值再+1
}
}
延伸:最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量,注意是叶子节点
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
int leftDepth = minDepth(root.left);
int rightDepth = minDepth(root.right);
if (root.left == null) return rightDepth + 1;
if (root.right == null) return leftDepth + 1;
//子树空,自己画张图就理解了,要叶子结点
return Math.min(leftDepth, rightDepth) + 1;
//左右子树都不为空,则返回左右子树深度的最小值再加上当前节点的深度
}
}
226.翻转
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
提示:
树中节点数目范围在 [0, 100] 内
-100 <= Node.val <= 100
递归交换左右孩子的指针/递归地翻转左右子树
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
invertTree(root.left);
invertTree(root.right);
swap(root);
//前序后序都可以
return root;
}
private void swap(TreeNode root){
TreeNode temp=root.left;
root.left=root.right;
root.right=temp;
//交换指针
}
}
101.对称
给你一个二叉树的根节点 root , 检查它是否轴对称。
提示:
树中节点数目在范围 [1, 1000] 内
-100 <= Node.val <= 100
进阶:你可以运用递归和迭代两种方法解决这个问题吗?
比较子树内外侧是否相等
class Solution {
public boolean isSymmetric(TreeNode root) {
return compare(root.left,root.right);
//直接返回 compare 的布尔值结果
}
private boolean compare(TreeNode left,TreeNode right){
if(left==null&&right!=null) return false;
if(left!=null&&right==null) return false;
if(left==null&&right==null) return true;
if(left.val!=right.val) return false;
//不要再打一个=了!
boolean out=compare(left.left,right.right);
boolean in=compare(left.right,right.left);
return out&∈
}
}
543.直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
提示:
树中节点数目在范围 [1, 104] 内
-100 <= Node.val <= 100
直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
最长直径就是左右子树的最大深度之和,递归遍历每个节点时更新全局最大值
class Solution {
int ans = 0;//全局变量记录最大值
public int diameterOfBinaryTree(TreeNode root) {
dfs(root);
return ans;
}
int dfs(TreeNode u) {
if (u == null) return 0;
int l = dfs(u.left), r = dfs(u.right);//左右子树的深度
ans = Math.max(ans, l + r);//更新最大值
return Math.max(l, r) + 1;//父节点需要返回值(子树的深度)计算以它为根的最大深度,并继续向上返回
}
}
可以看作,最大深度+更新一个全局变量
102.层序
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
提示:
树中节点数目在范围 [0, 2000] 内
-1000 <= Node.val <= 100
回顾一下BFS
把每个还没有搜索到的点依次放入队列
然后再弹出队列的头部元素当做当前遍历点
level:遍历到二叉树中的哪一层
size:队列中的元素数/当前层的所有元素
level = 0
while queue 不空:
size = queue.size()
while (size -- >0) {
//循环直到当前层的所有节点都处理完毕
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
queue.push(该节点)
}
level ++;
注意二维列表和当前层级的列表
class Solution {
public List<List<Integer>> res = new ArrayList<List<Integer>>();
//res 记录整个树的所有层次的节点值
public List<List<Integer>> levelOrder(TreeNode root) {
Fun(root);
return res;
}
public void Fun(TreeNode node) {
if (node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);//将根节点入队
while (!que.isEmpty()){
List<Integer> itemList = new ArrayList<Integer>();
//itemList临时列表,每次处理完一层节点后存储该层的节点值
int len = que.size();
while (len-- > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);
//记录单层节点
if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
}
res.add(itemList);//当前层级的节点值列表添加到结果列表
}
}
}
108.有序数组转换BST
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵
平衡
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 按 严格递增 顺序排列
根据数组构造一棵二叉树就是寻找分割点
有序数组构造二叉搜索树,分割点就是数组中间位置的节点(奇偶不影响,答案不唯一)
以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树
public class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return buildBST(nums, 0, nums.length - 1);
}
private TreeNode buildBST(int[] nums, int start, int end) {
if (start > end) return null; // 超过数组范围
int mid = start + (end - start) / 2;
TreeNode root = new TreeNode(nums[mid]);// 选择中间位置的元素作为根节点
root.left = buildBST(nums, start, mid - 1);//左
root.right = buildBST(nums, mid + 1, end);//右
return root;
}
}
相关延伸:
*数组换成链表
链表分割点,用快慢指针找 876. 链表的中间结点
*数组构造普通二叉树
*递归函数返回值
*循环不变量
98.验证BST
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵
平衡
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 按 严格递增 顺序排列
//中序遍历是有序序列,验证BST=判断序列是否递增
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 = root.val;//更新pre,为什么写找个叶子自己模拟一下
return isValidBST(root.right); // 右
}
}
230.BST中第K小
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。
提示:
树中的节点数为 n 。
1 <= k <= n <= 104
0 <= Node.val <= 104
进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
转化为求中序遍历的第 k 个节点
public class Solution {
int n=0,res=0; // 计数器
public int kthSmallest(TreeNode root, int k) {
inorder(root, k);
return res;
}
private void inorder(TreeNode node, int k) {
if (node == null) return;
inorder(node.left, k);
n++;
if (n== k) {
res= node.val; //保存其值并结束遍历
return;
}
inorder(node.right, k);
}
}
199.右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
提示:
二叉树的节点个数的范围是 [0,100]
-100 <= Node.val <= 100
BFS :记录下每层的最后一个元素
DFS: 根结点 -> 右子树 -> 左子树
//BFS
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
while (size-- > 0) {
TreeNode node = queue.poll();
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
if (size == 1) res.add(node.val);
}
}
return res;
}
}
//DFS
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> rightSideView(TreeNode root) {
dfs(root, 0); // 从根节点开始访问,根节点深度是0
return res;
}
private void dfs(TreeNode root, int depth) {
if (root == null) return;
if (depth == res.size()) res.add(root.val);//该深度下的最右边的节点
depth++;//递归到下一层,深度增加
dfs(root.right, depth);
dfs(root.left, depth);
}
}
144.展开为链表
你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
提示:
树中结点数在范围 [0, 2000] 内
-100 <= Node.val <= 100
进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?
根据二叉树前序遍历的顺序,将节点依次串起来
最好理解的版本
class Solution {
public void flatten(TreeNode root) {
if (root == null) return;
Stack<TreeNode> s= new Stack<>();
s.push(root);
TreeNode pre= null;
while (!s.isEmpty()) {
TreeNode cur= s.pop();
if (cur.right != null) s.push(cur.right);
if (cur.left != null) s.push(cur.left);
cur.left = null;
if (pre != null) pre.right = cur;
pre = cur;
}
}
}
还有两个不易理解的版本,自己画图去
展开链表的各个节点是通过right
进行来连接,有些会丢失,所以对当前节点的右子树进行暂存
class Solution {
TreeNode p;
public void flatten(TreeNode root) {
if(root == null)return;
if(p == null) p= root; //首个节点直接记录
else{
p.left = null;
p.right = root;
p = p.right;
}
TreeNode r = root.right; // 暂存节点的右子树,下一轮用
flatten(root.left);
flatten(r);
}
}
//如果存在左子树,则将左子树插入当前节点右边,否则遍历至右子树
class Solution {
public void flatten(TreeNode root) {
while(root != null){
TreeNode p = root.left;
if(p != null){
while(p.right != null) p = p.right;
p.right = root.right;
root.right = root.left;
root.left = null;
}
root = root.right;
}
}
}
105.前中序构造
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示:
1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder 和 inorder 均 无重复 元素
inorder 均出现在 preorder
preorder 保证 为二叉树的前序遍历序列
inorder 保证 为二叉树的中序遍历序列
数据结构经典题目
class Solution {
Map<Integer, Integer> m;
public TreeNode buildTree(int[] qian, int[] zhong) {
m = new HashMap<>();
for (int i = 0; i < zhong.length; i++) m.put(zhong[i], i);
return findNode(qian, 0, qian.length, zhong, 0, zhong.length); // 前闭后开
}
public TreeNode findNode(int[] qian, int pa, int pb, int[] zhong, int ia, int ib) {
if (pa >= pb || ia >= ib) return null;
int rootIndex = m.get(qian[pa]); // 找到前序遍历的第一个元素在中序遍历中的位置
TreeNode root = new TreeNode(zhong[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - ia; // 保存中序左子树个数,用来确定前序数列的个数
root.left = findNode(qian, pa + 1, pa + lenOfLeft + 1,zhong, ia, rootIndex);
root.right = findNode(qian, pa + lenOfLeft + 1, pb,zhong, rootIndex + 1, ib);
return root;
}
}
延伸:中后序
class Solution {
Map<Integer, Integer> m; // 方便根据数值查找位置
public TreeNode buildTree(int[] i, int[] p) {
m = new HashMap<>();
for (int j = 0; j < i.length; j++) { // 用m保存中序序列的数值对应位置
m.put(i[j], j);
}
return findNode(i, 0, i.length, p, 0, p.length); // 前闭后开
}
public TreeNode findNode(int[] i, int ia, int ib, int[] p, int pa, int pb) {
if (ia >= ib || pa >= pb) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int idx = m.get(p[pb - 1]); // 找到后序遍历的最后一个元素在中序遍历中的位置
TreeNode root = new TreeNode(i[idx]); // 构造结点
int lenOfLeft = idx - ia; // 保存中序左子树个数,用来确定后序数列的个数
root.left = findNode(i, ia, idx, p, pa, pa + lenOfLeft);
root.right = findNode(i, idx + 1, ib, p, pa + lenOfLeft, pb - 1);
return root;
}
}
437.路径总和
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
提示:
二叉树的节点个数的范围是 [0,1000]
-109 <= Node.val <= 109
-1000 <= targetSum <= 1000
class Solution {
public int pathSum(TreeNode root, long sum) { //遍历每个节点,并对每个节点调用 rootSum
if (root == null) return 0;
int res = rootSum(root, sum);//当前节点
res += pathSum(root.left, sum);
res += pathSum(root.right, sum);
return res;
}
public int rootSum(TreeNode root, long sum) { //从当前节点开始查找路径
int res = 0;
if (root == null) return 0;
int val = root.val;
if (val == sum) res++;
//路径和等于 (sum - 当前节点值) 的路径数量
res += rootSum(root.left, sum - val);
res += rootSum(root.right, sum - val);
return res;
}
}
延伸:和为K的子数组
236.最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
提示:
树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
//递归处理去找到 p 或 q
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//没找到,说明两个都在另一边的左子树
if(left == null) return right;
if(right == null) return left;
//分别在左右子树
return root;
}
}
124.最大路径和h
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
提示:
树中节点数目范围是 [1, 3 * 104]
-1000 <= Node.val <= 1000
class Solution {
private int res = Integer.MIN_VALUE; // 记录当前全局最大路径和,初始化为最小值
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
private int dfs(TreeNode node) {
if (node == null) return 0;
int lVal = dfs(node.left);
int rVal = dfs(node.right);
res = Math.max(res, lVal + rVal + node.val);//当前节点可以贡献的最大路径和
return Math.max(Math.max(lVal, rVal) + node.val, 0); //左子树或右子树中的一个方向来延展路径,负数则放弃
}
}