二叉树高频题目-上-不含树型dp
题目1 : 二叉树的层序遍历
-
测试链接 : https://leetcode.cn/problems/binary-tree-level-order-traversal/
-
思路
- 自己使用数组实现队列, 在队列中进行广度优先遍历
- 先将根结点进队, 如果队列里还有东西, 按照队列大小进行循环, 让队列里的结点进入本层的链表中, 如果正在处理的结点有子节点, 就让左右节点分别进入队列, 待每层的循环结束后, 将该层的列表装入总列表中
- 待队列里没有节点, 返回
-
代码
O(n)
-
// 如果测试数据量变大了就修改这个值 public static int MAXN = 2001;// 没有数据类型的初始化,别忘了赋值 public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; // l: 拿数拿l位置的数,l++ r: 放数放r位置的数, r++ // 提交时把方法名改为levelOrder,此方法为每次处理一层的优化bfs,此题推荐 public static List<List<Integer>> levelOrder2(TreeNode root) { List<List<Integer>> ans = new ArrayList<>();// 记录所有层的结点 if (root != null) {// 如果二叉树不为空 l = r = 0;// 逻辑清空 queue[r++] = root;// 先把根结点进队列 while (l < r) { // 队列里还有东西 int size = r - l;// 队列大小 ArrayList<Integer> list = new ArrayList<Integer>();// 用于记录该层结点 for (int i = 0; i < size; i++) {// 如下行为重复size遍 TreeNode cur = queue[l++];// 弹出一个节点 list.add(cur.val);// 把它放在本层的链表里去 if (cur.left != null) {// 有左加左 queue[r++] = cur.left; } if (cur.right != null) {// 有右加右 queue[r++] = cur.right; } } ans.add(list);// ans接收这一层的链表 } } return ans; }
-
题目2 : 二叉树的锯齿形层序遍历
-
测试链接 : https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/
-
思路
- 先判断根节点是否为空, 为空直接返回ans
- 不为空, 加入根结点
- 先收集结点
- reverse == false, 左 -> 右, l…r-1, 收集size个
- reverse == true, 右 -> 左, r-1…l, 收集size个
- 左 -> 右, i = i + 1
- 右 -> 左, i = i - 1
- 后进行广度优先遍历
- 搜集该层 并 置reverse = !reverse
- 返回ans
-
代码
O(n)
-
public class Code02_ZigzagLevelOrderTraversal { // 不提交这个类 public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } // 提交以下的方法 // 用每次处理一层的优化bfs就非常容易实现 // 如果测试数据量变大了就修改这个值 public static int MAXN = 2001; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; public static List<List<Integer>> zigzagLevelOrder(TreeNode root) { List<List<Integer>> ans = new ArrayList<>(); if (root != null) { l = r = 0; queue[r++] = root; // false 代表从左往右 // true 代表从右往左 boolean reverse = false; while (l < r) { int size = r - l; ArrayList<Integer> list = new ArrayList<Integer>(); // 1. 收集结点 // reverse == false, 左 -> 右, l....r-1, 收集size个 // reverse == true, 右 -> 左, r-1....l, 收集size个 // 左 -> 右, i = i + 1 // 右 -> 左, i = i - 1 for (int i = reverse ? r - 1 : l, j = reverse ? -1 : 1, k = 0; k < size; i += j, k++) { TreeNode cur = queue[i]; list.add(cur.val);// 加的是.val } // 2. 广度优先遍历 for (int i = 0; i < size; i++) { TreeNode cur = queue[l++]; if (cur.left != null) { queue[r++] = cur.left; } if (cur.right != null) { queue[r++] = cur.right; } } ans.add(list);// 收集该层 reverse = !reverse;// 反转 } } return ans; } }
-
题目3 : 二叉树的最大特殊宽度
-
测试链接 : https://leetcode.cn/problems/maximum-width-of-binary-tree/
-
思路
- 将每一个节点进行编号(此处从1开始), nq为节点队列, iq为id队列
- 每次BFS结点入队时编号也要入队
- 当每层节点入队完成, 最大宽度: 最右结点编号 - 最左结点编号 + 1
- 选取最大宽度
-
代码
O(n)
-
// 用每次处理一层的优化bfs就非常容易实现 // 如果测试数据量变大了就修改这个值 public static int MAXN = 3001; public static TreeNode[] nq = new TreeNode[MAXN];// 结点队列 public static int[] iq = new int[MAXN];// id队列 public static int l, r; public static int widthOfBinaryTree(TreeNode root) { l = r = 0;// 初始化结点队列 int ans = 1;// 根 // 同步放根 nq[r] = root;// nq里面放root iq[r++] = 1;// iq里面放1 while (l < r) {// 结点队列有值 int size = r - l; // 左 ... 右 // l ... r-1 ans = Math.max(ans, iq[r - 1] - iq[l] + 1);// 取最大的宽度 for (int i = 0; i < size; i++) {// 一次处理一层的BFS // 弹出 TreeNode node = nq[l]; int id = iq[l++]; // 有左放左 if (node.left != null) { nq[r] = node.left; iq[r++] = id * 2;// 注意自己的编号 } // 有右放右 if (node.right != null) { nq[r] = node.right; iq[r++] = id * 2 + 1;// 注意自己的编号 } } }//while return ans; }
-
题目4 : 求二叉树的最大深度、求二叉树的最小深度
1. 最大深度
-
测试链接 : https://leetcode.cn/problems/maximum-depth-of-binary-tree/
-
思路
- 递归
- 当前结点是否为空?
- 为空, 返回深度: 0
- 不为空, 返回左右子树最大深度 + 1(本层所占的深度)
-
代码
O(n)
public static int maxDepth(TreeNode root) { return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }
2. 最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
如下图: a -> b -> c 为有效路径 a -> b -> null 为无效路径
(有叶子结点一定要走到叶子结点)
-
测试链接 : https://leetcode.cn/problems/minimum-depth-of-binary-tree/
-
思路
-
递归
-
当前的树是空树. 最小深度为0
-
当前树只有根结点, 最小深度为1
-
当前树不是以上两种情况
- 设置两个变量标识最右最小深度, 初始化为整数最大值
- 若左子树不为空, 递归计算左子树的最小深度
- 若右子树不为空, 递归计算右子树的最小深度
- 返回左右子树的最小深度+1(当前层所占深度)
-
代码
O(n)
-
public int minDepth(TreeNode root) {// 从根数到某个叶子结点 if (root == null) { // 当前的树是空树 return 0; } if (root.left == null && root.right == null) { // 当前root是叶节点 return 1; } int ldeep = Integer.MAX_VALUE; int rdeep = Integer.MAX_VALUE; // 只能调有节点的一边, 否则会被干扰, 计算出一个不符合要求的最小深度 if (root.left != null) { ldeep = minDepth(root.left); } if (root.right != null) { rdeep = minDepth(root.right); } return Math.min(ldeep, rdeep) + 1; }
-
-
题目5 : 二叉树先序序列化和反序列化
注意:中序遍历无法完成二叉树的序列化和反序列化,代码中给出了说明。后序遍历可以但不再详述。
-
测试链接 : https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/
-
思路
-
序列化
- 使用按照先序遍历的路线, 有值用StringBuilder追加 " 值 + ‘,’ “, 为空则追加”#,", 递归操作
-
反序列化
- 将拼接后的字符串用’,'分割为字符串数组, 使用全局变量 cnt来指示要处理的数组下标, 使用变量cur来读取当前正在处理的字符串
- 若cur为#, 返回null
- 若cur不为#, 则建立新的头结点, 赋值为cur的int值, 递归赋值左右节点, 返回头节点
-
代码
O(n)
-
// 二叉树可以通过先序、后序或者按层遍历的方式序列化和反序列化 // 但是,二叉树无法通过中序遍历的方式实现序列化和反序列化 // 因为不同的两棵树,可能得到同样的中序序列,即便补了空位置也可能一样。 // 比如如下两棵树 // __2 // / // 1 // 和 // 1__ // \ // 2 // 补足空位置的中序遍历结果都是{ null, 1, null, 2, null} // 提交这个类 public class Codec { public String serialize(TreeNode root) { StringBuilder builder = new StringBuilder();// 使用StringBuilder f(root, builder);// 递归调用序列化 return builder.toString(); } void f(TreeNode root, StringBuilder builder) {// 使用的是原来的builder, 返回void即可 // 类似于二叉树的先序遍历 if (root == null) {// 为null, 直接停 builder.append("#,");// 占位 } else { builder.append(root.val + ",");// 加值 f(root.left, builder);// 左树序列化 f(root.right, builder);// 右树序列化 } } // 当前数组消费到哪了 public static int cnt;// (全局变量方便) public TreeNode deserialize(String data) { String[] vals = data.split(",");// 用,分割出每一个值 cnt = 0;// 要处理的字符下标 return g(vals);// 递归消费 } TreeNode g(String[] vals) { // !!!每个条件判断下都有一个出口 String cur = vals[cnt++];// 用1个变量保存, 否则容易多++ if (cur.equals("#")) { return null;// 空结点 } else { TreeNode head = new TreeNode(Integer.valueOf(cur));// 建立有效结点 head.left = g(vals);// 左树消费 head.right = g(vals);// 右树消费 return head; } } }
-
-
题目6 : 二叉树按层序列化和反序列化
-
测试链接 : https://leetcode.cn/problems/serialize-and-deserialize-binary-tree/
-
思路
-
序列化
- 先判空
- 层次遍历中, 每次进队列之前进行序列化
-
反序列化
- 先判空
- 将字符串分割, 依次消耗数组中的值
- 先建立根结点, 根结点入队
- 当队列中还有结点
- 取出当前结点, 消耗当前后两位的值, 生成结点的左右孩子, 左右孩子中谁不为空,谁进队列,先加左,再加右
- 返回根结点
-
代码
O(n)
-
public class Codec { // 用于按层遍历 public static int MAXN = 10001; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; public String serialize(TreeNode root) { StringBuilder builder = new StringBuilder(); if (root != null) {// 树不为空,追加字符串 // 进队列之前,序列化 builder.append(root.val + ","); // 初始化队列 l = 0; r = 0; queue[r++] = root; while (l < r) { root = queue[l++];// 弹出当前结点 if (root.left != null) {// 左孩子不为空 // 进队列之前,序列化 builder.append(root.left.val + ",");// 左孩子的值,不要写错!!! queue[r++] = root.left; } else {// 没有左: 序列化 但 不加到队列 builder.append("#,"); } if (root.right != null) { // 进队列之前,序列化 builder.append(root.right.val + ",");// 右孩子的值,不要写错!!! queue[r++] = root.right; } else {// 没有右: 序列化 但 不加到队列 builder.append("#,"); } }// while } // endIf: 如果root==null,返回""(空串) return builder.toString();// 返回序列化后的字符串 } public TreeNode deserialize(String data) { if (data.equals("")) {// root == null return null; } String[] nodes = data.split(",");// 分割字符串 int index = 0;// 当前消耗的下标 TreeNode root = generate(nodes[index++]);// 建立根结点 // 初始化队列 l = 0; r = 0; queue[r++] = root;// 分割出的根结点 加入 队列 while (l < r) {// 队列不为空 TreeNode cur = queue[l++];// 弹出一个结点 // 直接把根结点的左右孩子消费,建立出来 cur.left = generate(nodes[index++]); cur.right = generate(nodes[index++]); // 左右孩子中谁不为空,谁进队列,先加左,再加右 if (cur.left != null) { queue[r++] = cur.left; } if (cur.right != null) { queue[r++] = cur.right; } }// end while return root;// 返回根结点 } private TreeNode generate(String val) { return val.equals("#") ? null : new TreeNode(Integer.valueOf(val)); } }
-
-
题目7 : 利用先序与中序遍历序列构造二叉树
-
测试链接 : https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
-
思路
- 特殊判断, 输入不合法的情况
- 根据先序遍历找出每一次递归的根结点, 使用根节点将中序遍历分为两半, 左半边为对应的左子树, 先序遍历中 根节点以后数对应数目的结点 也为左子树的节点; 右半边为对应的右子树, 先序遍历中 根结点以后数对应数目的结点 也为右子树的节点.
- 递归, 分别用以下方式递归构造左右子树: 给出先序遍历 及 处理子树下标范围, 给出中序遍历 及 处理子树下标范围, 根据第二点建立
-
代码
O(n)
-
public static TreeNode buildTree(int[] pre, int[] in) { // 特殊判断, 不合法情况 if (pre == null || in == null || pre.length != in.length) { return null; } // 设置一张表, 用于加速查找对应节点在中序遍历的位置 HashMap<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < in.length; i++) { map.put(in[i], i); } // 开始建树 return f(pre, 0, pre.length - 1, in, 0, in.length - 1, map);// 输入的下标, 看好, 不要越界 } public static TreeNode f(int[] pre, int l1, int r1, int[] in, int l2, int r2, HashMap<Integer, Integer> map) { // 特殊判断, 不合法情况 if (l1 > r1) { return null; } // 建头结点 TreeNode head = new TreeNode(pre[l1]); // 只有一个数, 直接返回头结点即可 if (l1 == r1) { return head; } // 查出中序遍历的位置 int k = map.get(pre[l1]); // pre : l1(........)[.......r1] // in : (l2......)k[........r2] // (...)是左树对应,[...]是右树的对应 head.left = f(pre, l1 + 1, l1 + k - l2, in, l2, k - 1, map); head.right = f(pre, l1 + k - l2 + 1, r1, in, k + 1, r2, map); return head; }
-
题目8 : 验证完全二叉树
-
测试链接 : https://leetcode.cn/problems/check-completeness-of-a-binary-tree/
-
思路
- 使用BFS, 挨个遍历, 判断每一个节点是否违规
- 第一个违规条件: 当前节点有右子树无左子树
- 第二个违规条件: 遇到过孩子不双全的节点 且 当前结点不是叶节点
- 若出现违规节点, 则该二叉树不是完全二叉树
- 若所有节点都符合要求, 则该二叉树是完全二叉树 (空树是完全二叉树)
- 使用BFS, 挨个遍历, 判断每一个节点是否违规
-
代码
O(n)
-
// 如果测试数据量变大了就修改这个值 // 用于BFS public static int MAXN = 101; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; public static boolean isCompleteTree(TreeNode h) { if (h == null) {// 判空 return true; } // BFS l = r = 0; queue[r++] = h; // 是否遇到过左右两个孩子不双全的节点 boolean leaf = false; // 开始遍历 while (l < r) { // 1.弹出一个结点 h = queue[l++]; // 2.判断是不是违规 // 第一个违规条件: 有右无左 第二个违规条件: 遇到过孩子不双全的节点 且 该结点不是叶节点 if ((h.left == null && h.right != null) || (leaf && (h.left != null || h.right != null))) { return false; } // 3.BFS if (h.left != null) { queue[r++] = h.left; } if (h.right != null) { queue[r++] = h.right; } // 4.判断该节点是不是孩子不双全节点 if (h.left == null || h.right == null) { // h.left == null: 左右孩子都没有; h.right == null: 没有右孩子 leaf = true; } }// end while return true;// 没有违规, 返回true }
-
题目9 : 求完全二叉树的节点个数,要求时间复杂度低于O(n)
-
测试链接 : https://leetcode.cn/problems/count-complete-tree-nodes/
-
思路
- 递归
- 根据根节点右子树 的 最左节点 能否到达最深层, 从而判断出为满二叉树的一侧和需要进行递归计算的一侧
- 若能到达最深层, 说明根节点的左子树为满二叉树, 节点个数为 2^(子树深度) - 1, 左子树节点个数加上根节点个数后的总节点个数为2^(子树深度); 右子树一定为完全二叉树, 递归调用即可
- 若不能到达最深层, 说明根节点的右子树为满二叉树, 且右子树的深度比根结点所在树的深度少1, 节点个数为2^(子树深度) - 1, 右子树节点个数加上根节点个数后的总节点个数为2^(子树深度); 左子树一定为完全二叉树, 递归调用即可
-
代码
O(h^2) h == log2(n)
-
public static int countNodes(TreeNode head) { // 判空 if (head == null) { return 0; } return f(head, 1, mostLeft(head, 1)); } // cur : 当前来到的节点 // level : 当前来到的节点在第几层 // h : 整棵树的高度,不是cur这棵子树的高度 // 求 : cur这棵子树上有多少节点 public static int f(TreeNode cur, int level, int h) { // BaseCase: 当前结点就是最深层的结点 即 叶子结点 if (level == h) { return 1;// 只有这一个结点 } // 判断cur右树上的最左节点 是否 到达最深层, 从而判断出为 满二叉树的一侧 和 需要递归调用的一侧 if (mostLeft(cur.right, level + 1) == h) { // cur右树上的最左节点,扎到了最深层 return (1 << (h - level)) + f(cur.right, level + 1, h); // cur左树 + 我自己 的深度 cur右树递归计算 } else { // cur右树上的最左节点,没扎到最深层 return (1 << (h - level - 1)) + f(cur.left, level + 1, h); // cur右树 + 我自己 的深度 cur左树递归计算 } } // 当前节点是cur,并且它在level层 // 返回从cur开始不停往左,能扎到几层 public static int mostLeft(TreeNode cur, int level) { while (cur != null) {// 不能用cur.left判断, 因为cur可能为空 level++; cur = cur.left; } return level - 1; }
-