面试题55 - I. 二叉树的深度-动态规划递推回溯

做题目前随便说点

  • 树是一种抽象数据类型,一种具有树结构形式的数据集合。

  • 节点个数确定,有层次关系。

  • 有根节点。

  • 除了根,每个节点有且只有一个父节点。

  • 没有环路。

  • 所有数据结构都可以用链表表示或者用数组表示,树也一样。

面试题55 - I. 二叉树的深度

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

3
/
9 20
/
15 7
返回它的最大深度 3 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题:

审题:

  • 也就是知道根节点的引用和属性,和左右子树的引用。求根节点所在的树深度。

  • 什么是深度呢?题目给出了定义。从根节点到所有叶子的深度路径长度中最长的那条路径的长度。就是树的深度。

  • 那么什么是路径长度呢?也就是说长度的单位是什么?审题,审一下题目的例子可以得知,就是从根节点到叶子节点经过的能访问到节点属性的节点个数,这个约定不怎么优化,有点拗口。但是比较容易理解。

  • 换句话说,多少个叶子就有多少条路径了。

  • 我们要找出这些路径中最长的那个。

  • 我们可以把所有路径长度放到一个数组中,然后从数组中找出最大的长度,就是树的深度。

  • 求数组最大值,这个不难用“打擂法”,什么是打擂法,可以查一下资料。

  • 我们可以简单的理解打擂法就是,循环访问数,然后把数组和一个初始值为0的全局变量比较大小,如果比这个变量大就取代他的位子。等遍历玩每一个数组元素之后,这个变量的值就是数组的最大值了。这个变量就好像擂台的金腰带。只有最强者才可以拿到金腰带。

  • 还有一个问题要解决,怎么获取根节点到每个叶子节点的长度呢?这只能使用二叉树的遍历,因为我们得一个一个节点的树,一顿操作下来,每个节点都被我们数了一遍。我们怎么数呢?就是访问一个节点就记一下。把上一次记的结果+1就是当前节点的长度数了。

  • 记哪里呢?当然是记数据结构了,树的属性就是用来记数据的。自然的,我把每个节点到都记录到根节点的长度即可,而且有趣的事,我们只需要将父节点的长度+1就是当前没节点的长度了。

  • 另外按照树的性质定义,如果该节点没有叶子节点的话,那么该节点就会叶子节点了。

  • 其次我们得知道递归遍历树是深度优先遍历算法的一种,所以很适合用来做深度计算。(广度优先算法一样可以解答这类题目。)

  • 接下来可以开始写代码了,先找出代码框架。

代码框架如下:

class Solution {

    TreeNode recursive(TreeNode node) {
        // 边界判断
        if(...) {
            return node;
        }
        
        // 访问节点,读取或者修改属性值。
        // 递归访问左右子树
        recursive(node.left);
        recursive(node.right);
        // 也可以在这里读取或者修改属性值,这种是回溯思路。
        // 返回值。
        return node;
    }
}
  • 如果先递归左右子树叫回溯法,因为编译成计算机指令程序的话,会先修改叶子的值再修改根的值。然后一步一步return。他是自下而上的update每个节点的属性。
  • 如果先访问修改节点信息,然后才递归左右子树,就是普通的非线性递归了。

(这里不要扯复杂先。专心学习框架。暴力解题。)

开始解题:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    
    public static int kingDepth = -1;
    
    public static void setNodeFatherDepth(TreeNode node, int fatherDepth) {
        if( node != null ) {
          node.val = fatherDepth;
        }
    }
     
    static TreeNode recursive(TreeNode node) {
        // 边界判断
        /* explain: 我们的任务是记录节点的长度,所以如果节点引用不存在也就没有记录的地方了,所以直接返回;*/
        if(node == null) {
            return node;
        }
        
        // 访问节点,读取或者修改属性值。
        /* explain: 记录node节点长度 = 父节点的单位长度 + 1, node.val表示父节点的单位长度*/
        int nodeDepth = node.val + 1;
       
        /* explain: 其次是要判断该节点是否为叶子节点,如果是和擂台变量kingDepth比较. 同时如果该节点已经是叶子节点了,也就没有进一步递归的必要了。直接return.*/
        boolean isLeaf = (node.left == null && node.right == null);
        if(isLeaf) {
            boolean isBetterKing = nodeDepth > Solution.kingDepth;
            Solution.kingDepth = isBetterKing?nodeDepth : Solution.kingDepth;
            return node;
        }
        
        /* explain: 然后如果有子节点的话,我们要还有同步一下子节点的“父节点长度值”val */
        Solution.setNodeFatherDepth(node.left, nodeDepth);
        Solution.setNodeFatherDepth(node.right, nodeDepth);
        // 递归访问左右子树
        recursive(node.left);
        recursive(node.right);
        // 也可以在这里读取或者修改属性值,这种是回溯思路。这里也是【后续遍历】的地方
        // 返回值。
        return node;
    }
    
    public int maxDepth(TreeNode root) {
        // 初始化根节点的父节点长度;
        Solution.setNodeFatherDepth(root, 0);
        kingDepth = 0;
        recursive(root);
        return kingDepth;
    }
}

总结:

该题目有趣,竟然用到了打擂法。

  • 这道题目是求深度,也就是求所有节点到根节点的最长路径的长度(经过的节点数)。
  • 所以可以用动态规划法,快速求解。动态规划题目具备3个要素:重叠子问题,最优子结构,动态转移方程。
  • 重叠子问题,就是子节点的深度求解和根节点的深度求解是一样的。(你可以说大部分“树”求最值的题目都存在重叠子问题)
  • 最优子结构,即要求解给定问题的答案有多个选择,而且存在最优解。
  • 剩下就是拼凑状态转移方程,我们需要确定3个概念,才能得出动态转移方程。
 - 状态是什么? (状态通常是题目给定的参数变量)
确定了状态后就可以构建dp表了。
这道题目要求我们求解的是二叉树的深度。
那么变量就是二叉树的节点,dp值就是深度。
所以dp[二叉树的节点n] = 第n个结构构成的树的深度。
也就是说二叉树的节点就是“状态”

- base case是什么?base case就是最简单的状态,且能够显而易见的得出深度。那么这个状态就是我们要找的【Base case】,base case 可以有多个,通常找出1~2个就可以,base case主要用于前期的递推。

- 定义选项以及择优策略。选项是由dp表的元素构成,定义选项相对灵活很多,没有标准答案。而择优策略则是确定一个规则,从多个选项中选择最优,并赋值给下一个dp[状态]。

  • 动态规划是自下而上的求解方式。

  • 本题目,可以确定存在重叠子问题。这个无疑。毕竟是二叉树求最优解。需要遍理所有节点,从而求深度。

  • 也存在最优子结构,因为从根节点到叶子节点的路径有多条,而且存在最长路径。

  • 状态就是dp[二叉树的节点n] = 第n个结构构成的树的深度。

  • base case 就是 dp[节点为Null] = 0(或者 dp[叶子节点] = 1);

  • 选项就是 选项1=( dp[左子节点] +1 )。选项2 = (dp[右子节点] + 1)

  • 择优策略就简单了,看哪个选项大就选哪个。

  • 状态转移方程最终就是长这个样子:


dp[root] = max( dp[左子节点] + 1 ,dp[右子节点] + 1) , if  节点== null, return 0;

```

代码如下:

```java


 int maxDepth( TreeNode node ) {
    if(node == null) {
        return 0;
    }
    int leftDepth = maxDepth(node.left);
    int rightDepth = maxDepth(node.right) ;
    return  Math.max(leftDepth + 1 , rightDepth + 1 );
 }

```

- 和上面的算法比起来,其实就是一个后续遍理的回溯的过程。
- 我们的第一种解法采用的是,前序遍理,然后再递推的过程累计节点长度。
- 而第二种揭发是采用后续遍理,在回溯的过程累计节点长度。
- 理论上我们并没有真正的用动态规划算法(我们用的是递归的回溯算法,因为我们并没有记录dp表。),但是思想上是动态规划的思想。
- 不管如何这道题目后续和前需遍理都可以求解。
- 前序遍理有利于我们对算法进行剪支,减少不必要的遍理(我们这里没有可以用剪支的场景)。
- 而后续遍理可以用回溯计算,减少函数局部变量的开销(函数栈的开销在递归算法里是无法避免的,而且如果参数和返回值不大的话也不值一提)。但是无法剪支。
- 而中序遍历普遍是二叉搜素树会用到,先遍历的子节点无法剪支,后遍历的节点可以剪支。你问我什么是剪支?剪支就是在递归函数调用前,加一个If判断,如果不满足条件就不递归了(不递归就等于被流程被我们if语句剪掉了)。


你可能感兴趣的:(用算法来学计算机)