创业公司应该选择无聊的技术

感想

昨天又重新读了一下 Boring Technology 和 Boring Company 两篇文章,得出对于创业公司技术选型的一些想法。

  1. 创业必须是老鸟,老鸟已经知道了足够的技术,有足够的技术可供选择,才能有全局思考选择最合适的少数几个技术。
  2. 什么是 Boring Technology?Boring 也不能无脑,Oracle 和 MySQL 都已经足够成熟且无聊了,但是 Oracle 显然不是未来的方向,所以我们肯定要选择 MySQL。
  3. 对于新手来说,显然是不适合创业的。别扯啥比尔盖茨和扎克博格的例子,人家高中水平比不少五年工作经验的人都高。新手还需要足够的时间了解足够的技术知识。就像文中说的一样,每个人的精力是有限的,创业不能把精力都放在技术选型上,更多地是实现商业逻辑。
  4. 作为一个技术人员,或者说技术公司,还是应该固定地花费时间去学习新技术或者更基础的底层技术,比如 20%时间、串讲或者黑客马拉松。否则的话,因为对于技术的追求被压抑了,可能反倒倾向于在生产环境中引入各种 fancy 的新技术而引起问题。
  5. 总体来说,还是哪句话:不要因为手里拿着锤子就看哪里都是钉子。但是工具箱里一定要有锤子,不然看到钉子了你就傻眼了。锤子应该放在工具箱里,需要的时候用一下,而不是一直攥在手里。
  6. 运维团队的一个核心 KPI 就是组织开发人员增加一些功能重复的组件,比如 Python vs Ruby, Redis vs Memcached, Influxdb vs Prometheus.
  7. build from what you really need

原文的一些摘抄

  • 很重要的一步是认识到注意力是非常宝贵的,人类对于细节的把控粒度是有限的。
  • 我的朋友 Andrew 每天穿着同一个牌子的黑衬衫。他认为如果他节省下选择衣服穿的脑力,就可以存起来用到别的事情上。
  • 我们应该选择能够覆盖我们问题的最小工具集,然后把工作搞定。
  • 增加一项技术是容易的,但是和它共存很难
  • 我可以随时 brew install 一个数据库,并且开始往里写数据,但是要在生产环境运行数据库就是另一个技能了。
  • 如果你给不同的团队做出局部选择的权力,那么在你是在全局性地伤害自己。
  • 当你使用多个相同功能的服务的时候,你不只要付出多一分的维护精力,还要失去大家使用同一个平台的好处。
  • 技术对你的公司有全局影响,不应该交给单独的工程师们自己决定。
  • 如果你觉得你用现有的工具无法实现某个新功能,那么一般是你想的不够用力
  • 你可以把你觉得很难实现的功能写下来,然后就就会发现并没有那么难,至少比维护一个新的工具要简单。
  • 当然你有可能得到相反的结论,使用一个新的工具也许是值得的。
  • 如果你正在添加一项冗余的技术,那么你应该尝试着替换掉旧的工具,而不是维护两套。也应该准备着如果新的工具不好用的话,要回滚。
  • 优先做能够让你集中精力到真正重要的事情上的事情。
  • 全局思考你要做什么,选择一组最小的工具能够解决你所有问题的。
  • 有意思的是你选择的任何工具可能都不是”正确的工具”. 但是他们组合起来依然可能是正确的选择。

熟练使用你选择的工具非常重要。你应该往马斯洛需求模型的上面爬,关心大局。你不应该每天都在讨论要使用那个数据库,而应该是跟高阶的业务问题。如果你真的真么做的话,问问自己是不是哪里搞错了。

我们生活在一个创建公司的美妙年代,有那么多直接可用的工具和服务可以让我们解决时间和金钱并提高生产力。从来没有像现在这样,一个小团队(甚至一个人)可以使用简单无聊的技术来做出一点对世界有用的工具来。

大多数时候,构建和交付的最大障碍来自想得过多。如果这样怎么办,那样怎么办。天啊,你根本不重要。每个人都很忙的。没人关心你和你创造的东西,知道你证明你值得别人的注意力。即使你把第一次产品发布搞砸了,几乎不会有人注意到。Think big, start small, act fast. 使用无聊的技术来做一些简单的东西(甚至很丑), 只要你解决了实际问题。

你的过度思考就是我的优势所在。

最后

Boring technology 的思路和 Facebook 工程师的文章 [4] 看似是冲突的。一个强调使用各种”无聊的”的技术,而另一个则是炫耀各种高级的基础设施。其实两个并不是矛盾的,反倒是统一的。无聊的技术才是经过大规模检验的基础设施,而其他高级设施要按照自己的思路,而不是看到乱七八糟的东西为了用就用。

Happiness comes from shipping stuff. 快乐源自交付。

参考

  1. Choose Boring Technology
  2. The Boring Technology Behind a One Person Company
  3. 引入开源库的取舍
  4. FB 海归国内高管:谈谈 infra、代码管理、微服务、测试系统,以及技术人员海归能做什么

疫苗概念股研究

前期买过未名医药,但是感觉和和科兴的股权有问题就没有拿着,错过了翻倍的机会。

山东药玻早就在关注,但是没有下手,现在收益率也没过少。没有买更加小盘的正川科技,错过了翻倍机会

现在开始炒作预充针技术,因为据观察现有的疫苗都是采用预充针技术,也就是跳过了玻璃瓶的需求。

吹管充一体化

  • 人福医药
  • 东富龙
  • 楚天科技

玻璃瓶

需要中硼硅还是低硼硅玻璃?反正是特殊玻璃

  • 正川科技
  • 山东药玻

预充针材料

就是直接把药罐在注射器里面,需要特殊塑料

  • 阿科力
  • 康德莱(注射器厂商)

冷链运输

疫苗运输需要特殊温度,要求还挺严格的。

  • 澳柯玛
  • 九州通
  • 英特医疗

参考资料

  1. https://xueqiu.com/3171998589/153753646
  2. https://finance.sina.cn/stock/relnews/dongmiqa/2020-07-09/detail-iircuyvk2933260.d.html?vt=4

刷 LeetCode 的经验

最近断断续续面试了三个月,也拿了一些大厂(美团百度阿里)的 offer, 分享下刷题经验

  • 一定要使用自己最擅长的语言,不要想其他的,不要引入额外的复杂度;
  • 复习基础算法,如排序,二分查找,树的遍历等,同时做一些基础题;
  • 简单的递归,比如树的一些题目,是最容易引导你进入刷题状态的;
  • 递归是几乎所有有技巧的题目的基础,栈是递归、树是递归、回溯也是递归、动归更是递归;
  • 除了递归之外,剩下的基本是考察基础操作的题目,比如螺旋打印矩阵等等,一定要注意细节;
  • 每道题 10 分钟完全没思路就看答案;
  • 如果连续有 5 道题目看答案了,去补习知识,不要再做了;
  • 代码超过 20 行了,一般情况下说明写的有问题,直接看答案(确实有个别非常繁琐的题目,面试一般不考);
  • 开始的时候按分类刷,不要按顺序刷;
  • 如果遇到完全没有思路的题目,不要悲伤,不要觉得自己是个傻瓜。秉承「闻过则喜」的态度,试想如果是面试时候恰好考到这道题目呢?
  • 刷过了过几天又忘了也没关系,根据艾宾浩斯记忆曲线,前几天是忘得最快的时候,多看几遍;
  • 当然,死记硬背是没用的,一定要理解题目;
  • 做题的时候身边要有白纸,随时在纸上画下示意图,不要空想;
  • 做了几道同类题之后,总结做题模板,比如说 backtracking 的做题模板;
  • 后期刷熟了,不要分类刷,随机刷;
  • 已有的题库就当做例题,反复看,彻底学透。每周参加周赛,做新题,作为模拟面试。

最后一点,刷 LeetCode 其实就像是大学里的期末考试,如果你两周复习时间可以搞定一门期末考试,那么两周时间也完全能搞定 LeetCode。要做到「战略上藐视,战术上重视」。

按照考察的方面,题目大概分成两类:

  1. 递归;
  2. 其他题目。

对于递归题目,拿到题目首先要思考:

  1. 怎么才能把题目的输入规模缩小,简化为规模更小的问题
  2. 另一方面基础情形是什么

然后再考虑选用哪种实现方式,是回溯还是动归。

特别注意的地方

  • 不要沉迷于研究语言特性。

    就像上面说的,使用自己最熟悉,日常使用最多的语言。不要觉得 C++ 或者 Java 或者 Whatever 刷题最爽,或者你看的答案采用了什么语言,抓住主要矛盾,你是来刷题的,不是学习新语言的。我之前犯得错误就是边学 Rust 边刷题。

  • 不要沉迷于数论

    LeetCode 上有一些涉及到数论的题目,但是代码面试不是 ACM, 也不是你的大学期末考试。最多熟悉一下排列组合算法、全排列和欧几里得算法就行了,不要沉迷于数论。

  • 要总结自己的特殊问题

    说了这么多,都是共性的问题,但是每个人都会有自己特殊的问题。比如说我的问题就在于不太敢用字典,总觉得会让人感觉投机取巧,我一个工作四年的人,肯定是知道字典的,但是做算法题的时候总在潜意识里避免使用。实际上 LeetCode 第一题就用到了字典。

面试刷题总结(五)- 贪心算法

天字第一条:永远不要首先尝试贪心算法,并尝试证明它是对的。而要使用动态规划推出一个算法,然后可以尝试简化到贪心算法。

尤其是在面试中,基本不会出现贪心算法,太 tricky 了。如果一个面试官问了一个题目,并且他只会用贪心算法解,那么这个面试官水平也一般,这样的公司不去也罢。

相比动态规划来说,贪心算法一般来说:

  1. 要求条件更严格,也就是贪心选择条件;
  2. 时间复杂度往往更低,能达到线性时间复杂度。

贪心选择性质:每一步都选择局部最优解,而局部最优解恰好是全局最优解。贪婪一般需要预排序,证明贪婪成立可以采用数学归纳法,或者证明不会有更好的方法

区间问题

合并、排序区间也是一个常见的问题。

例题

LeetCode 56

LeetCode 57 合并区间

这道题关键之处在于合并后,把剩余的区间直接加上来,这样就不用考虑不少特殊情况了。

class Solution:
    def insert(self, intervals, newInterval):
        ans = []
        start, end = newInterval
        remainder = 0
        for x, y in intervals:
            if start <= y:
                if end < x:
                    break  # 找到了结尾了
                start = min(start, x)
                end = max(end, y)
            else:
                ans.append([x, y])
            remainder += 1
        ans.append([start, end])
        ans += intervals[remainder:]
        return ans

会议室 II

LeetCode 矩形重叠问题

参考资料

  1. 面试不会考贪心算法

UnionFind 算法

unionfind 算法是用于计算图中的节点连通性的算法。其中连同的意思是满足『自反』『对称』『传递』三个意思的连通。

class UnionFind:
    def __init__(self, count):
        self.count = count
        self.parents = list(range(count))  # 初始化时 parent 指针指向自己

    def union(self, p, q):
        """把 p, q 两个节点连通起来"""
        p_root = self.find(p)
        q_root = self.find(q)
        if p_root == q_root:
            return
        self.parents[p_root] = q_root
        self.count -= 1

    def find(self, p):
        """找到 p 节点的根节点"""
        while self.parents[p] != p:
            p = self.parents[p]
        return p

    def is_connected(p, q):
        p_root = self.find(p)
        q_root = self.find(q)
        return p_root == q_root

在上面的算法中,我们发现 union 和 is_connected 主要依赖于 find 的时间复杂度。而在树极端不平衡的情况下,是可能退化到 O(n) 的,所以不优化的前提下,时间复杂度是 O(n)。

优化1 – 保持树平衡

class UnionFind:
    def __init__(self, count):
        self.count = count
        self.parents = list(range(count))  # 初始化时 parent 指针指向自己
        self.sizes = [1] * count  # 记录每棵树的大小

    def union(self, p, q):
        """把 p, q 两个节点连通起来"""
        p_root = self.find(p)
        q_root = self.find(q)
        if p_root == q_root:
            return
        if self.sizes(p_root) > self.sizes(q_root):
            self.parents[q_root] = p_root
            self.sizes[p_root] += self.sizes[q_root]
        else:
            self.parents[p_root] = q_root
            self.sizes[q_root] += self.sizes[p_root]
        self.count -= 1

经过这个优化之后,时间复杂度就降到 O(logN) 了。

优化2 – 路径压缩

在调用 find 函数的时候,把

class UnionFind:
    def find(self, p):
        """找到 p 节点的根节点"""
        while self.parents[p] != p:
            # 神奇的路径压缩
            self.parents[p] = self.parents[self.parents[p]]
            p = self.parents[p]
        return p

Go 语言实现

type UnionFind struct {
    count int,
    parents []int,
    sizes []int
}

func NewUnionFind(n int) (*UnionFind) {
    parents := make([]int, n)
    sizes := make([]int, n)
    for i := 0; i < n; i++ {
        parents[i] = i
        sizes[i] = 1
    }
    return &UnionFind{n, parents, sizes)
}

func (uf *UnionFind) Union (p, q int) {
    pRoot := uf.Find(p)
    qRoot := uf.Find(q)
    if pRoot == qRoot {
        return
    }
    if uf.sizes[pRoot] < uf.sizes[qRoot] {
        uf.parents[pRoot] = qRoot
        uf.sizes[qRoot] += uf.sizes[pRoot]
    } else {
        uf.parents[qRoot] = pRoot
        uf.sizes[pRoot] += uf.sizes[qRoot]
    }
    uf.count -= 1
}

func (uf *UnionFind) Find(p int) int {
    while uf.parents[p] != p {
        uf.parents[p] = uf.parents[uf.parents[p]]
        p = uf.parents[p]
    }
    return p
}

func (uf *UnionFind) IsConnected(p, q int) bool {
    pRoot := uf.Find(p)
    qRoot := uf.Find(q)
    return pRoot == qRoot
}

LeetCode 题目

LeetCode 200 岛屿的数量

这个题就很简单了,简直是 UnionFind 的直接应用。

  1. Union-Find 并查集算法详解

小技巧

时间复杂度需要优化

如果你的算法和预期的时间复杂度还差一些数量级,那么考虑下是不是某个操作可以转化成 hash 查表,这样这个操作就是 O(1) 了。

如何证明算法正确

一般使用归纳法。

面试刷题总结(六)-栈和队列

栈本身是一种特别简单的数据结构,这里就不多说了,主要记录一些以前不会的题目。

单调栈

单调栈就是满足单调性的栈。比如递增的栈,如果新的元素比栈顶的元素小的话,就要一直弹出,直到找到满足条件的栈顶,然后再入栈。

单调队列

单调队列使用两个队列组成。其中一个是普通的队列,另一个是维护大小顺序的队列。这里的队列不是严格的队列,而是可以从尾部弹出的双端队列。

from collections import deque

class MonoQueue:
    def __init__(self):
        self.q = deque()  # 实际储存数据
        self.m = deque()  # 维护单调关系,队首元素总是最大值

    def push(self, x):
        self.q.append(x)
        while len(self.m) > 0 and self.m[-1] < x:
            self.m.pop()
        self.m.append(x)

    def pop(self):
        x = self.q.popleft()
        if self.m[0] == x:
            self.m.popleft()
        return x

    def __len__(self):
        return len(self.q)

    def top(self):
        return self.m[0]

例题

单调队列的题目

  • LC84. Largest Rectangle in Histogram
  • LC239. Sliding Window Maximum
  • LC739. Daily Temperatures
  • LC862. Shortest Subarray with Sum at Least K
  • LC901. Online Stock Span
  • LC907. Sum of Subarray Minimums

POJ3250 Bad Hair Day

有 N 头牛从左到右排成一排,每头牛有一个高度 h[i],设左数第 i 头牛与「它右边第一头高度 >= h[i]」的牛之间有 c[i] 头牛,试求 sum(c[i])。

这道题使用单调栈做。

def sum_distance(H):
    ans = 0
    stack = []
    for h in H:
        # 单调递减的栈
        while stack and stack[-1] < h:
            stack.pop()
        ans += len(stack)
        stack.append(h)
    return ans

参考资料

  1. https://oi-wiki.org/ds/monotonous-stack/
  2. https://www.hankcs.com/program/algorithm/poj-3250-bad-hair-day.html
  3. https://oi-wiki.org/ds/monotonous-queue/
  4. https://oi.men.ci/monotone-queue-notes/

前缀和

前缀和这个概念可以说是「会者不难,难者不会」,概念本身是很简单的,但是如果不知道的话,好多题就两眼一抹黑了。前缀和即为数组从 0 到当前位置的和,类似的概念还有后缀和,前缀积,后缀积等。

presum[0] = A[0]
presum[i] = presum[i-1] + A[i]

例题

求数组中绝对值最小的子数组

利用前缀和的性质:A[i] + A[i+1] + ... + A[j] = presum[j] - presum[i-1] 前缀和排序,取差值最小值。

有一道题,求两个时间之间的差值,其实是类似的。当差值直接累加计算不好求的时候可以转换为前缀和的差。

把一个数组从中间 p 位置分开,使得 a[0]+…+a[p-1] 与 a[p]+a[p+1]+…+a[n-1] 差值最小?

也就是使得:presum - (total - presum) = 2 * presum - total 最小。

参考资料

  1. https://www.cnblogs.com/AndyJee/p/4474073.html

面试刷题总结(十)- 指针类题目与窗口

链表的指针题目

一般使用 dummy head 的话,循环条件是 while p.next, 而不使用的话,则是 while p.

递归反转列表

int reverseList(ListNode node) {
    if (node == null || node.next == null) {
        return node;
    }
    ListNode head = reverseList(node.next);
    node.next.next = node;
    node.next = null;
    return head;
}

都比较简单,主要是细节的使用。

  • LeetCode 24
  • LeetCode 25
  • LeetCode 206

参考资料

  1. https://mp.weixin.qq.com/s/YFe9Z35CE4QWPDmPNitd4w