二叉树的递归套路
例1:如何设计一个直观打印整棵二叉树的函数
将原本的树逆时针转90度
右中左的顺序
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
例2:二叉树结构如下定义:
Class Node {
V value;
Node left;
Node right;
Node parent;
}
给你二叉树中的某个节点,返回该节点的后继节点(在中序遍历中的该节点的下一个节点)
普遍做法是先找到head,然后再中序遍历找后继节点
假设X节点有右树,那么后继节点一定是右树上的最左节点
没有右树,往上不断找父,这个父是他的父的左孩子,这个父就是后继节点
public static Node getSuccessorNode(Node node) {
if (node == null) {
return node;
}
if (node.right != null) {
return getLeftMost(node.right);
} else { // 无右子树
Node parent = node.parent;
while (parent != null && parent.left != node) { // 当前节点是其父亲节点右孩子
node = parent;
parent = node.parent;
}
return parent;
}
}
public static Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
例3:
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。 如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。
给定一个输入参数N,代表纸条都从下边向上方连续对折N次。 请从上到下打印所有折痕的方向。
例如:N=1时,打印: down N=2时,打印: down down up
实际对折可发现,每次对折后,老折痕的左边会出现down痕,右边会出现up痕,实际上就是一个完全二叉树构,从上往下打印也就是中序遍历
public static void printAllFolds(int N) {
printProcess(1, N, true);
}
// 递归过程,来到了某一个节点,
// i是节点的层数,N一共的层数,down == true 凹 down == false 凸
public static void printProcess(int i, int N, boolean down) {
if (i > N) {
return;
}
printProcess(i + 1, N, true);
System.out.println(down ? "凹 " : "凸 ");
printProcess(i + 1, N, false);
}
二叉树的递归套路
可以解决面试中绝大多数的二叉树问题尤其是树型dp问题
本质是利用递归遍历二叉树的便利性
- 假设以X节点为头,假设可以向X左树和X右树要任何信息
- 在上一步的假设下,讨论以X为头节点的树,得到答案的可能性(最重要)
- 列出所有可能性后,确定到底需要向左树和右树要什么样的信息
- 把左树信息和右树信息求全集,就是任何一棵子树都需要返回的信息S
- 递归函数都返回S,每一棵子树都这么要求
- 写代码,在代码中考虑如何把左树的信息和右树信息整合出整棵树的信息
例1:给定一棵二叉树的头节点head,返回这颗二叉树是不是平衡二叉树(左子树与右子树的高度差不超过1)
需要的信息:左平,右平,|左高-右高| < 2
// 信息返回的结构体
public static class Info {
public boolean isBalaced;
public int height;
public Info(boolean b, int h) {
isBalaced = b;
height = h;
}
}
public static boolean isBalanced2(Node head) {
return process2(head).isBalaced;
}
public static Info process2(Node head) {
if (head == null) {
return new Info(true, 0);
}
Info leftInfo = process2(head.left);
Info rightInfo = process2(head.right);
int height = Math.max(leftInfo.height, rightInfo.height) + 1;
boolean isBalanced = true;
if (!leftInfo.isBalaced || !rightInfo.isBalaced || Math.abs(leftInfo.height - rightInfo.height) > 1) {
isBalanced = false;
}
return new Info(isBalanced, height);
}
普通解法:
public static boolean isBalanced1(Node head) {
boolean[] ans = new boolean[1];
ans[0] = true;
process1(head, ans);
return ans[0];
}
public static int process1(Node head, boolean[] ans) {
if (!ans[0] || head == null) {
return -1;
}
int leftHeight = process1(head.left, ans);
int rightHeight = process1(head.right, ans);
if (Math.abs(leftHeight - rightHeight) > 1) {
ans[0] = false;
}
return Math.max(leftHeight, rightHeight) + 1;
}
例2:给定一棵二叉树的头节点head,任何两个节点之间都存在距离,返回整棵二叉树的最大距离
分情况
1、与X无关,那么最大距离就是左树上的最大距离和右树上的最大距离取大
2、与X有关,就是左树上离X最远的点(子树高度)+右树上离X最远的点+1
因此需要的信息就是,左树的最大距离和高度,右树的最大距离和高度
public static class Info {
public int maxDistance;
public int height;
public Info(int dis, int h) {
maxDistance = dis;
height = h;
}
}
public static int maxDistance2(Node head) {
return process(head).maxDistance;
}
public static Info process(Node head) {
if (head == null) {
return new Info(0, 0);
}
Info leftInfo = process(head.left);
Info rightInfo = process(head.right);
int height = Math.max(leftInfo.height, rightInfo.height) + 1;
int maxDistance = Math.max(Math.max(leftInfo.maxDistance, rightInfo.maxDistance),
leftInfo.height + rightInfo.height + 1);
return new Info(maxDistance, height);
}
例3:给定一棵二叉树的头节点head,返回这颗二叉树中最大的二叉搜索子树的大小
搜索二叉树就是没有重复值,左树上的值都比其头小,右树上的值都比其头大
对X分类:
与X无关,就是左子树上的最大二叉搜索子树的大小与右子树上的取大
与X有关,左子树和右子树都必须是二叉搜索树,左侧最大值要小于X,右侧最小值要大于X
需要的信息:
最大搜索子树大小,最大最小值,整体是不是二叉搜索树
public static class Info {
public boolean isBST;
public int maxSubBSTSize;
public int min;
public int max;
public Info(boolean is, int size, int mi, int ma) {
isBST = is;
maxSubBSTSize = size;
min = mi;
max = ma;
}
}
public static int maxSubBSTSize2(Node head) {
if (head == null) {
return 0;
}
return process(head).maxSubBSTSize;
}
public static Info process(Node head) {
if (head == null) {
return null;
}
Info leftInfo = process(head.left);
Info rightInfo = process(head.right);
int min = head.value;
int max = head.value;
int maxSubBSTSize = 0;
if (leftInfo != null) {
min = Math.min(min, leftInfo.min);
max = Math.max(max, leftInfo.max);
maxSubBSTSize = Math.max(maxSubBSTSize, leftInfo.maxSubBSTSize);
}
if (rightInfo != null) {
min = Math.min(min, rightInfo.min);
max = Math.max(max, rightInfo.max);
maxSubBSTSize = Math.max(maxSubBSTSize, rightInfo.maxSubBSTSize);
}
boolean isBST = false;
if ((leftInfo == null ? true : (leftInfo.isBST && leftInfo.max < head.value))
&& (rightInfo == null ? true : (rightInfo.isBST && rightInfo.min > head.value))) {
isBST = true;
maxSubBSTSize = (leftInfo == null ? 0 : leftInfo.maxSubBSTSize)
+ (rightInfo == null ? 0 : rightInfo.maxSubBSTSize) + 1;
}
return new Info(isBST, maxSubBSTSize, min, max);
}
例4:派对的最大快乐值
最大快乐值
员工信息的定义如下:
class Employee {
public int happy; // 这名员工可以带来的快乐值
List<Employee> subordinates; // 这名员工有哪些直接下级
}
公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、 没有环的多叉树。树的头节点是公司唯一的老板。除老板之外的每个员工都有唯一的直接上级。 叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每个员工都有一个或多个直接下级。
这个公司现在要办party,你可以决定哪些员工来,哪些员工不来,规则:
1.如果某个员工来了,那么这个员工的所有直接下级都不能来
2.派对的整体快乐值是所有到场员工快乐值的累加
3.你的目标是让派对的整体快乐值尽量大
给定一棵多叉树的头节点boss,请返回派对的最大快乐值。
X来,他的直接下属们不来时,各自整颗树的最大快乐值
X不来,他的直接下属们来或者不来,各自整颗树的最大快乐值
public static class Info {
public int yes; // 头结点来的时候最大快乐值
public int no; // 头结点不来时最大快乐值
public Info(int y, int n) {
yes = y;
no = n;
}
}
public static int maxHappy2(Employee boss) {
if (boss == null) {
return 0;
}
Info all = process2(boss);
return Math.max(all.yes, all.no);
}
public static Info process2(Employee x) {
// 以基层员工作为终止条件
if (x.nexts.isEmpty()) {
return new Info(x.happy, 0);
}
int yes = x.happy;
int no = 0;
for (Employee next : x.nexts) {
Info nextInfo = process2(next);
yes += nextInfo.no;
no += Math.max(nextInfo.yes, nextInfo.no);
}
return new Info(yes, no);
}
例5:给定一棵二叉树的头节点head,返回这颗二叉树是不是满二叉树
性质:高度为h的满二叉树,有(2^h)-1个结点
需要的信息是高度和节点数
public static class Info {
public int height;
public int nodes;
public Info(int h, int n) {
height = h;
nodes = n;
}
}
public static boolean isFull2(Node head) {
if (head == null) {
return true;
}
Info all = process(head);
return (1 << all.height) - 1 == all.nodes;
}
public static Info process(Node head) {
if (head == null) {
return new Info(0, 0);
}
Info leftInfo = process(head.left);
Info rightInfo = process(head.right);
int height = Math.max(leftInfo.height, rightInfo.height) + 1;
int nodes = leftInfo.nodes + rightInfo.nodes + 1;
return new Info(height, nodes);
}
例6:给定一棵二叉树的头节点head,返回这颗二叉树中最大的二叉搜索子树的头节点
1、与X无关,左子树上是最大二叉搜索树或在右子树上
2、与X有关,看能不能和X连起来
需要的信息:
// 每个子树
public static class Info {
public Node maxSubBSTHead; // 子树上的最大二叉搜索子树的头结点
public int maxSubBSTSize; // 最大二叉搜索子树的结点数量
public int min; // 最大二叉搜索子树上的最小值
public int max; // 最大二叉搜索子树上的最大值
public Info(Node h, int size, int mi, int ma) {
maxSubBSTHead = h;
maxSubBSTSize = size;
min = mi;
max = ma;
}
}
头结点是自己就证明整颗树都是二叉搜索子树
public static Node maxSubBSTHead2(Node head) {
if (head == null) {
return null;
}
return process(head).maxSubBSTHead;
}
public static Info process(Node head) {
if (head == null) {
return null;
}
Info leftInfo = process(head.left);
Info rightInfo = process(head.right);
int min = head.value;
int max = head.value;
Node maxSubBSTHead = null;
int maxSubBSTSize = 0;
if (leftInfo != null) {
min = Math.min(min, leftInfo.min);
max = Math.max(max, leftInfo.max);
maxSubBSTHead = leftInfo.maxSubBSTHead;
maxSubBSTSize = leftInfo.maxSubBSTSize;
}
if (rightInfo != null) {
min = Math.min(min, rightInfo.min);
max = Math.max(max, rightInfo.max);
if (rightInfo.maxSubBSTSize > maxSubBSTSize) {
maxSubBSTHead = rightInfo.maxSubBSTHead;
maxSubBSTSize = rightInfo.maxSubBSTSize;
}
}
if ((leftInfo == null ? true : (leftInfo.maxSubBSTHead == head.left && leftInfo.max < head.value))
&& (rightInfo == null ? true : (rightInfo.maxSubBSTHead == head.right && rightInfo.min > head.value))) {
maxSubBSTHead = head;
maxSubBSTSize = (leftInfo == null ? 0 : leftInfo.maxSubBSTSize)
+ (rightInfo == null ? 0 : rightInfo.maxSubBSTSize) + 1;
}
return new Info(maxSubBSTHead, maxSubBSTSize, min, max);
}
例7:给定一棵二叉树的头节点head,返回这颗二叉树中是不是完全二叉树
基本解法是做宽度优先遍历
1、任何节点有右,无左,一定不是完全二叉树,否则继续
2、一旦遇到,左右不双全,后续结点都必须是叶节点
public static boolean isCBT1(Node head) {
if (head == null) {
return true;
}
LinkedList<Node> queue = new LinkedList<>();
// 是否遇到过左右两个孩子不双全的节点
boolean leaf = false;
Node l = null;
Node r = null;
queue.add(head);
while (!queue.isEmpty()) {
head = queue.poll();
l = head.left;
r = head.right;
if (
// 如果遇到了不双全的节点之后,又发现当前节点不是叶节点
(leaf && !(l == null && r == null)) || (l == null && r != null)) {
return false;
}
if (l != null) {
queue.add(l);
}
if (r != null) {
queue.add(r);
}
if (l == null || r == null) {
leaf = true;
}
}
return true;
}
递归套路:
以X为头的二叉树是不是满二叉树,根据最后一个缺口的位置来分
1、没有缺口,是一个满二叉树
2、有缺口
(1)、缺口(左树的叶子几点不是满的)在左树上
(2)、左树满了,但是没到右树
(3)、左树满了,右树上挂了节点
需要的信息,是否满,高度
如果左,右都满,高度一样,那么符合条件1
左树是否是完全二叉树,右树是满,左树高度比右树高度大1,符合条件2.1
左树是满的,右树是满的,左树的高度比右树高度大1,符合条件2.2
左树是满的,右树是完全二叉树,左树与右树高度一样,符合条件2.3
public static class Info {
public boolean isFull;
public boolean isCBT;
public int height;
public Info(boolean full, boolean cbt, int h) {
isFull = full;
isCBT = cbt;
height = h;
}
public static boolean isCBT2(Node head) {
if (head == null) {
return true;
}
return process(head).isCBT;
}
public static Info process(Node head) {
if (head == null) {
return new Info(true, true, 0);
}
Info leftInfo = process(head.left);
Info rightInfo = process(head.right);
int height = Math.max(leftInfo.height, rightInfo.height) + 1;
boolean isFull = leftInfo.isFull && rightInfo.isFull && leftInfo.height == rightInfo.height;
boolean isCBT = false;
if (isFull) {
isCBT = true;
} else {
// 以X为头,不满,左树与右树有一个不是完全,那么条件2.1,2.2,2.3都不成立
if (leftInfo.isCBT && rightInfo.isCBT) {
// 2.1
if (leftInfo.isCBT && rightInfo.isFull && leftInfo.height == rightInfo.height + 1) {
isCBT = true;
}
// 长满了 2.2
if (leftInfo.isFull && rightInfo.isFull && leftInfo.height == rightInfo.height + 1) {
isCBT = true;
}
// 最后一层结点左边撑满了,条件2.3
if (leftInfo.isFull && rightInfo.isCBT && leftInfo.height == rightInfo.height) {
isCBT = true;
}
}
}
return new Info(isFull, isCBT, height);
}
例8:给定一棵二叉树的头节点head,和另外两个节点a和b。返回a和b的最低公共祖先
解法一:通过遍历实现父节点表,将a的所有父节点行程一个HashSet,然后找b的祖先,有交汇就是最低公共祖先
public static Node lowestAncestor1(Node head, Node o1, Node o2) {
if (head == null) {
return null;
}
// key的父节点是value
HashMap<Node, Node> parentMap = new HashMap<>();
parentMap.put(head, null);
fillParentMap(head, parentMap);
HashSet<Node> o1Set = new HashSet<>();
Node cur = o1;
o1Set.add(cur);
while (parentMap.get(cur) != null) {
cur = parentMap.get(cur);
o1Set.add(cur);
}
cur = o2;
while (!o1Set.contains(cur)) {
cur = parentMap.get(cur);
}
return cur;
}
public static void fillParentMap(Node head, HashMap<Node, Node> parentMap) {
if (head.left != null) {
parentMap.put(head.left, head);
fillParentMap(head.left, parentMap);
}
if (head.right != null) {
parentMap.put(head.right, head);
fillParentMap(head.right, parentMap);
}
}
递归套路:
1、a,b 无一个在x上
2、a,b只有一个在x上
3、a,b都在x上
3.1 左右各有一个
3.2 a,b都在左
3.3 a,b都在右
3.4 x是a,或b
public static class Info {
// 最初交汇点
public Node ans;
// 发现了o1
public boolean findO1;
// 发现了o2
public boolean findO2;
public Info(Node a, boolean f1, boolean f2) {
ans = a;
findO1 = f1;
findO2 = f2;
}
}
public static Node lowestAncestor2(Node head, Node o1, Node o2) {
return process(head, o1, o2).ans;
}
public static Info process(Node head, Node o1, Node o2) {
if (head == null) {
return new Info(null, false, false);
}
Info leftInfo = process(head.left, o1, o2);
Info rightInfo = process(head.right, o1, o2);
boolean findO1 = head == o1 || leftInfo.findO1 || rightInfo.findO1;
boolean findO2 = head == o2 || leftInfo.findO2 || rightInfo.findO2;
Node ans = null;
// 左树上已经提前交汇了
if (leftInfo.ans != null) {
ans = leftInfo.ans;
}
// 右树已经提前交汇了
if (rightInfo.ans != null) {
ans = rightInfo.ans;
}
// 左,右都没提前交汇,但是此时又都找到了,那么说明X就是交汇点
if (ans == null) {
if (findO1 && findO2) {
ans = head;
}
}
// 到了这一步,就说明至少有一个没找到,所以将信息传递上去,继续判断
return new Info(ans, findO1, findO2);
}