Arts 第八十三周(5/17 ~ 5/23)

414 阅读6分钟

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

Algorithm

LC 387. First Unique Character in a String

题目解析

给定一个字符串,找到其中第一个出现的不重复的字符,返回这个字符所在的下标。这道题算是一道非常简单的题目,可以轻易写出下面的代码:

public int firstUniqChar(String s) {
    int[] hash = new int[26];
    int n = s.length();

    for (int i = 0; i < n; ++i) {
        hash[s.charAt(i) - 'a']++;
    }

    for (int i = 0; i < n; ++i) {
        if (hash[s.charAt(i) - 'a'] == 1) {
            return i;
        }
    }

    return -1;
}

不难看出上面的代码的时间复杂度是 O(n),n 是字符串的长度。这里我们遍历了两次输入字符串,但设想一下,如果这个输入字符串非常的长,比如说 n == 10^9,那么时间复杂度里面的常数项也会对程序的运行效率带来不小的影响,如何优化呢?更确切地说,如何只遍历一遍数组就可以得到答案?

这里强调一下 “遍历一遍数组” 并不等于 “用一个循环实现”。有一个双指针的解法确实是用一个循环实现,但是数组中每个元素还是会在某些情况下被访问两次。所以更严格的定义是,“数组中的每个元素仅访问一次”

这里有多种解法,比如最简单的就是在哈希中记录每个访问到的字符在数组中出现的位置,然后最后遍历一遍哈希,找到符合条件的最小位置即可。

另外一个我觉得挺有意思的一个解法是利用队列,队列 先进先出 的性质保证了数组元素的遍历顺序。最后只需要从队列中取出元素拿去和哈希检验即可。总体上来说这种做法的时间是最优的。


参考代码

public int firstUniqChar(String s) {
    int[] hash = new int[26];
    Queue<int[]> q = new LinkedList<>();
    int n = s.length();

    int index = 0;
    for (char c : s.toCharArray()) {
        if (hash[c - 'a']++ == 0) {
            q.add(new int[]{c - 'a', index});
        }
        index++;
    }

    while (!q.isEmpty()) {
        int[] i = q.remove();
        if (hash[i[0]] == 1) {
            return i[1];
        }
    }

    return -1;
}

Review

What Is It That Makes Your Code Smell?

文章主要是讲代码中的坏味道。更确切地说,文章想告诉我们这些坏味道是如何形成的,我们该如何避免。一开始听说 “坏味道” 这个词,说实话我并不是很理解。为什么要用这个词语来描述 “代码的不好之处” 呢?后来通过读一些文章理解了。

其实我们可以类比想象一下,就比如说你某天回到家,打开冰箱,扑面而来的是一股馊味,一股不好的味道,这里的原因就是你没有对冰箱里的食物进行很好的放置,比如容易腐烂的食物需要提前做一些处理,易坏的食物需要考虑冷冻保存,某些食物不能放在一起等等。代码也是一样,长时间的不管不顾,任意堆叠,代码也会有 “腐烂” 的时候,会散发出让人难以忍受的 “坏味道”,也就是难读、难懂,难以维护。让我们看看,想要维护好代码,有哪些值得注意的地方:

  • Shotgun Surgery

    枪弹外科手术,这个术语应该不常见。但在外科手术中,它是最大的一种过度杀伤性的定义。也就是说,为了达到一个看似简单的目的,你需要做很多看似没有关系的事情。

    就好比你想在某个类中做一个小的改动,但是因为依赖性,这个改动可能会需要你去到其它的类中做一些更大的改动。牵一发动全身,这样的代码难以维护和扩展。

    如何解决这样的问题呢?或者说,这样的问题是怎样造成的。问题主要来自重复的代码,当代码中表示相同逻辑的代码重复出现的时候就会增加维护的成本,主要是当我们想要对代码进行更改的时候,我们就有可能需要考虑对多个地方的代码进行更改。所以,解决方案就是尽量减少重复的代码,拥有相同逻辑的代码能合并的尽量合并。


  • Oddball Solution

    奇怪的解。如果你发现你的系统中在用不同的方法解决相同的问题,那么就有这样的坏味道。这会造成系统的逻辑不一致,而且也会导致很多重复性的代码。

    解决方案就是从多个解决方法中选出你觉得最好的一个,然后把其它的解决方法从代码中移除。在此之中,你也可以移除重复性的代码逻辑。


  • Overcomplicating the Obvious

    过度地把问题复杂化。比如说很多地方完全可以用很简单,很直接的方式实现,但是却用了很高级的设计模式。说的更直接一点就是,尝试着去解决一个不存在的问题。这就会让代码在某种程度上变得复杂。

    当我们尝试着去写代码解决未来可能会有的问题,也会导致这个问题。总之,不要过度地优化。


  • Too Many Parameters

    过多的参数。在 《Clean Code》 中也提到过,过多的函数参数对代码的理解和阅读百害无一利。因为参数越多,表明需要知道的细节也就会越多,越难理解,代码量也会变得更长。如果真的需要传入多个参数,尽量把这些东西抽象成一个结构体或者对象。


  • Long Methods

    过长的方法。一个过长的方法,比如大几百行,往往意味着方法里充斥着各种各样的业务逻辑,最主要的问题是因为这个方法做的事情很多,牵连很大,代码量只会只增不减,逻辑只会变的越来越复杂,越来越乱。

    解决方法就是根据 单一职责原则 进行代码的重构,把逻辑进行分层,然后每一层下的函数只负责一件事。


  • Inconsistent Naming

    命名的不一致。命名是计算机软件领域最难的事情之一,好的命名可以直接反应一个函数或者一个变量的职责和用途,甚至连看代码都省去了。但说实话,要做到这一点很难。但我们可以做的是,养成一些好的命名习惯,我们可以统一一些命名的惯例。比如,用 get 表示索取,用 delete 表示删除,用 async 表示异步处理等等。


  • Data Clumps

    数据块。当多个函数或者对象使用相同的参数进行传递,往往表明这些参数之间存在着相互的关联。我们可以把这些参数绑在一起来提高代码的组织性。把关联的参数绑在一起就会形成数据块,这样的数据块往往不会有问题,但它可能指代更深层次的问题。


  • Middle Man

    中间人。很多时候,在代码中存在某些中间函数,这些函数并不做任何的事情,仅仅是接受请求,然后把请求转发给另一个函数。有些时候我们可能需要这样的中间函数来实现逻辑分层,但是在大多数情况下直接请求会让逻辑变得清晰和简单。

上面说到的这些点不是全部,但是这些点在我看来都是很容易实现的,只需要在平时写代码多多注意,不要偷懒。


Tip

平时我们需要让程序在后台运行的时候往往都会想到 screen 或者是 pm2 这样的工具。但其实最简单的方式就是在命令的末尾加上 &,比如:

$> go run app.go &

这里完全不需要任何的其它的工具,比较方便快捷,适合用于一些小程序。当然,如果你还想要看 logs,只需要把文件输入的内容导入到某个日志文件中即可,比如:

$> go run app.go > goLog.log &

如果还有一些比较高级的需求,比如实时监控,自动重启,那么还是推荐 pm2 这样的工具,并且使用起来还是非常的方便的。


Share

工作的意义