目录
10.二叉树(上)结构与遍历 总览
笔记思维导图链接
参考左程云体系算法课程笔记
参考慕课网算法体系课程笔记
常见题目汇总:
1. 二叉树广度优先遍历
题目链接:
题意:
- 给你二叉树的根节点
root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
题解:
- 1.使用队列实现层序遍历的功能,按层入队出队
- 2.与前序队列类似,每次先处理要访问的一层头节点,并将左右子节点入队
- 3.同样注意边界条件,null不能入队列
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
// - 1.使用队列实现层序遍历的功能,按层入队出队
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
// - 2.与前序队列类似,每次先处理要访问的一层头节点,并将左右子节点入队
while(!queue.isEmpty()) {
// 处理措施:将每一层的节点放入一个集合中
List<Integer> list = new ArrayList<>();
int size = queue.size(); // 每一层的大小,每层处理时都改变
while(size > 0) { // 处理当前层size个节点
TreeNode cur = queue.poll();
list.add(cur.val);
size--;
// - 3.同样注意边界条件,null不能入队列
if(cur.left != null) queue.add(cur.left);
if(cur.right != null) queue.add(cur.right);
}
res.add(list);
}
return res;
}
复杂度分析:
2. 二叉树序列化和反序列化
题目链接:
题意:
-
请实现两个函数,分别用来序列化和反序列化二叉树。
-
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,
-
你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
- 注意:结构要保证唯一,即树与字符串要一一对应
示例:
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
题解:
使用先序遍历实现
序列化:
- 1.使用stringBuilder来拼接每个节点代表的字符串
- 2.使用先序遍历,每遍历到一个节点,就将节点转为字符串拼接到总字符串中
- 3.注意,
- null的处理,要用占位符,并且有分隔符,反序列化时才知道是null节点,返回的标志
- 每个节点字符串要有","号分割,是为了区分1,23 与 12,3这两种树结构节点
反序列化
- 1.准备原材料
- 通过之前定义的分割符",",将字符串进行分割,逐个将每个节点放入字符串数集合中准备构建
- 2.先序遍历构建节点
- 即,每次拿出的节点,先构建出头节点,再深度遍历左右子树
注意:也可以使用后序遍历,但不能使用中序遍历,中序遍历序列化有歧义
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
// - 1.使用stringBuilder来拼接每个节点代表的字符串
StringBuilder res = new StringBuilder();
if(root == null) return "[]";
// - 2.使用先序遍历,每遍历到一个节点,就将节点转为字符串拼接到总字符串中
preDFS(root, res);
return res.toString();
}
private void preDFS(TreeNode node, StringBuilder res) {
// - 3.注意,
// - null的处理,要用占位符,反序列化时才知道是null节点,返回的标志
// - 每个节点字符串要有","号分割,是为了区分1,23 与 12,3这两种树结构节点
if(node == null) {
res.append("#,"); // 切记,不要忘记分割符
return;
}
res.append(node.val + ",");
preDFS(node.left, res);
preDFS(node.right, res);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
// - 1.准备原材料
// - 通过之前定义的分割符",",将字符串进行分割,逐个将每个节点放入字符串数集合中准备构建
if(data.equals("[]")) return null;
LinkedList<String> list = new LinkedList<>();
for(String s : data.split(",")) {
list.add(s);
}
// - 2.先序遍历构建节点
// - 即,每次拿出的节点,先构建出头节点,再深度遍历左右子树
return preDS(list);
}
private TreeNode preDS(LinkedList<String> list) {
String curStr = list.poll();
if(curStr.equals("#")) return null;
TreeNode cur = new TreeNode(Integer.valueOf(curStr));
cur.left = preDS(list);
cur.right = preDS(list);
return cur;
}
使用层序遍历实现
序列化
-
1.同样使用队列进行层序遍历,在遍历时将节点转为字符串进行拼接
-
2.因要处理null节点,故在遍历子节点时就要序列化,将null处理了
-
3.同样注意边界问题,null,分隔符
// Encodes a tree to a single string. public String serialize2(TreeNode root) { StringBuilder res = new StringBuilder(); if(root == null) return "[]"; // - 1.同样使用队列进行层序遍历,在遍历时将节点转为字符串进行拼接 Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); res.append(root.val + ","); // 先序列化根节点 // - 2.因要处理null节点,故在遍历子节点时就要序列化,将null处理了 while(!queue.isEmpty()) { TreeNode cur = queue.poll(); if(cur.left != null) { res.append(cur.left.val + ","); // 序列化 queue.add(cur.left); // 将非空子节点放入队列,用以序列化它的子节点 } else { // 处理null的清空 res.append("#,"); } if(cur.right != null) { res.append(cur.right.val + ","); queue.add(cur.right); } else { res.append("#,"); } } return res.toString(); }
反序列化
-
1.同样,先准备原材料,用分隔符将字符都拿到放入字符串数组中
-
2.与序列化顺序一致,先建立根节点
-
3.再用父节点去建立它的左右孩子节点
- 不为null,放进队列,继续下一轮的反序列化
- 为null,只建立空节点
// Decodes your encoded data to tree. public TreeNode deserialize(String data) { if(data.equals("[]")) { return null; } // - 1.同样,先准备原材料,用分隔符将字符都拿到放入字符串数组中 LinkedList<String> list = new LinkedList<>(); for(String s : data.split(",")) { list.add(s); } // - 2.与序列化顺序一致,先建立根节点 Queue<TreeNode> queue = new LinkedList<>(); TreeNode root = generateNode(list.poll()); queue.add(root); // - 3.再用父节点去建立它的左右孩子节点 // - 不为null,放进队列,继续下一轮的反序列化 // - 为null,只建立空节点 while(!queue.isEmpty()) { TreeNode cur = queue.poll(); cur.left = generateNode(list.poll()); cur.right = generateNode(list.poll()); if(cur.left != null) queue.add(cur.left); if(cur.right != null) queue.add(cur.right); } return root; } private TreeNode generateNode(String str) { if(str.equals("#")) return null; return new TreeNode(Integer.valueOf(str)); }
复杂度分析:
3. 将 N 叉树编码为二叉树
题目链接:
题意:
- 设计一个算法,可以将 N 叉树编码为二叉树,并能将该二叉树解码为原 N 叉树。
- 一个 N 叉树是指每个节点都有不超过 N 个孩子节点的有根树。类似地,一个二叉树是指每个节点都有不超过 2 个孩子节点的有根树。
- 编码 / 解码的算法的实现没有限制,你只需要保证一个 N 叉树可以编码为二叉树且该二叉树可以解码回原始 N 叉树即可。
例如:
题解:
总体思路:X节点的所有孩子节点,放在X左子树的右边界上,右子树可以不用,这样对应的二叉树是唯一的
多叉树转二叉树
-
1.使用递归法,先建立根节点,将深度递归建立右边界结构作为根节点的左子树
-
2.遍历每层的子节点集合,将子节点集合中的节点连成一条右边界
- 即,每新建的二叉树节点,都连在前一个节点的右子树上
- 注意,先拿到第一个子节点,作为当前边界的头节点(长兄节点),作为返回节点与父节点相连
-
3.处理每个子节点时,要继续深度递归处理,
- 例如b,还要处理b的孩子节点e,f.将b一个整分支处理好后再处理下个子节点
// Encodes an n-ary tree to a binary tree. public TreeNode encode(Node root) { // - 1.使用递归法,先建立根节点,将子节点深度递归建立右边界的结构作为根节点的左子树 if(root == null) return null; TreeNode head = new TreeNode(root.val); head.left = encodeDFS(root.children); return head; } private TreeNode encodeDFS(List<Node> children) { // - 2.遍历每层的子节点集合,将子节点集合中的节点连成一条右边界 // - 即,每新建的二叉树节点,都连在前一个节点的右子树上 // - 注意,先拿到第一个子节点,作为当前边界的头节点(长兄节点),作为返回节点与父节点相连 TreeNode head = null; TreeNode pre = null; for(Node child : children) { TreeNode curNode = new TreeNode(child.val); if(head == null) { // 表明是第一个子节点,长兄,右边界的头 head = curNode; } else { pre.right = curNode; // 否则,作为一个子节点的右子树 } // - 3.处理每个子节点时,要继续深度递归处理,整孩子们处理好后再处理下个兄弟节点 pre = curNode; // 指针来到当前节点,先处理当前节点的孩子们,再处理当前节点的弟弟们 pre.left = encodeDFS(child.children); } return head; }
二叉树恢复成多叉树
-
1.先建立头节点,并深度遍历找到所有孩子节点,才是一个完整的多叉树节点结构
-
2.root是长兄,要将所有兄弟节点做成列表返回给父亲节点
-
3.每个兄弟节点,要继续深度递归,将兄弟节点下的所有孩子信息拿到
// Decodes your binary tree to an n-ary tree. public Node decode(TreeNode root) { // - 1.先建立头节点,并深度遍历找到所有孩子节点,才是一个完整的多叉树节点结构 if(root == null) return null; return new Node(root.val, deDFS(root.left)); } private List<Node> deDFS(TreeNode root) { // - 2.root是长兄,要将所有兄弟节点做成列表返回给父亲节点 List<Node> children = new ArrayList<>(); while(root != null) { // - 3.每个兄弟节点,要继续深度递归,将兄弟节点下的所有孩子信息拿到 Node curNode = new Node(root.val, deDFS(root.left)); children.add(curNode); root = root.right; } return children; }
复杂度分析:
- 均为O(n)
4. 求二叉树最宽的一层有多少节点
题目链接:
题意:
-
给定一个二叉树,编写一个函数来获取这个树的最大宽度。
- 树的宽度是所有层中的最大宽度。
- 这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空。
-
每一层的宽度被定义为两个端点之间的长度。
- (该层最左和最右的非空节点,两端点间的null节点也计入长度)
示例:
输入:
1
/ \
3 2
/ \ \
5 3 9
输出: 4
解释: 最大值出现在树的第 3 层,宽度为 4 (5,3,null,9)。
题解:
null节点不计入宽度的情况
-
1.问题关键是添加一种找到每层结束节点位置的机制,这样才能统计每层的宽度
- 申请两个变量进行统计:
- curEnd:表示当前层最右的一个节点
- nextEnd表示将要处理的下一层的最后一个节点,因为处理当前层,将子节点入队列时可以一并标记
- 申请两个变量进行统计:
-
2.每层在没有遍历到curEnd节点前,队列中弹出每个节点,并计数,
- 使用max与每层的宽度比较,得出最大宽度
-
3.在处理当前层计数的同时,要将孩子节点放入队列中,并统计最后一个节点位置,
- 当遍历到下一层时,统计的结果可以直接赋值给curEnd使用
public static int maxWidthNoMap(Node root) { // - 1.问题关键是添加一种找到每层结束节点位置的机制,这样才能统计每层的宽度 // - 申请两个变量进行统计: // - curEnd:表示当前层最右的一个节点 // - nextEnd表示将要处理的下一层的最后一个节点,因为处理当前层,将子节点入队列时可以一并标记 if (root == null) return 0; Queue<Node> queue = new LinkedList<>(); queue.add(root); Node curEnd = root; Node nextEnd = null; int max = 0; int curLevelNodes = 0; // 当前层的宽度 // - 2.每层在没有遍历到curEnd节点前,队列中弹出每个节点,并计数, // - 使用max与每层的宽度比较,得出最大宽度 while (!queue.isEmpty()) { Node cur = queue.poll(); // addNode(cur.left, nextEnd, queue);引用传递,值传不回来 // - 3.在处理当前层计数的同时,要将孩子节点放入队列中,并统计最后一个节点位置, // - 当遍历到下一层时,统计的结果可以直接赋值给curEnd使用 if (cur.left != null) { queue.add(cur.left); nextEnd = cur.left; } if (cur.right != null) { queue.add(cur.right); nextEnd = cur.right; } curLevelNodes++; // 维护节点个数 if (cur == curEnd) { max = Math.max(max, curLevelNodes); curLevelNodes = 0; curEnd = nextEnd; } } return max; }
null节点计入宽度的情况
-
当null要进行统计处理时,再用节点来统计,就很麻烦,因为null的左右子节点不好处理
- 如果将null放入队列,null是没有左右子树,会报空指针错误
- 如果new TreeNode()对象放入queue,这个对象与null不等价,无法找到这个对象
-
1.故,为了统计,改为用节点下标位置来统计,即,父节点i,左子2 * i +1,右子树2 * 2 + 2
-
2.这样,需要将节点信息进行封装,用变量记录每个节点所处的层次,下标的位置,节点node
-
3.每层只需将非空节点的子节点处理好,
- 对于每一个深度,第一个遇到的节点是最左边的节点,最后一个到达的节点是最右边的节点。
- R - L + 1即宽度
public int widthOfBinaryTree(TreeNode root) { // - 1.故,为了统计,改为用节点下标位置来统计,即,父节点i,左子2 * i +1,右子树2 * 2 + 2 // - 2.这样,需要将节点信息进行封装,用变量记录每个节点所处的层次,下标的位置,节点node Queue<NodeVo> queue = new LinkedList<>(); queue.add(new NodeVo(root, 0, 0)); int curDepth = 0, left = 0, ans = 0; while(!queue.isEmpty()) { NodeVo cur = queue.poll(); // - 3.每层只需将非空节点的子节点处理好, // - 对于每一个深度,第一个遇到的节点是最左边的节点,最后一个到达的节点是最右边的节点。 // - 其中,R - L + 1即宽度 if(cur.node != null) { // 只需处理非null节点,子节点会通过数组下标确定 queue.add(new NodeVo(cur.node.left, cur.depth + 1, cur.pos * 2 + 1)); queue.add(new NodeVo(cur.node.right, cur.depth + 1, cur.pos * 2 + 2)); if(curDepth != cur.depth) { // 说明cur为下一层的元素,当前层已经遍历完了 curDepth = cur.depth; left = cur.pos; // 下一层的第一个元素位置 } ans = Math.max(ans, cur.pos - left + 1); // 统计每层每个元素构成的宽度最大值 } } return ans; } class NodeVo { TreeNode node; int depth, pos; public NodeVo(TreeNode node, int depth, int pos) { this.node = node; this.depth = depth; this.pos = pos; } }
复杂度分析:
5. 二叉树指定节点的后继节点
类似题目链接:
题意:
- 给定一棵二叉搜索树和其中的一个节点 p ,找到该节点在树中的中序后继。
- 如果节点没有中序后继,请返回 null
示例:
输入:root = [2,1,3], p = 1
输出:2
解释:这里 1 的中序后继是 2。请注意 p 和返回值都应是 TreeNode 类型。
题解:
节点信息中,有指向父节点的指针
求X节点的后继节点分为两种情况
-
1.X有右子树的情况
- 下一个后继节点一定是右树上的最左孩子,
- 即左头右,X为头,在右树中,也是左头右,第一个后继节点就是右树中的最左孩子
-
2.X没有右子树的情况
-
就要找父节点,一直往上找父亲节点,
- 如果某一个节点是父亲节点的左孩子,该父节点就是要找的后继节点
-
因为,x没有右数,x所在的树为一个节点的左树,
- 即x为后继节点左树中的最后一个节点,
- 则,x的下个节点就是这个父节点
public static class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data) {
this.value = data;
}
}
public static Node getSuccessorNode(Node node) {
if(node == null) return null;
// 1. x有右子树的情况
if(node.right != null) return getLeftMost(node.right);
// 2. x没有右子树,找父亲节点
Node parent = node.parent;
while(parent != null && parent.right == node) {// 当前节点是其父亲节点右孩子
node = parent;
parent = node.parent; // 继续往上找
}
return parent;
}
private static Node getLeftMost(Node node) {
if(node == null) return null;
while(node.left != null) { // 一直找node.left
node = node.left;
}
return node;
}
复杂度分析:
- O(k),即从当前节点到后继节点中间的距离k,即二叉树中两个节点之间的节点个数
二叉树是二叉搜索树,节点有大小关系
- 后继节点的值一定不会小于节点p的值,而是大于或等于节点p的值中的所有节点中值最小的一个
- 从根节点开始,每到达一个节点就比较当前节点的值和节点p的值
- 如果当前节点的值小于或等于节点p的值,那么节点p的后继节点应该在它的右子树
- 如果当前节点的值大于或等于节点p的值,那么当前节点有可能是p的下一个节点,
- 此时当前节点的值比节点p的值大,但节点p的后继节点是所有比它大的节点中值最小的一个,
- 因此接下来前往当前节点的左子树,确定是否能找到值更小,但仍然大于节点p的值的节点
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
if(root == null) return null;
TreeNode cur = root;
TreeNode res = null;
while(cur != null) {
if(cur.val > p.val) { // 候选者
res = cur; // 每找到一个候选者,都更新
cur = cur.left; // 继续找左子树,看有没有大于p节点的
} else {
cur = cur.right;
}
}
return res;
}
复杂度分析:
O(k),是根节点到后继节点的距离
6. 从上到小打印所有折痕方向
题目链接:
题意:
-
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。 如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
-
给定一个输入参数N,代表纸条都从下边向上方连续对折N次。 请从上到下打印所有折痕的方向。
例如:N=1时,打印: down N=2时,打印: down down up
题解:
- 折痕的方向实质就是二叉树中序遍历的结果
- 因此,本题的实质就是二叉树的中序遍历打印
public static void printAllFolds(int N) {
process(1, N, true);
}
// 当前你来了一个节点,脑海中想象的!
// 这个节点在第i层,一共有N层,N固定不变的
// 这个节点如果是凹的话,down = T
// 这个节点如果是凸的话,down = F
// 函数的功能:中序打印以你想象的节点为头的整棵树!
public static void process(int i, int N, boolean down) {
if (i > N) {
return;
}
process(i + 1, N, true);
System.out.print(down ? "凹 " : "凸 ");
process(i + 1, N, false);
}
复杂度分析:
O(n)