Arts 第三十四周(11/4 ~ 11/10)

171 阅读7分钟

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

Algorithm

LC 84. Largest Rectangle in Histogram


题目解析

给一个条形图,让你找出这里面面积最大的矩形,条形图中每一个位置的高度不固定,但是宽度都是 1。因为矩形的方向,大小都不确定,直观去看的话思路并不明显,但是有一点很明确,一段区间形成的矩形总是和最短的高度有关,我们还是来看看题目给的例子:

[2,1,5,6,2,3]

我们假定矩形可以由一个区间 [start, end] 确定,
那么这个区间的矩形的高度其实是由这个区间的最小值决定

[0,1] 区间内的矩形的高度是 1,面积 1 * 2 = 2
[2,3] 区间内的矩形的高度是 5,面积 5 * 2 = 10
[0,5] 区间内的矩形的高度是 1,面积 1 * 6 = 6
[2,5] 区间内的矩形的高度是 2,面积 2 * 4 = 8
...

如果你明白了上面的例子,这道题目思路就有了,也就是 “找出数组的所有区间(子数组),区间中 最小元素的值 * 区间的长度 就是当前区间表示的矩形的面积,在所有区间中找最大面积的矩形即可”。这个思路非常的直接,这么样下来,找出数组的所有子数组,这个时间复杂度是 O(n^2),有没有办法优化呢?

上面的解法,你直观上看就是一个暴力的解法,因为我们不断地去找子数组,这里面其实有很多的重复计算,比如,还是刚刚那个例子:

[2,1,5,6,2,3]

[2,1,5,6,2] 我们遍历了一遍,得到矩阵的高度的最小值,然后求出面积
[1,2,6,2] 我们又接着重新遍历一遍,得到矩阵的高度的最小值,然后求出面积

求第二个区间的时候,我们完全没有借鉴第一个区间的答案,存在着重复计算

这个时候,我们可能需要换一个思路来看待这个问题,从上面的分析,我们已经得知,矩阵的高都来自于数组中元素的值,可以思考 “如果以当前位置的元素作为矩形的高,最多向左向右分别延伸多少个位置”,比如:

[2,1,5,6,2,3]

如果我们取数组中第 2 个元素,也就是 1,作为矩阵的高
它向左可以延伸到 2,向右可以延伸到 3,于是面积就是 1 * 6 = 6

如果我们取数组中第 5 个元素,也就是 2,作为矩阵的高
它向左可以延伸到 5,向右可以延伸到 3,于是面积就是 2 * 4 = 8

如果我们取数组中第 3 个元素,也就是 5,作为矩阵的高
它向左可以延伸到 5(它自己),向右可以延伸到 6,于是面积就是 5 * 2 = 10

...

好了,思路分析完了,现在如何实现这么一个思想呢?我们需要确定一个元素可以延伸到的左边界和右边界,这么说你可能不太好理解,换种说法,其实我们需要找 “左边第一个比当前元素小的元素所在的位置,右边第一个比当前元素小的元素所在位置”,还是来看看例子:

[2,1,5,6,2,3]

数组第 2 个元素 1,左右均没有比它小的元素,因此它所确定的区间就是整个数组

数组第 5 个元素 2,左边第一个比它小的元素是 1,右边第一个比它小的元素是 -1(表示没有)
因此它所确定的区间就是 [2, 5]

数组第 3 个元素 5,左边第一个比它小的元素是 1,右边第一个比它小的元素是 6
因此它所确定的区间就是 [2, 3]

...

在实现上面我们需要利用栈这个数据结构,栈里面存放的元素对应的值都是单调递增的,这样可以保证从左向右遍历数组,前一个入栈的元素是后一个入栈的元素的左边界,另外,如果下一个准备入栈的元素比栈顶元素的值小,说明栈顶元素的右边界也找到了,左右边界都找到了,栈顶元素出栈进行计算。这样下来,一个元素只会进栈一次,出栈一次,因此时间复杂度是 O(2 * n) 也就是 O(n)

单调栈这个数据结构应用还是比较广泛的,如果发现题目中需要针对数组中一个元素向左右两边延伸去确定区间,而且延伸的条件跟元素的值有关,那么就可以考虑使用单调栈。


参考代码(暴力解法)

public int largestRectangleArea(int[] heights) {
    if (heights == null || heights.length == 0) {
        return 0;
    }
    
    int n = heights.length;
    int result = 0;
    
    for (int i = 0; i < n; ++i) {
        int curMin = heights[i];
        
        for (int j = i; j < n; ++j) {
            curMin = Math.min(curMin, heights[j]);
            result = Math.max(result, (j - i + 1) * curMin);
        }
    }
    
    return result;
}

参考代码(单调栈)

public int largestRectangleArea(int[] heights) {
    if (heights == null || heights.length == 0) {
        return 0;
    }

    int n = heights.length;
    
    Stack<Integer> stack = new Stack<>();
    
    int result = 0;
    
    for (int i = 0; i <= n; ++i) {
        // curElement 表示当前元素的值,用 -1 表示数组的结束
        int curElement = (i == n) ? -1 : heights[i];
        
        // 元素的出栈操作,表明当前栈顶元素找到了右边界
        // 加上栈内存放的元素是单调递增的,因此左边界也找到了
        // while 里面针对栈顶元素这个高度的矩形计算面积即可
        while (!stack.isEmpty() && heights[stack.peek()] >= curElement) {
            int high = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            
            result = Math.max(result, high * width);
        }
        
        stack.push(i);
    }
    
    return result;
}

Review

What Is Clean Code?

《Clean Code》 被誉为 “软件工程的圣经”,这边文章从各个角度剖析了写出好代码的重要性,另外文章还摘录了书中的一些重要思想。

这里还是讲讲自己对 “写好代码” 这件事的认识吧,曾记得自己刚入职那会,对什么是好代码没有清晰的认识,简单认为高效的代码就是好代码,但是经历过了同事的抱怨、不断地重改、以及项目的延期,深深地意识到好的代码可以给别人,给自己省下巨大的时间。其实 “写好代码” 比起 “写出高效的程序” 要简单多了,因为写出高效的程序需要你了解很多高效的算法,了解一些系统的底层设计与实现,并且理解相对来说比较复杂的思想,这些需要你有一定的计算机功底,往往并不容易,但是 “写好代码” 其实是人人都可以做到的事情,首先你需要有这个意识,不断去发现自己写的东西里面的问题,然后把一些最佳实践当作自己的习惯去练习,去巩固和消化,避免踩自己之前踩过的坑,时常总结,时常更新,随着时间的推移,你的写代码的能力自然而然就上来了,这里面有两点很重要,一个是坚持,一个是好奇心,始终相信自己之前写的代码有改进和提高的空间,就像不断看昨天的自己,只要今天的自己比昨天的自己有提高,有收获,有进步,就没问题,慢一点没关系,到最后也能学懂,也能做得不错,怕就怕突然停止了,不再继续了。


Tip

这周学习了一个缓存的设计模式:Cache-Aside

这个模式下,客户端访问缓存的步骤如下,主要是考虑尽量避免数据不一致

  • 数据获取策略:
    • 应用先去查看缓存是否有需要的数据
    • 如果有,直接返回,没有,去到数据库 query
    • 上一步缓存中没有,应用将 query 的结果写入缓存
  • 数据更新策略:
    • 应用先更新数据库
    • 应用再令缓存失效
    • 如果先令缓存失效则会导致数据不一致的结果
    • 如果直接更新缓存也会倒是数据不一致
    • 这个策略中,如果更新数据库成功,但是令缓存失效失败会比较麻烦,但是这个几率比较小

Share

这周继续动态规划,从简单的开始

动态规划之矩阵类动规