[算法系列]递归应用——二叉树(2):一种带信息递归返回的求解方式

[算法系列]递归应用——二叉树(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);
    }
}

那么这道题目除了用 二叉树的 ”遍历“ 思路,还可以怎么想呢?

回忆我们求二叉树的高度时用到的思路:

  1. 当前子树的高度 =max( 左子树的高度 ,右子树高度)+ 1
  2. 在递归中逐渐返回到头结点:树的高度 = max(根的子树高度,根的右子树高度) +1

这其实就是一种和遍历思路稍显区别的,俺将其命名为带信息返回的求解思路。那么如果将其运用到求整棵树的最大最小值时,就可以自然而然地有如下想法:

  1. 当前子树的最大值 = max(左子树最大值,右子树最大值,当前节点的值)
  2. 当前子树的最小值 = min(左子树最小值,右子树最小值,当前节点的值)
  3. 在递归中逐渐向上返回到根节点。

该思路其一般可写成如下框架:

public ReturnData process(TreeNode node){
    if(node == null){
        //处理节点为空的情况,也是”递“到达的最深处,”归“上去的起点
    }
    ReturnData leftDate = process(node.left);
    ReturnData rightData = process(node.right);
    
    return new ReturnData(
    	//根据当前节点考虑可能性,构造当前要向上返回的信息集
    )
}

下面来解释一波上述的伪代码:

  1. ReturnData是自定义的一个类,里面包含的是递归过程中每次需要返回的所有信息

  2. 看整个process函数框架,看过我前面详解递归的小伙伴应该比较清楚:node == null 实际上就是边界条件,是递归的出口,在此处我们需要拿捏最小问题的处理方式。

    紧接着,是两个递归调用process的过程,用leftData和rightData去接收左子树和右子树的结果。这一点前面也提到过:该模式下process函数会一口气走到最左下的为null的节点,然后再返回到右下,从叶节点开始,逐渐往上地返回。

    在返回过程中,构建了一个新的ReturnData实例对象。这也就把所需要返回的信息向上地”归“到了根节点。

  3. 在新构建返回信息的实例对象中,就是我们根据可能性进行构造的过程。

好了,现在看看用这个方法时怎样操作的:

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=trueh=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需要四个信息:

  1. 当前节点的最大子BST的size:size

    这个不用解释了吧,最后要求的就是这个

  2. 当前节点的最大子BST的头结点:head

    考虑一下,若左子树的head是当前的左子树,右子树的head就是当前节点右子树,是不是可能这三个连成一块变成一个更大的BST呢?

  3. 当前节点的最小值和最大值

    光是满足2是不够的,还得需要左子树的最大值小于当前节点值,右子树的最小值大于当前节点值才行。

(2)思考递归边界条件。

当节点为null时。size为0,maxHead为null,最大值保存系统最小值,最小值保存系统最大值

(3)考虑每一次递归返回信息中的个属性赋值的可能性。

  1. size:有三种可能:

    左子树的子BST的size最大时,size = leftData.size

    右子树的子BST的size最大时,size = rightData.size

    左子树、右子树、当前节点连成一个BST时,size为leftData.size + 1+ rightData.size

  2. head:同样是三可能:

head = leftData.head

head = rightData.head

head = 当前节点

  1. 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)设计返回信息

  1. 当前节点的最大距离:这正是我们需要求的
  2. 当前节点的最大深度:这个值用于辅助求最大距离

(2)递归边界条件:即最小情况时的返回信息

当node=null时,当前节点最大距离即为0,深度也为0

(3)每一次递归向上传递信息的各种可能性

  1. 当前节点的最大距离取值为如下三种可能性:

    1. 左子树的最大距离

      如上图中,1的左子树最大距离就是红色线6-2-9那根

    2. 右子树的最大距离

    3. 包含当前节点的横跨左右两子树深度的距离

      如上图中的绿色线。

      其求法为:左子树的深度 + 1 + 右子树的深度

  2. 当前节点的高度值存在两种可能性:

    1. 左子树更高,则为左子树高度+1
    2. 右子树更高,则为右子树高度+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),只有来和不来情况,下面对这两种可能性分别讨论:

  1. X来。

    此时以X为根的树的活跃度为所有孩子不来时的活跃度和x1不来的活跃度 + x2不来的活跃度 + ... + xn不来的活跃度

  2. 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);
        }
    }
}

小结

其实之前在做此类题时,采用递归方法会在递归中干许多事情,或者还会设置全局变量、中间值啥的。而该方法比较清晰地将每次递归需要做什么事情,传递什么信息打包。在每次调用时可以只用关注传递信息中的每个值得所有可能性。

另外,进一步理解递归设计、应用或者对递归过程有所疑惑的小伙伴可浏览之前的文章:

  1. [算法系列] 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进
  2. [算法系列] 递归应用: 快速排序+归并排序算法及其核心思想与拓展 … 附赠 堆排序算法
  3. [算法系列] 深入递归本质+经典例题解析——如何逐步生成, 以此类推,步步为营
  4. [算法系列]搞懂DFS(1)——经典例题(数独游戏, 部分和, 水洼数目)图文详解
  5. [算法系列]搞懂DFS(2)——模式套路+经典例题详解(n皇后问题,素数环问题)
  6. [算法系列]递归应用——二叉树(1):二叉树遍历详解解+LeetCode经典题目+模板总结

待续。。。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值