LeetCode二叉树篇题解,看这一篇就够了
文章目录
基础知识
java中提供 Deque双端队列实现队列与栈的各种需求, 题目中经常会用到如下
- offerlast(node)添加元素到队尾
- offerFist(node)添加元素到队头
- new LinkedList<>(deque), 利用双端队列新建列表
- 以上三步可用 List 列表的 add(node) 尾插 , add(0,node)头插直接实现
树的三种遍历
-
二叉搜索树的遍历共有四种, 即前序遍历、中序遍历、后序遍历、以及层序遍历、这四种遍历方式是 BST 的重中之重, 大部分树的题目都可以在这四种遍历的基础上修改得以解答. 所以我们必须牢记它们的写法.
-
首先让我们通过三道题来了解 BST 的前序, 中序, 后序遍历, 同时,每种遍历又可以用递归和迭代两种方式实现, 最后我们还补充一个迭代的统一写法.
//树节点类定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int x) {
val = x;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
递归实现
List<Integer> list = new ArrayList<>();
//前序遍历
public void preorder(TreeNode root){
if(root != null){
list.add(root.val);
preorder(root.left);
preorder(root.right);
}
}
//中序遍历
public void inorder(TreeNode root){
if(root != null){
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
}
//后序遍历
public void backorder(TreeNode root){
if(root != null){
backorder(root.left);
backorder(root.right);
list.add(root.val);
}
}
栈迭代
//先序遍历
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stk = new LinkedList<TreeNode>();
while(root != null || !stk.isEmpty()){
while(root != null){
res.add(root.val);//先序和中序的差别就是这条语句的插入位置
stk.push(root);
root = root.left;
}
root = stk.pop();
root = root.right;
}
return res;
}
//中序遍历
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stk = new LinkedList<TreeNode>();
while(root != null || !stk.isEmpty()){
while(root != null){
stk.push(root);
root = root.left;
}
root = stk.pop();
res.add(root.val);
root = root.right;
}
return res;
}
//后序遍历
//与中序和前序的结构差别大,不便记忆
统一结构
-
参考 颜色标记法
-
实现了前中后序遍历的统一结构写法,只需改变节点的进栈顺序
public List<Integer> inorderTravelsal(TreeNode root){
HashMap<TreeNode, Integer> map = new HashMap<>();
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
if(root == null) //初始检查
return res;
//根节点进栈
stack.push(root);
map.put(root,1); //1 表示第一次访问, 2 表示第二次访问
while(!stack.isEmpty()){
TreeNode node = stack.pop();
if(1 == map.get(node)){
//中序遍历,因为栈的特点,先进后出,右-中-左进栈, 出栈时就是 左-中-右
if(node.right != null){
stack.push(node.right);
map.put(node.right,1);
}
stack.push(node);
map.put(node,2);
if(node.left != null){
stack.push(node.left);
map.put(node.left,1);
}
//前序遍历
if(node.right != null){
stack.push(node.right);
map.put(node.right,1);
}
if(node.left != null){
stack.push(node.left);
map.put(node.left,1);
}
stack.push(node);
map.put(node,2);
//后序遍历
stack.push(node);
map.put(node,2);
if(node.right != null){
stack.push(node.right);
map.put(node.right,1);
}
if(node.left != null){
stack.push(node.left);
map.put(node.left,1);
}
}else{
res.add(node.val);
}
}
return res;
}
层序遍历
- 层序遍历也就是我们经常所说的广度优先遍历 (BFS)
public class Tree_102_107 {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> lists = new ArrayList<>();
if(root == null)
return lists ;
//核心数据结构 : 队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
//while 循环每一层
while(!q.isEmpty()){
int sz = q.size();
List<Integer> list = new ArrayList<>();
//for 循环对当前层进行操作,同时将下一层入队
for(int i = 0 ; i < sz ; ++i){
//出队操作在for 循环内,容易写到外面导致出错
TreeNode node = q.poll();
list.add(node.val);
if(node.left != null)
q.offer(node.left);
if(node.right != null)
q.offer(node.right);
}
lists.add(list);
//leetcode_107
//lists.add(0,list); //lists.add(0,list) 头插 🌟
}
return lists;
}
}
判断 BST
- 学会四种遍历方式后, 先让我们看看如何通过中序遍历的方式判断一个树是否为一个有效的二叉搜索树
- 有效的二叉树即中序遍历为升序, 这里我们利用栈的特性进行判断, 从而省去构建好后在判断的时间.
public class IsValidBST_98 {
public boolean isValidBST(TreeNode root) {
double inorder = -Double.MAX_VALUE;
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder)
return false;
inorder = root.val;
root = root.right;
}
return true;
}
}
构建 BST
- 现在我们能判断 BST了, 但是如何构建一棵 BST 呢? 如何构建多棵不一样的 BST 呢?
题目: 给定 n , 构建一棵从 1…n 的树
- 根据 BST 的特性, 我们很容易用的递归写出代码.
- 假设给的是一个数组 nums[] , 我们只需要把新建节点处改为nums[i].参见 将有序数组转换为二叉搜索树
public class BinaryTree {
public TreeNode createBinaryTree(int n){
return helper(1,n);
}
//构建一颗平衡二叉树
public TreeNode helper(int start, int end){
if(start > end){
return null;
}
int i = (start + end) / 2;
TreeNode root = new TreeNode(i);
root.left = helper(start, i -1);
root.right = helper(i + 1, end);
return root;
}
}
-
进阶(leetcode–95):
给定一个整数 n,生成所有由 1 … n 为节点所组成的BST.
**难度:**🌟🌟🌟🌟
public class GenerateTrees_95 {
public List<TreeNode> generateTrees(int n){
if(n == 0)
return new LinkedList<TreeNode>();
return generateTrees(1,n);
}
public List<TreeNode> generateTrees(int start, int end){
List<TreeNode> allTrees = new LinkedList<>();
//结束条件
if(start > end){
//必须添加一个空元素,不然下面枚举会出错
//比如 leftTrees = null , 则不会进第一层for循环,导致要走的二层for循环无法进行
//但是添加了一个空元素后, leftTree = [null], 是含有一个空元素的,二者的意思不同
allTrees.add(null);
return allTrees;
}
//枚举可行根节点
for(int i = start ; i <= end ; i++){
//获取所有可行的左子树集合
List<TreeNode> leftTrees = generateTrees(start, i - 1);
List<TreeNode> rightTrees = generateTrees(i+1,end);
//从左子树集合中选出一个左子树,从右子树集合中选出一颗右子树,拼接到根节点上
for(TreeNode left : leftTrees){
for(TreeNode right : rightTrees){
TreeNode currTree = new TreeNode(i);
currTree.left = left;
currTree.right = right;
allTrees.add(currTree);
}
}
}
return allTrees;
}
}
还原树(LeetCode——105,106)
- 利用前序遍历和中序遍历还原树
- **重要程度:**🌟🌟🌟🌟
- 解题关键: 确定每次左右子树递归的界限, 通过HashMap存储中序遍历的值和下标, 实现 O ( 1 ) O(1) O(1)的查找复杂度, 并以此计算得到左右子树的长度, 从而得到下标.
- 中序遍历和后序遍历还原树, 只需要改变一下结束条件和递归的下标值就行.
import java.util.HashMap;
import java.util.Map;
public class Tree_105 {
private Map<Integer, Integer> indexMap;
public TreeNode buildTree(int[] preorder, int[] inorder){
int n = preorder.length;
indexMap = new HashMap<Integer, Integer>();
for(int i = 0 ; i < n ; i++){
indexMap.put(inorder[i],i);
}
return myBuildTree(preorder, inorder, 0,n-1,0,n-1);
}
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right){
//前序和中序遍历还原树的结束条件
if(preorder_left > preorder_right){
return null;
}
//前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
//中序遍历中定位根节点
int inorder_root = indexMap.get(preorder[preorder_root]);
//先把根节点建立出来
TreeNode root = new TreeNode(preorder[preorder_root]);
//得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
//递归地构造左子树, 并连接到根节点
root.left = myBuildTree(preorder, inorder , preorder_left + 1 , preorder_left + size_left_subtree , inorder_left , inorder_root - 1);
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
}
平衡二叉树
- 判断一棵树是否是平衡二叉树
- **难度:**自底向上思想——🌟🌟🌟
- 平衡二叉树定义: 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1
- 当遇到重复计算问题时, 可以考虑反方向思考, 一般自底向上优于自顶向下
//自顶向下,双重递归,重复计算,时间复杂度高
public boolean isBalanced(TreeNode root) {
if(root == null)
return true;
else
return Math.abs(getLength(root.left)-getLength(root.right))<=1
&& isBalanced(root.left) && isBalanced(root.right);
}
public int getLength(TreeNode root){
if(root == null)
return 0;
return Math.max(getLength(root.left),getLength(root.right)) + 1;
}
//自底向上,类似后序遍历
public boolean isBalanced2(TreeNode root){
return height(root) >= 0;
}
public int height(TreeNode root){
if(root == null){
return 0;
}
int leftHeight ;
int rightHeight ;
//将递归放入判断逻辑中, 达到剪枝的效果, 如果左子树不平衡则不用递归右子树
if((leftHeight = height(root.left)) == -1 || (rightHeight = height(root.right)) == -1
|| Math.abs(leftHeight - rightHeight) > 1)
return -1;
else
return Math.max(leftHeight,rightHeight) + 1;
}
二叉树的路径问题
- 接下来让我们来看看二叉树的路径问题
-
112判断是否存在路径
-
129所有路径和相加
-
二叉树的路径数即叶子节点数, 可以采用 BFS 和 DFS 两种方法求解, 个人推荐 BFS
-
使用 BFS 我们只需一个队列层序遍历从上到下更新路径和, 到叶子节点出进行处理即可
-
两题使用层序遍历的解法核心可以说几乎相同, 只需修改一下处理逻辑即可
//112、路径总和, BFS
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null){
return false;
}
//一个队列存储节点
Queue<TreeNode> queNode = new LinkedList<>();
//一个队列存储值,更新到最后只剩下每一个叶子节点的对应的路径总和
Queue<Integer> queVal = new LinkedList<>();
queNode.offer(root);
queVal.offer(root.val);
while(!queNode.isEmpty()){
TreeNode now = queNode.poll();
int temp = queVal.poll();
//叶节点处进行判断
if(now.left == null && now.right == null){
if(temp == sum){
return true;
}
continue;
}
//
if(now.left != null){
queNode.offer(now.left);
queVal.offer(now.left.val + temp);
}
if(now.right != null){
queNode.offer(now.right);
queVal.offer(now.right.val + temp);
}
}
return false;
}
//129、所有路径和、BFS
public static int sumNumbers(TreeNode root) {
if (root == null) {
return 0;
}
Queue<Integer> sum = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
sum.offer(root.val);
int sumAll = 0;
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
int num = sum.poll();
if (node.left == null && node.right == null) {
sumAll += num;
} else {
if (node.left != null) {
queue.offer(node.left);
sum.offer(num * 10 + node.left.val);
}
if (node.right != null) {
queue.offer(node.right);
sum.offer(num * 10 + node.right.val);
}
}
}
return sumAll;
}
- 112的进阶, 给出所有符合条件的路径
- 参考 : 回溯法解题框架
public class Tree_113 {
List<List<Integer>> ret = new LinkedList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
dfs(root, sum);
return ret;
}
public void dfs(TreeNode root, int sum){
if(root == null){
return ;
}
path.offerLast(root.val);
sum -= root.val;
if(root.left == null && root.right == null && sum == 0)
ret.add(new LinkedList<Integer>(path));
dfs(root.left, sum);
dfs(root.right, sum);
path.pollLast();
}
}
其他
LeetCode——117、填充每个节点的下一个右侧节点指针 2
- 本题与 116 题相同, 只是要求空间只能使用常数级
//树节点定义
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
//利用层序遍历, 通过巧妙利用哑节点, 把每一层连接成一个链表
public Node connect(Node root) {
if(root == null)
return root;
Node cur = root;
while(cur != null){
Node dummy = new Node(0);
Node pre = dummy;
while(cur != null){
if(cur.left != null){
pre.next = cur.left;
pre = pre.next;
}
if(cur.right != null){
pre.next = cur.right;
pre = pre.next;
}
cur = cur.next;
}
cur = dummy.next;
}
return root;
}
- 自定义了树迭代器 iterator 的写法
class BSTIterator {
ArrayList<Integer> list;
int index = -1;
public BSTIterator(TreeNode root) {
list = new ArrayList<>();
dfs(root);
}
public void dfs(TreeNode root){
if(root != null){
dfs(root.left);
list.add(root.val);
dfs(root.right);
}
}
public int next() {
index += 1;
return list.get(index);
}
public boolean hasNext() {
return index + 1 < list.size();
}
}
以上就是 leetcode 前两百道题树标签的题目汇总, 可以看出, 基本都逃不出四种遍历的框架, 所以重点记忆四种遍历框架, 多加练习, 树你就掌握啦!!