[算法系列]递归应用——二叉树(2):一种带信息递归返回的求解方式
本文是递归系列文的第七篇,和上篇文章类似,介绍BinaryTree的解题思路。这里介绍一种和“遍历”行为类似的,自下而上递归返回信息的解题思路。其规则的写法、并不复杂的思路,可解决大多bintree中与子树有关的问题(但愿吧哈哈哈)
0.引子:求二叉树节点中的最大值和最小值
此题当然可以通过遍历整棵二叉树,将遍历途中遇到的最大值和最小值进行保存。遍历完成后maxVal和minVla即为所求:
class MaxAndMinInBinTree{
int maxVal = Integer.MIN_VALUE;
int minVal = Integer.MAX_VALUE;
public int[] getMinMaxInBT(TreeNode node){
process(node);
return new int[]{maxVal, minVal};
}
public void process(TreeNode node){
if(node == null) return;
maxVal = Math.max(node.val , maxVal);
minVal = Math.min(node.val, minVal);
process(node.left);
process(node.right);
}
}
那么这道题目除了用 二叉树的 ”遍历“ 思路,还可以怎么想呢?
回忆我们求二叉树的高度时用到的思路:
当前子树的高度 =max( 左子树的高度 ,右子树高度)+ 1
- 在递归中逐渐返回到头结点:
树的高度 = max(根的子树高度,根的右子树高度) +1
这其实就是一种和遍历思路稍显区别的,俺将其命名为带信息返回的求解思路。那么如果将其运用到求整棵树的最大最小值时,就可以自然而然地有如下想法:
当前子树的最大值 = max(左子树最大值,右子树最大值,当前节点的值)
当前子树的最小值 = min(左子树最小值,右子树最小值,当前节点的值)
- 在递归中逐渐向上返回到根节点。
该思路其一般可写成如下框架:
public ReturnData process(TreeNode node){
if(node == null){
//处理节点为空的情况,也是”递“到达的最深处,”归“上去的起点
}
ReturnData leftDate = process(node.left);
ReturnData rightData = process(node.right);
return new ReturnData(
//根据当前节点考虑可能性,构造当前要向上返回的信息集
)
}
下面来解释一波上述的伪代码:
-
ReturnData是自定义的一个类,里面包含的是递归过程中每次需要返回的所有信息
-
看整个process函数框架,看过我前面详解递归的小伙伴应该比较清楚:node == null 实际上就是边界条件,是递归的出口,在此处我们需要拿捏最小问题的处理方式。
紧接着,是两个递归调用process的过程,用leftData和rightData去接收左子树和右子树的结果。这一点前面也提到过:该模式下process函数会一口气走到最左下的为null的节点,然后再返回到右下,从叶节点开始,逐渐往上地返回。
在返回过程中,构建了一个新的ReturnData实例对象。这也就把所需要返回的信息向上地”归“到了根节点。
-
在新构建返回信息的实例对象中,就是我们根据可能性进行构造的过程。
好了,现在看看用这个方法时怎样操作的:
class MaxAndMinInBinTree2{
/* 这个题的返回信息就是所要求的最大值,最小值 */
public static class ReturnData{
public int maxVal;
public int minVal;
public ReturnData(int maxVal , int minVal){
this.maxVal = maxVal;
this.minVal = minVal;
}
}
public void getMinMax(TreeNode node){
ReturnData data = process(node);
System.out.println(data.maxVal + " " + data.minVal);
}
private ReturnData process(TreeNode head){
/* 递归边界条件,构造最初的返回信息 */ */
if(head == null){
return new ReturnData(Integer.MIN_VALUE, Integer.MAX_VALUE);
}
ReturnData leftData = process(head.left);
ReturnData rightData = process(head.right);
/* 逐渐向上递归的信息,由每一层的各种可能性决定:
比如:当前子树的最大值 = max(左子树最大值,右子树最大值,当前节点的值) */
return new ReturnData(
Math.max(Math.max(leftData.maxVal ,rightData.maxVal),head.val),
Math.min(Math.min(leftData.minVal , rightData.minVal),head.val)
);
}
}
1.判断一个二叉树是否平衡
在引子中,我们分别用”遍历“和"递归信息返回"两种思路求得了一颗二叉树中最大最小值,可以体会一下这两种思路的区别和联系。
看上去第二种方法要比第一种更繁琐,其实不然。因为,求二叉树中最大值这种问题,天然就属于是遍历可解的问题,当然遍历就好啦。
但是,”遍历“固然好,有些问题并不适合用遍历的方式做,比如一些直接或间接与子树有关的问题,比如就求一棵树中满足xxx条件的最大子树,或者像这个” 判断一个二叉树是否平衡“。下面来体会一下。
(1)设计ReturnData。
判断是否平衡,需要一个isBalance
的boolean变量来判断。另外,判断是否平衡实际上是根据左右子树的高度差来决定的,因此还需要一个int h
来保存当前节点高度。在递归过程中传递这两个值
(2)思考递归边界条件。
依旧是head为null的情况,此时返回的信息为isBalance=true
,h=0
,
(3)考虑每一次递归返回信息中的个属性赋值的可能性。
当前树为平衡树的条件为:左子树为平衡的 && 右子树为平衡的 && 左右子树高度差<=1
因此,若左子树不平衡,直接返回不平衡。右子树不平衡,直接返回不平衡。高度差 > 1直接返回不平衡
。
三者都通过了,就可在返回的信息中把isBalance置为true,同时求其高度了。
class CheckBlance{
public static class ReturnData{
public boolean isBalance;
public int h;
public ReturnData(boolean isB, int h){
this.isBalance = isB;
this.h = h;
}
}
public ReturnData process(TreeNode head){
if(head == null){
return new ReturnData(true ,0);
}
ReturnData leftReturnData = process(head.left);
/*当左子树不平衡时,当前树定不平衡,可以不用再考虑右树了。
这里isB为false,直接返回到根处都不为平衡,
因此h实际上没啥用
*/
if(!leftReturnData.isBalance)
return new ReturnData(false, 0);
ReturnData rightRetunrData =process(head.left);
//当左子树不平衡时,当前树定不平衡,可以不用再考虑当前节点了
if(!rightRetunrData.isBalance)
return new ReturnData(false,0);
//判断平衡调节并返回
if(Math.abs(leftReturnData.h - rightRetunrData.h) > 1 )
return new ReturnData(false,0);
//能到这个位置,说明一定是平衡的,此时h才有他的用武之地
return new ReturnData(true , Math.max(leftReturnData.h , rightRetunrData.h) + 1);
}
}
2.求二叉树中最大的二叉搜索子树
通过上面的两道可以看出这种方法还是挺可以的叭~
给一颗二叉树,返回该二叉树中最大的二叉搜索子树,比如下图中的最大二叉搜索子树为红圈中部分。
(1)设计ReturnData。
ReturnData需要四个信息:
-
当前节点的最大子BST的size:
size
。这个不用解释了吧,最后要求的就是这个
-
当前节点的最大子BST的头结点:
head
。考虑一下,若左子树的head是当前的左子树,右子树的head就是当前节点右子树,是不是可能这三个连成一块变成一个更大的BST呢?
-
当前节点的最小值和最大值
光是满足2是不够的,还得需要左子树的最大值小于当前节点值,右子树的最小值大于当前节点值才行。
(2)思考递归边界条件。
当节点为null时。size为0,maxHead为null,最大值保存系统最小值,最小值保存系统最大值
(3)考虑每一次递归返回信息中的个属性赋值的可能性。
-
size:有三种可能:
左子树的子BST的size最大时,
size = leftData.size
右子树的子BST的size最大时,
size = rightData.size
左子树、右子树、当前节点连成一个BST时,size为
leftData.size + 1+ rightData.size
-
head:同样是三可能:
head = leftData.head
head = rightData.head
head = 当前节点
-
min 和 max,
考察左子树的min、max,右子树min、max和当前节点值即可
下面为代码
class FindSubBST{
public static class ReturnData{
public int size;
public TreeNode head;
public int min;
public int max;
public ReturnData(int size,TreeNode head,int min ,int max){
this.size = size;
this.head = head;
this.min = min;
this.max = max;
}
}
public Node getMaxSizeSubBST(TreeNode head){
return process(head).head;
}
public ReturnData process(TreeNode head){
if(head == null){
return new ReturnData(0,null,Integer.MAX_VALUE,Integer.MIN_VALUE);
}
TreeNode left = head.left;
TreeNode right = head.right;
ReturnData leftData = process(left);
ReturnData rightData = process(right);
/* 左孩子就是左BST的头 右孩子也是右子BST的头
且
左孩子的最大值小于当前节点值
右孩子的最小值小于当前节点值
此时将三个部分合并,即为所求
*/
int includeItSelf = 0;
if(leftData.head == left && rightData.head == right
&& leftData.max < head.val && rightData.min > head.val)
includeItSelf = leftData.size + 1 + rightData.size;
/*
maxSize的三种可能性:
左子树,右子树,才求的三合一
*/
int p1 = leftData.size;
int p2 = rightData.size;
int maxSize = Math.max(Math.max(p1,p2),includeItSelf);
/*
maxHead的三种可能性:
maxSize为左子树时: maxHead为左子树头
maxSize为右子树时: maxHead为右子树头
maxSize为includeItSelf时 : 说明已经三者合1,maxHead为当前节点
*/
TreeNode maxHead = p1 > p2 ? leftData.head : rightData.head;
if(maxSize == includeItSelf)
maxHead = head;
/* 所有信息的所有可能性讨论完毕,构建一个新对象返回 */
return new ReturnData(maxSize, maxHead ,
Math.min(Math.min(leftData.min, rightData.min),head.val),
Math.min(Math.max(leftData.max, rightData.max),head.val)
);
}
}
3.求一颗二叉树上的最远距离
二叉树中,一个节点可以往上走和往下走,那么从节点A总能走到节点B。节点A走到节点B的距离为:A走到B最短路径上的节点个数。
如上图所示的二叉树中,最远距离为从6或7或8或9出发,连到11的距离(绿色线为其中一个路线)
在其左子树中,红色线代表的路径。
【思路】
(1)设计返回信息
- 当前节点的最大距离:这正是我们需要求的
- 当前节点的最大深度:这个值用于辅助求最大距离
(2)递归边界条件:即最小情况时的返回信息
当node=null时,当前节点最大距离即为0,深度也为0
(3)每一次递归向上传递信息的各种可能性
-
当前节点的最大距离取值为如下三种可能性:
-
左子树的最大距离
如上图中,1的左子树最大距离就是红色线6-2-9那根
-
右子树的最大距离
-
包含当前节点的横跨左右两子树深度的距离
如上图中的绿色线。
其求法为:
左子树的深度 + 1 + 右子树的深度
-
-
当前节点的高度值存在两种可能性:
- 左子树更高,则为左子树高度+1
- 右子树更高,则为右子树高度+1
代码:
/**
* 求二叉树的最大距离
*/
class FindMaxDistance{
public static class ReturnData{
public int maxDistance;
public int h;
public ReturnData(int maxDistance , int n){
this.maxDistance = maxDistance;
this.h = h;
}
}
public static int getMaxDistance(TreeNode head){
return process(head).maxDistance;
}
public static ReturnData process(TreeNode head){
if(head == null)
return new ReturnData(0,0);
ReturnData leftData = process(head.left);
ReturnData rightData = process(head.right);
int crossHeadDistance = leftData.maxDistance + 1 + rightData.maxDistance;
/* 当前maxDiatance就是上面谈到的三种可能性中选择最大的,然后作为返回信息的maxDistance */
int curMaxDistance = Math.max(Math.max(leftData.maxDistance,rightData.maxDistance),crossHeadDistance);
/* 同理当前H也一样*/
int curH = Math.max(leftData.h,rightData.h) + 1;
return new ReturnData(curMaxDistance, curH);
}
}
4.最大的快乐指数♂
N个员工,编号为1~N
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直属上司。现在有个宴会,宴会每邀请来一个员工 i 都会增加一定的快乐指数 Ri,但如果某个员工的直属上司来了,那么这个员工就不会来。计算邀请哪些员工可以使快乐指数最大,输出最大的快乐指数
【思路】
根据题意,首先设计出结点类:
public static class Node{
public int huo;
public List<Node> nexts;
public Node(int huo){
this.huo = huo;
nexts = new ArrayList<>();
}
对于每一个结点X(假设他的孩子为x1,x2…xn),只有来和不来情况,下面对这两种可能性分别讨论:
-
X来。
此时以X为根的树的活跃度为所有孩子不来时的活跃度和 :
x1不来的活跃度 + x2不来的活跃度 + ... + xn不来的活跃度
-
X不来
此时以X为根的树的活跃度为所有孩子来或者不来时的活跃度和 :
max(x1来的活跃度, x1不来的活跃度) + max(x2来的活跃度, x2不来的活跃度) + ... + max(xn来的活跃度,xn不来的活跃度)
啊,好像DP~~
对这就是树形DP。
那么现在我们来设计返回类ReturnData
返回类包含两个信息:当前节点来的活跃度、当前节点不来的活跃度
public static class ReturnData{
public int lai_huo;
public int bu_lai_huo;
public ReturnData(int lai_huo , int bu_lai_huo){
this.lai_huo = lai_huo;
this.bu_lai_huo = bu_lai_huo;
}
}
完整代码如下:
class MaxHappy{
public static class Node{
public int huo;
public List<Node> nexts;
public Node(int huo){
this.huo = huo;
nexts = new ArrayList<>();
}
public static class ReturnData{
public int lai_huo;
public int bu_lai_huo;
public ReturnData(int lai_huo , int bu_lai_huo){
this.lai_huo = lai_huo;
this.bu_lai_huo = bu_lai_huo;
}
}
/* 主函数调用入口 */
public static int getMaxHappy(Node head){
return Math.max(process(head).lai_huo , process(head).bu_lai_huo) ;
}
public static ReturnData process(Node head){
int lai_huo = 0 , bu_lai_huo = 0;
for(int i = 0 ; i < head.nexts.size() ; i ++ ){
Node next = head.nexts.get(i);
ReturnData nextData = process(next);
/* 当前节点来的情况 */
lai_huo += nextData.bu_lai_huo;
/* 当前节点不来的情况 */
bu_lai_huo += Math.max(nextData.lai_huo , nextData.bu_lai_huo);
}
return new ReturnData(lai_huo , bu_lai_huo);
}
}
}
小结
其实之前在做此类题时,采用递归方法会在递归中干许多事情,或者还会设置全局变量、中间值啥的。而该方法比较清晰地将每次递归需要做什么事情,传递什么信息打包。在每次调用时可以只用关注传递信息中的每个值得所有可能性。
另外,进一步理解递归设计、应用或者对递归过程有所疑惑的小伙伴可浏览之前的文章:
- [算法系列] 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进
- [算法系列] 递归应用: 快速排序+归并排序算法及其核心思想与拓展 … 附赠 堆排序算法
- [算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营
- [算法系列]搞懂DFS(1)——经典例题(数独游戏, 部分和, 水洼数目)图文详解
- [算法系列]搞懂DFS(2)——模式套路+经典例题详解(n皇后问题,素数环问题)
- [算法系列]递归应用——二叉树(1):二叉树遍历详解解+LeetCode经典题目+模板总结
待续。。。