Arts 第三十周(10/7 ~ 10/13)

108 阅读7分钟

ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。

Algorithm

LC 437. Path Sum III

题目解析

在一个二叉树中找到等于 sum 的路径的个数,路径必须是从上到下,也就是路径中的每条边的方向必须是从 parent 到 child,路径的起点和终点可以不固定。

对于一般的二叉树问题,我们习惯去用 “遍历树访问节点” 的思想去解题,每遍历到一个节点,我们就去做一些事情,往往每个节点只需要访问一遍即可,通常二叉树类问题的时间复杂度是 O(n),这里的 n 表示的是树节点的个数。但是用这种 “遍历树访问节点” 的思想来解这道题的话,你会发现比较困难,原因在于路径的起点和终点不固定,在每个树节点上并不能很好地处理当前的 sum 值,因此这里我们需要跳出传统二叉树题目的固定思维。

既然是上下两个点确定一条路径,那我们就可以去枚举所有的可能的情况。思路就是,我们首先遍历树去寻找路径的起始节点,这里树上每个节点都可以成为起始节点,确定起始节点后,再遍历起始节点的两个子树去找终止节点,你可以看到这其实是一个嵌套的树遍历,也可以把它想象成两层嵌套的 for 循环,那么这么看的话时间复杂度就是 O(n^2)。

上面的解法可以说是非常直观的,只是说和我们平时解二叉树题目的思考方向不太一样,但是上面的解法看上去就比较暴力,是否有更优的解法呢?优化的突破口可以是 “当前节点如何获取到之前遍历过的节点的 sum 信息”,我们来看看下面这个题目给我们的例子:

sum = 8

                       10(10)10
                            /  \
                   15(15,5)5   -3(...)
                          / \     \
               18(18,8,3)3   2(...)11(...)
                        / \    \
          21(21,11,6,3)3  -2(...)1(...)

这里我们来分析一下,从上到下遍历,一条从根到叶子节点的路径上(10 -> 5 -> 3 -> 3) 到底发生了什么事情,上图中括号外面的数是当前路径上遍历过的所有节点的和,也就是当前的路径总和,括号里面的是以当前节点结尾可能的 pathSum

  • 一开是在根节点,如果根节点同时作为起始节点和终止节点,那么 pathSum = 10
  • 遍历到左子树值为 5 的节点,如果这个节点同时作为起始节点和终止节点,那么 pathSum = 5,path 还有一种情况是 10 -> 5,那么 pathSum = 15
  • 继续遍历左子树,和上面的分析方式类似,pathSum 可能的值是 {18, 8, 3}
  • 继续遍历左子树,和上面的分析方式类似,pathSum 可能的值是 {21, 11, 6, 3}

由此可以发现一个现象,在同一条路径上,后面节点上的可能的 pathSum 值和前面节点上的路径总和存在依赖关系,比如对于最后一个叶子节点 3:

  • 21 = 21 - 0
  • 11 = 21 - 10
  • 6 = 21 - 15
  • 3 = 21 - 18

你可以看到这里的 10,15,18 都是这条路径上的之前遍历到的节点的路径总和,如果我们把上面等式左侧的数换成我们要找的答案,那么这里就是一个验证的过程,看右侧的计算是否可以得到我们想要的答案,这样我们只需要保存这些路径总和即可,这种方式其实有点类似于 Two Sum 的思想,但是有一点需要注意的是,这种分析只限定于一条路径上,去到另外一条路径时,我们需要把之前保存的东西清除,如何做到呢,利用回溯,这么下来,可以保证每个节点只访问一遍,时间就是 O(n)


参考代码(一)

O(n^2) 暴力解法

public int pathSum(TreeNode root, int sum) {
    if (root == null) {
        return 0;
    }
    
    return helper(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
}

private int helper(TreeNode root, int sum) {
    if (root == null) {
        return 0;
    }
    
    int count = 0;
    if (sum - root.val == 0) {
        count++;
    }
    
    count += helper(root.left, sum - root.val);
    count += helper(root.right, sum - root.val);
    
    return count;
}

参考代码(二)

O(n) TwoSum + Backtracking

public int pathSum(TreeNode root, int sum) {
    // 不同的路径可能会有相同的路径值,因此这里使用 Map
    Map<Integer, Integer> count = new HashMap<>();

    // 初始化,用于判断如果路径就是节点本身的情况
    count.put(0, 1);

    return helper(root, 0, sum, count);
}

private int helper(TreeNode root, int cur, int target, Map<Integer, Integer> count) {
    if (root == null) {
        return 0;
    }
    
    // 计算以当前节点结尾的路径总和
    cur += root.val;

    // 寻找以当前节点结尾的答案  
    // curSum - previousSum ?= target 
    // curSum - target ?= previousSum
    int result = count.getOrDefault(cur - target, 0);

    // 保存当前的路径总和
    count.put(cur, count.getOrDefault(cur, 0) + 1);

    result += helper(root.left, cur, target, count);
    result += helper(root.right, cur, target, count);
    
    // 回溯,离开当前节点,清理当前节点的数据
    count.put(cur, count.getOrDefault(cur, 0) - 1);

    return result;
}

Review

STOP LEARNING FRAMEWORKS

现在这个时代,新技术层出不穷,我们有时会感觉学不过来,不知所措。这篇短文给的一个观念就是 “我们应该把大部分时间花在学习一些不变的思想上面,而不是框架”,不知道你有没有发现很多框架都只是火一阵子,过一段时间就会被新的、更好用的框架所替代,没错,框架的生命周期比较短,我们如果把自己的大部分时间花在如何配置框架,去理解一个框架的细枝末节,那么这笔时间上面的投资其实是不划算的,这并不会对你的认知以及逻辑思考能力的提升有帮助。再反过来分析,看看我们程序员花在技术相关上的时间都有哪些,下面是我工作中的大部分技术相关的时间开销:

  • 根据业务逻辑设计代码的组织架构
  • 设计并实现函数
  • 编写测试代码
  • 设计数据库 Schema
  • 和其他人实现的模块建立连接
  • debug
  • 部署

这里面和框架相关的地方只有部署这个环节,但是部署这一个环节基本上也就是一两天就可以解决的事情,其实大部分时间还是花在程序和系统的设计与实现上,那这里面有多少是和某个特定框架有关的呢?不多,在设计上我们更在意的是一种思想,比如每个系统如何连接,是使用基本的 MVC 模式还是其他,我们并不会太多考虑框架当中的某个参数如何给的问题,当然,话说回来,框架的设计其实也是基于某种思想的,我们只有先理解了这个思想,才能更好地去理解对应的框架。

花多点时间去学习并理解那些基本的思想吧,不要再把自己的发展绑在某个特定的框架上,没有这些框架我们一样也能借着前人的经验设计出我们想要的系统。


Tip

学习了解了什么是面向切面编程AOP(Aspect Oriented Programming)

AOP: 一种通过横切关注点(Cross-cutting Concerns)来增强代码模块性的方法,它能够在不修改主体代码的情况下,为他添加额外的行为

另外学习到了一个和 AOP 设计目的类似的技术:控制反转,IoC(Inversion Of Control)

IoC: 主程序中不需要关注资源层的对象是怎么来的,怎么生成,直接调用即可,解耦了业务和资源访问这两部分的逻辑。

实现 IoC 的两个步骤

  • 依赖查找(Dependency Lookup,DL)
  • 依赖注入(Dependency Injection,DI)

IoC 的好处:

  • 业务代码不再包含资源访问的逻辑,资源访问和业务流程的代码解耦开了
  • 资源的统一配置管理

Share

这次讲讲面对问题,应该保持怎样的心态

应该如何看待 “问题”