继续做 DFS 相关的题目,本次做的是 695. Max Area of Island, 题目本身是 200. Number of Islands 的改编版,题目要求:
- 由 1 组成的为陆地,1 的个数代表陆地的区域大小
- 找出区域最大的陆地
解决题目的思路还是利用 DFS 进行深度遍历,同时每次遍历时都要计算当前陆地的区域数量。实现代码如下:
class Solution {
private int[][] grid;
private int row;
private int column;
private int maxCount = 0;
private int currentCount = 0;
public int maxAreaOfIsland(int[][] grid) {
this.grid = grid;
this.row = grid.length;
if (row == 0) {
return row;
}
this.column = grid[0].length;
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
if (grid[i][j] == 1) {
dfs(i, j);
maxCount = Math.max(currentCount, maxCount);
currentCount = 0;
} else {
continue;
}
}
}
return maxCount;
}
private void dfs(int i, int j) {
if (i >= row || j >= column | i < 0 | j < 0) {
return;
}
if (grid[i][j] == 1) {
currentCount += 1;
grid[i][j] = 0;
dfs(i + 1, j);
dfs(i - 1, j);
dfs(i, j + 1);
dfs(i, j - 1);
}
}
}
所有的节点都会被遍历到,时间复杂度应该 0(N),实际提交执行时间 2ms,beats 100%,一次提交通过,总算有点进步的感觉了= =
本周 Review 皓叔练级攻略中推荐的一篇文章Writing Code that Scales。作者分享了写出高扩展、高性能代码的一些工程原则和实践。
作者首先分享了在写代码时应该考虑的 5 个概念:
随着现在硬件设备的发展,CPU、内存、带宽资源都变得非常充足,使得我们在编程时往往忽略了软件的资源占用情况。但是通过降低软件对资源的开销,往往是解决可扩展性问题最有效的方式。如果软件可以占用更少的 CPU,传输更少的数据,占用更低的内存,那么应用的执行速度会更快,获得执行的机会也会更多。关于资源占用还是常说的四点:CPU、内存、IO、网络。IO 和网络越少越好,CPU、内存配置的越多越好,同时注意内存的及时回收。
这要求我们尽可能的将程序分解为不相干的模块,分别安排在单独的 CPU、存储设备甚至是单独的服务器上来实现并行处理。这就要求程序之间需要进行同步的地方尽可能的少,因为一旦需要同步可能就需要引入锁,而锁本质上会降低并发性能。如果所有的程序模块依赖同一个资源,每个程序访问资源都需要事先获取锁,那么程序到了这里其实就变成串行执行了。
在传统的单一数据库作为存储媒介的系统中,数据的伸缩只能是通过提高硬件配置的方式来进行。这样数据库很快就会成为整个系统的瓶颈,如果采用分散的多数据中心,可以将存储容量压力和读写压力分散到多个数据库中,同时也使的系统的伸缩性得到提升。
设计系统时需要明确哪些数据对实时性没有特别高的要求,对于这种数据的处理可以采用异步通信的方式,实现系统之间的解耦。 这种设计虽然在某些时候导致用户在更新数据后没法及时的看到更新的结果,但只要时间延迟是可以容忍的,那么完全可以采用最终一致性的方式,而非 ACID 强一致性设计。
通过直接提高硬件配置的方式是最有效的提升性能的手段,但是单机性能是无法做到无限提高的,因此随着系统负载的增加肯定需要做水平伸缩的工作。
对于水平伸缩的方式,要注意三点:
- 减少并发访问中的数据同步
- 线程数尽量不要超过 CPU 核数
- 通过线程池的方式降低线程创建回收的性能损耗
上面是作者提到的在做伸缩架构时需要考虑的五点方案,下面是一些帮助实现上述方案的 tip:
事先做压力测试
尽可能模拟最坏的情况,向系统不断加压,测试出系统可以承受的最大负载,在设计系统、编写代码时时刻明确谨记。皓叔有一篇文章专门讲了这个,强烈推荐:性能测试应该怎么做?。
缓存热点数据
对于某些访问非常频繁的数据,将他们存在缓存中以提高读取速度。另外像 Memcached 这样基于内存的分布式缓存集群优于基于主机的缓存组件。
网络传输压缩数据
网络带宽始终是有限的,因此可以通过压缩数据提高网络数据传输的效率,但其代价就是增加了收发两端的处理数据的操作,主要是 CPU 对数据加压、解压的操作。因此在实际操作中要测试权衡,达到最优解。
压缩磁盘存储的数据
虽然现在磁盘容量大大提高,但是 IO 操作仍然是整个系统中最慢的操作。压缩数据主要是为了提高 IO 的速度。
做好限流设计
原文中的描述比较复杂,我理解的是系统应该做好限流操作以使得系统始终保持在一个高可用、高性能的服务状态。比如一台机器最多可以同时处理 100 个请求,那么可能在 80 个并发时,其资源利用效率是最高的,如果加到 120,那么性能就可能出现下降,加到 200、300,系统可能就直接崩掉了。
围绕磁盘寻址做优化
IO 性能的来源往往是因为磁盘需要通过寻址来查找数据,我们可以围绕该操作来做优化,比如换机械硬盘为固态硬盘,换随机 IO 为顺序 IO。这里分享一个小知识点是 CPU 在读取数组时,不是按照索引只读一个值,而是按片读取,该索引相邻部分的数据也会读取到,下次读取时就不需要再次读磁盘了,这也是数组可以实现高效随机读取的原因之一。
只存需要的数据
在客户端服务端的交互中,尽量减少不必要的状态值的传递,有些数据可以通过 cookie 缓存在客户端。传递的数据过多会导致每个连接所消耗的内存、CPU 资源增加,造成性能瓶颈。
减少每个连接的内存占用
和上个 tip 类似,每个连接处理所占用的内存应该尽可能的少,这样我们就可以获得更高的并发性能。比如你有 8GB 内存,如果每个连接占用 100MB,那么最多只能有 80 的并发。如果采用一个具有 10 个线程每个线程占 100MB 内存空间的线程池,并且将连接的内存占用降低到 1MB,那么可以获得 7000+ 的并发量。
谨慎使用文本协议
通过文本比如 XML、JSON 等方式进行数据的传递会非常明显的增加服务器端的 CPU 负载,对于文本的解析都属于 CPU 密集型任务。如果可以的话尽量采用二进制的方式进行数据传递。
最近在学习极客时间专栏 《Linux 性能优化实战》,分享下内存篇的脑图笔记:
最近读到一个很有启发的观念:
现实生活中我们往往追求怎么变得更好,更成功,但是往往忽略了如何去规避失败。
这句话也可以拓展开来:
在想要变好之前,怎么做才能防止变得更坏。
在明确应该做什么之前,先明确应该不做什么。
个人觉得这句话对人生的意义比那些帮助你更好生活的建议更加的有意义。一般来说社会阶层的分布是符合正态分布的,极度富有成功、极度贫困失败的人只占极少数一部分,大部分芸芸众生其实都生活在一个差不多的水平线上,一个人想要提高自己的社会阶层可能具有较大的随机性,受到个人努力、机遇、环境甚至政策、家境等各方面因素的影响,因此在显著提高社会阶层的可能性具有很大不确定性的情况下,思考如何使自己的社会阶层不进一步的降低就显得非常有意义。一个是尽可能的提高上限,一个是提高下限。
具体到生活,比如理财,使我们更富有的方式有各种投资等,那么如何做使我们不至于陷入更加贫穷的地步呢?可以是减少消费来提高财富积累,可以是为自己买份保险保证意外发生时自己可以有一定的风险承担能力,不至于因病返贫,因意外返贫等。
在比如学习,在明确哪些可以提高学习效率之前,先明确哪些会影响你的学习效率,分心、被打断、刷朋友圈等,那么可以先着手将这些干扰因素降到最低,然后再去采用其他方法去改进学习效率。