关于 Hawstein

独立创造者。前豌豆荚后端工程师、GrowingIO 后端技术负责人, Scala 选手,Akka 贡献者,Reactive Design Pattern 译者。

RSS 地址: http://hawstein.com/feed.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

Hawstein RSS 预览

一个独立创造者的五年

2023-07-12 08:00:00

时间过得真快啊,游离在这个巨大系统之外,已经 5 年了。

和以往任何一篇文章一样,写作时机并没有发生在任何一个里程碑的点上(里程碑发生时缺少写作冲动)。如果非要为这篇文章安排一个写作契机,那就是 solo 五周年了,需要糊一篇文章表示表示。

注意:本文结构松散,没有中心思想,想到哪写到哪,记得清我就多写点,记不清我就一笔带过。

考虑到有人会来杠「独立创造者」与 Indie Hacker 这个翻译问题,无奈的我只好在这做个说明。在这里,我认为「独立创造者」是 Indie Hacker 这个专有名词的一个合适意译或近义词,在这个圈子的固定语境中,比直译「独立黑客」要好。所以我就这么用了。关于这个问题,我在几年前讨论过:关于新系列的命名考虑

分岔路的起点

2018 年 6 月 11 日,是我从公司离职成为独立创造者(Indie Hacker)的日子。回首看这个决定,可以借用乔布斯在斯坦福大学演讲中的一句话来概括它:

It was pretty scary at the time, but looking back it was one of the best decisions I ever made.

离职后,我开始做 AlgoCasts,一个算法教学视频网站。我们姑且把这个时期叫做 A 时期。在 A 时期,我每天的工作就是研究算法题,写视频脚本,录音频,录视频,剪辑视频,发布视频。这项模式固定的工作,我做了 2 年,从北京的各个咖啡厅做到瑞士的苏黎世联邦理工学院。A 时期的我写过一篇阶段性总结的文章,我不想把这部分内容又重复一遍,感兴趣的话可以去读一读那篇文章:不上班的 613 天

A 时期的我,一心在做内容,也可以叫 info product。这是当时的我能找到的最好切入点,也为我赚到离职后的第一桶金。

内容产品的问题

AlgoCasts 是一个面向国内用户、一次性收费的内容产品。这里的 3 个关键词带出 3 个问题:

当然,并不是说所有的面向国内用户、一次性收费的内容产品,就无法做出好营收。比如对于那些全平台有几百万粉丝或几千万粉丝的网红或大 V,写本书或是出个课程卖 99 元,一把梭可能就是几百万或几千万入账。

可惜我并不是这样的例子。我做的是独立产品,走的是小众路线,所以上面说的问题,是切切实实的问题。

而且,面向国内用户做内容产品,还有一个非常严重的问题:盗版。即使是像 AlgoCasts 如此小众的产品,我知道盗版过的就有 5 家了。当知道自己耗费心血做出来的内容,被人盗版然后贱卖,我的反应就是以后不会再做面向国内用户的产品了。

SaaS 之念念不忘

AlgoCasts 是我站在 2018 年 6 月 11 日这个时间点上,能为自己找到的最好切入点。但受到国外 Indie Hacker 社区影响,我其实一直都想做 SaaS (Software as a service)。加之上面提到的做面向国内用户、一次性收费内容产品的问题,我就越发渴望做一款面向海外用户、订阅制收费的 SaaS 产品。

这种渴望之强烈,以致于在我还没有任何产品想法的时候,就已经到处在和别人说我要出海做订阅制 SaaS 了。

A 时期,做算法教学视频之余,我花了很多时间泡在 indiehackers.com 这个网站上。这个网站上有非常多成功的独立创造者故事。我每天看着各式各样的独立创造者,做着五花八门的产品,想象着有一天自己也能做出一款服务于世界各国用户的 SaaS 产品。

在 indiehackers.com 上如果看到让我感兴趣的产品,我一般还会去 Twitter 上关注产品的创造者,去阅读他的推文和博客,全方位去了解这个产品及创造者背后的故事。有少数十分有意思的创造者,我会把他写过的所有博文都读了,上过的所有播客都听了,然后把他放到我的一个私人的 Twitter List 里,关注他每一天的动态。

就这么日复一日地看(帖子/采访/推文/博客)和听(播客),对国外的独立创造者圈,我几乎达到一种如数家珍的地步,仿佛自己也已经是其中的一员。

美国银行账户

2020 年春节的时候,我陪着老婆去美国出差。确切地说,是她去出差,我去当司机。每天把她送去公司后,我就开始做自己的事情。

忘了是哪天,工作之后我开始漫无目的地刷 YouTube,然后意外看到 @luoleiorg 做的美国银行开户攻略视频。视频做得很好,条理清晰。看完后,我想来都来了,要不去开个美国银行账户,方便以后给车加油。然后就走进酒店旁边的一家美国银行(Bank of America)支行,表示要开个账户(嗯,就是这么巧,我住的酒店旁边就是一家美国银行)。

接待我的是一个印度经理,一番交流得知我的基本情况后,印度经理表示现在不能给我开户。他说可以让我在美国的朋友给我开一个邀请函,拿着那个邀请函就可以来找他开户了。我心里一万头羊驼奔过,然后微笑着说了声谢谢,走出银行。

印度经理的拒绝让我更加想把这个美国银行账户办下来(越挫越勇说的就是我)。于是,我在 Google Map 上找出附近的美国银行支行,一脚油门开到最近的那一家。这次接待我的是一个白人小哥,一番愉快地交流后,不仅开了户,办了借记卡。小哥还问我要不要顺便办张信用卡,我拒绝了。毕竟给车加油,一张卡就够了。(此处还要感谢绍波帮我接收银行卡)

当时去办这张美国银行卡,纯粹就是想在美国给车加油时能方便一些。驱动力完全源于一个社恐不想在给车加油时和陌生人有任何交互的心理。

万万没想到,当时意外开的美国银行账户,却成为我后来做海外 SaaS,办离岸公司很重要的一步。当时走进一家 BOA 支行办卡时,我绝对想不到它未来会发挥的作用。但现在回过头去看,却非常的清晰。

You can’t connect the dots looking forward; you can only connect them looking backward.

SaaS 之必有回响

从美国出差归来,回到瑞士,短短一个月的时间,新冠就开始肆虐。我记得是 2020 年 2 月底,谷歌苏黎世宣布开始居家办公。我也告别在苏黎世联邦理工学院精心挑选出来的办公点(光线好,网络信号强),然后开始漫长的居家办公时期。

在居家办公时期,我每天主要还是做两件事:做视频和看产品(及其创造者的故事)。在这期间,我看到一个有意思的独立创造者,他做的是电商平台上的效率工具。产品本身其实中规中矩,但这个独立创造者的故事却很有意思。我阅读了他博客上大部分文章,很受启发。

当时我注意到,他做的工具只是针对北美市场的一个电商平台。于是我就想,欧洲市场是否也有电商平台需要类似的工具。经过一番调研,果真如我猜想的一样,欧洲这边的电商平台同样有类似的需求,而卖家们对工具的渴求散落在互联网各个角落。

完成调研后,我把调研结果包括当时已有的解决方案归档到我的笔记中,和所有其他产品想法一样,先让它沉淀一段时间。

这一次,我感觉到了些许微妙的不同。

不像以往的产品想法,沉淀着沉淀着就被遗忘。这一次的产品想法,随着日子的流逝而愈发强烈地侵占大脑。几个月后,我决定挥棒了(借用巴菲特的比喻,每个出现的产品想法就像一个抛向我的棒球,我可以静静地看着它们飞过,什么也不做。一旦发现一个绝佳好球,再全力挥棒。

一开始我还「理性」地分配了 60% 的时间做视频,40% 的时间做新产品。可是我很快发现,我做视频的时候,心里想的也全是新产品。后来我干脆也不装了,暂停视频的制作,所有时间全都投入到新产品的开发中。

三周后,新产品上线,包含 20 几个功能,宣发视频,使用指南,FAQ 一应俱全。

上线第一天,开始有用户。第一个试用期结束后,开始有付费用户。

我的第一款面向海外用户、订阅制收费的 SaaS 产品就这样诞生了。

而我的念念不忘,也有了第一个回响。

Stripe

三周的时间,撸了一个完整的 SaaS 产品。麻雀虽小,五脏俱全。后端服务的开发对我来说是最简单的,手握 Scala 刷刷两下就搞定。前端部分边学边做,感谢 Evan 的 Vue.js,让这部分工作也变得简单愉快。整个开发环节里,稍微比较复杂的就是支付服务的接入。毕竟是面向海外用户的产品,没法简单地放个微信/支付宝二维码就开始收款。

海外支付服务提供商有不少,在经过一番详尽的调研后,我选了 Stripe。这时候我那个为了加油而开的美国银行账户就派上用场了。由于 Stripe 不支持中国大陆,我只能在它支持的国家列表里选一个来注册。考虑到我已经有一个美国银行账户,于是决定注册 Stripe 美国账号。

注册 Stripe 美国个人账号,除了需要一个美国银行账户,还需要 SSN (Social Security Number) 或 EIN (Employer Identification Number)。SSN 我自然是没有的,但 EIN 则每个人都可以申请。更妙的是,你可以在 Fiverr 花点钱让别人帮你快速申请下来。有多快呢?我的一天就办下来了。

使用美国银行账户加上个人 EIN 顺利申请下来 Stripe 账号后,我就开始把 Stripe 集成到自己的产品中。这一块的开发者体验,也是好得不要不要的,无论是文档还是 API 设计,都相当棒!

随着产品营收的增加,Stripe 很快就发来邮件要求提供美国政府签发的文件,以进一步验证账户所有人的名字以及地址。我把办理 EIN 时 IRS 给的 147C 信件提交上去就可以了。

接下来,产品的 MRR (Monthly Recurring Revenue) 不断增长。到 2020 年 12 月,MRR 超过我上班时的工资并且增长趋势不停,我知道是时候注册一家公司了。

用了几年 Stripe,个人觉得,如果你要出海做 SaaS,支付服务商这一块,闭眼选 Stripe 就对了。当然,尽量别做高风险的业务,毕竟和钱直接相关,风控很严,Stripe 账号被封也是经常有的事。

美国公司

离岸公司有一些常见注册地,简单思索后,我决定注册一家美国公司。第一个原因是,我使用的是 Stripe 美国账号。注册美国公司后,我只需要把 Stripe 账号中的税务信息、商业信息和银行账户修改成公司的即可,无需迁移客户的订阅,非常方便。第二个原因是,注册美国公司很简单,全程可在网上进行。第三个原因,在互联网和科技行业,如果出了什么新产品或新服务,美国几乎总是第一个可以体验到的国家。第四个原因,美国公司可以做到 0 交税(在美国只报税不交税)。

注册美国公司我参考的是以下这篇文章,至于为什么没使用 Stripe Atlas 而是用了 Firstbase,文章中也有写。

https://www.indiehackers.com/post/setting-up-an-us-delaware-llc-and-bank-account-fully-remotely-as-a-non-us-citizen-resident-07a3191751

我的美国公司运营至今,每年除了报税季整理财务数据忙一下,几乎就没有其他维护负担了,非常轻量。

随手附上我在推上写的关于美国公司的报税小知识,说不定你想知道。链接:

https://twitter.com/Hawstein/status/1714157295419179120

微型公司与超级个体

公司会越来越小,个体会越来越强。这是我坚信的一种趋势。

随着科技的发展,各种基建越来越发达,越来越模块化,做事变得越来越容易。

这一点在互联网行业尤其明显。如果有一天你突然想做一个 SaaS 产品,你会发现这个过程中你需要的大部分东西都有相应的公司在提供垂直的细粒度的服务,而你基本上就只需要关注你的核心业务,其他全部可以使用第三方服务。这样一来,就极大地减少了你的工作量。

这种服务越来越垂直细化的局面极大地放大了个体的能力。在今天,你可以一个人开一家公司,不组建团队,不招聘员工,不需要办公室,做出一个完整度非常高的产品,去解决一个非常具体的问题。并且在这些第三方服务的帮助下,产品/设计/开发/市场/运营/销售各个环节一个都不落下。年利润可以做到百万甚至千万级别。这放在以前是根本不可能的,现如今不仅可能,而且成功案例越来越多。

微型公司的极致就是「一人公司」,一个人就是一家公司,个体就像传统的手艺人一样,全面参与到产品的各个环节。这正是我目前在践行的,并乐在其中。

雇人与否

产品的 MRR 稳健增长,我开始考虑是否需要雇个人来帮我处理客服相关的工作。

经过一番调研,我发现出海 SaaS 如果想雇佣非技术人员(比如客服),那么菲律宾会是一个非常好的选择。主要有两个原因:1. 人力成本低。2. 英语好。更棒的是,还有专门的公司帮你从菲律宾招到满足你要求的合同工,比如我收藏的 Shepherd。

研已经调好了,服务也选好了。但我最终没有走出「雇人」这一步。网上有张图可以很好地描述我在这件事情上的心理状态:

现阶段的我喜欢一个人自由自在做产品的感觉,我想做就做想歇就歇,但凡这个系统里多一个人,我的自由度就会被削弱一些。另外,我是一个社恐,但凡能不和人打交道就完成一件事,我就会选择不和人打交道。

以上是现阶段的我不雇人的「两个但凡」。虽然雇人的想法时不时还是会冒出来,但一想到这「两个但凡」,也就作罢了。

技术之外

Naval 说过

Learn to sell. Learn to build. If you can do both, you will be unstoppable.

我观察到不少优秀的技术人,尝试转型做独立创造者,最后不了了之,往往就是因为只会做(Build)不会卖(Sell)。

技术人嘛,做个 App 做个网站是不难的。但是要让别人花钱来买你的产品就没那么容易了。为了把自己做的产品卖出去,我做过不少事情。这些事并不是什么秘诀,网上一搜有大把的教程,甚至很多是你凭常识和直觉就知道可以这么做的。但根据我的观察,很多人连尝试一下都不愿意,或者才浅浅地试了一下发现不 work,就不愿意再做了。

下面是我为了卖产品做过的一些事情。

让朋友帮忙写好评。新产品上线,网络上关于它的讨论肯定是 0。这时候如果一个新用户到来,他能看到关于这个产品的一些好的评价,那么他安装使用你的产品的可能性是不是就增加了一点?另外,好评其实都是我精心写好的,朋友们只需要用着自己的账号粘贴上去就行。麻烦朋友好评,但不要麻烦他们费脑去帮你想那段话,这是对他们时间的尊重。

让用户写好评。一旦一个用户愿意付费,或是表现出对产品的喜爱,或是我及时完成了一次客户支持,我就会趁热打铁,让他帮忙去打个五星好评。这时候的用户一般都乐于完成这个举手之劳。

给潜在客户 DM 和邮件。你应该要知道你的潜在客户会在互联网上什么地方聚集或出现,TikTok? Twitter? Instagram? Reddit? LinkedIn? Facebook? Quora? 找到他们,如果能直接发私信,就发私信;或者能找着邮箱,就发邮件。精心准备好用于推销你产品的那条私信或邮件,并做好准备它们石沉大海,甚至收到嘲讽或劈头盖脸的批评。

在互联网上留下你的产品名字及官网链接。到 YouTube 上找到相关的视频,到 Reddit 上找到相关的帖子,到 Quora 上找到相关的问题,到 Twitter 上找到相关的讨论,到 Facebook 上找到相关的群组,留下评论和回答。尽量给出有用的信息,最后再带一嘴自己的产品。要卖,但不要那么明显。

内容营销一:写文章。写一篇图文并茂的长文,并把产品的关键词合理地放到文章当中。尽量使用短句,这样阅读起来更轻松;尽量多地使用图片,使文章看起来更丰富;行间距和段间距调大一些,给阅读留有呼吸距离。如果这些建议看起来又抽象又枯燥,那直接对照下面这篇文章,模仿这位著名光头的写作手法即可:Content Marketing - The Definitive Guide。长远来看,内容营销可以为产品带来免费的高质量的自然流量,值得投入时间。

内容营销二:做视频。和写文章类似,只不过换成了视频形式。

付费广告。这是短期内最简单有效的获客方式。我尝试过以下平台的付费广告:Google, Facebook, Instagram, Reddit,几个试验下来,最终发现对于我的产品,还是 Google Ads 最有效。在那之后,我就长期让 Google Ads 跑着,并切身地体会到 Google 赚钱有多么容易。

Affiliate Program。有偿地让用户帮你推荐,属于一种双赢方法。对于我的产品,如果一个用户推荐了一个订阅用户,那么我会把这个订阅收入的 25% 给推荐者。只要订阅者不取消订阅,推荐者就可以一直收到那 25% 的订阅费。

口碑传播。除了内容营销,口碑传播也属于卖产品的终极大杀器。我的产品有不少付费用户就是由已有的付费用户推荐而来。要让用户自发地去传播你的产品,首先你得有一个好产品,然后你要有一个好的客服。如果你的产品是惊为天人的好,那么 0 客服也能让千万人口碑传播,比如 ChatGPT。但大部分的产品都只能算是正常的好,这时候客服做得怎么样,就很大程度地影响了客户对你的产品的态度。对于我的产品,我估计大部分客户的态度都是:产品如预期般工作,但客服却是顶级的好。

另外还有一些小事不值一提,这里就不写了。比如工作之余,我会在 Instagram 上关注潜在客户,然后给他们的帖子点赞。这件事其实可以做个工具自动化来做,但由于我把它视为一种放松活动,所以一直都是手动进行。

产品想法的涌现

刚开始的时候,我不知道做什么产品。而一旦做出一个产品并成功运转起来,产品想法就不断涌现。

产品想法的第一个来源是已有产品的客户。我一直和客户保持良好的沟通,无论是问产品问题,还是报 bug,还是提建议,我都会很及时地回复。产品初期,我会在即时聊天中立即回复或者在收到邮件的 1 小时内回复。后期我会在收到邮件的 24 小时内回复。这样一来,这些客户就乐于把他们的想法告诉我。于是,我会在收件箱里收到客户请求做某些新产品的邮件。这些都是来自一线真实客户的需求,他们会清晰地描述自己遇到的问题,希望我能做个产品来帮助他们。这类需求邮件我收到不少,我会把它们都先记录到笔记中,也许未来某一天就会把它们做成产品。

产品想法的第二个来源是我自己做产品的过程中遇到的问题。一旦把围绕一个产品相关的各个环节(产品/设计/开发/市场/运营/销售)都跑一遍后,就会发现,有不少现有解决方案都不够好,而这些不够好的解决方案,就是新产品机会。

到了一个阶段,就会发现自己已经积累了不少产品想法。而且由于这些产品想法都是来自一线真实用户,它们往往是真实有效的需求,而不是自己 YY 出来的伪需求。这时候,我发现自己就到了另一个阶段,可以用 Derek Sivers 的一本书来描述:Hell Yeah or No

即,要么一个产品想法引起了我极大的兴趣,否则就不要去做它。这样有助于在很多的想法中,筛选出那个你特别想做的产品。

到了 2021 年 4 月,已有产品已经不太需要什么投入(功能,onboarding,文档都已经比较完善),于是我从那些记录的想法中,挑了一个挑战大复杂度高的,开始打造新产品。

把客户当作朋友

在做产品的时候,我有好几次遇到这样的情况:一个客户希望我开发一个功能来帮助他解决一个具体的问题。我分析后发现,如果要完美解决这个问题,需要考虑非常复杂的情况,开发时间会很长。但如果只是要解决这个客户的特定问题,就会简单很多。

以前的我一定是会选择花更长的时间去开发那个完美解决方案。但某次我突然想到以前帮助朋友或同事解决问题的场景。一个具体的朋友,遇到一个特定的问题,希望我写个程序帮他解决。这时候,我会花几天时间去开发一个软件帮他解决一个大类的问题吗?当然不会!对于我这个朋友来说,他遇到的是一个具体的特定的问题,而不是这个问题所属的泛化后的一类问题。他现在最关心的是怎么快速地解决眼前的问题,其他的如软件 UI,实现方案是否优美,他根本不会在意。想清楚这一点,我的最优做法就应该是花几分钟时间糊一个脚本,快速帮他把问题解决。

当然,做商业产品倒也不能这么潦草。只不过当我问自己,如果这个客户是我的朋友,那么我会让他等 3 周再拿到解决方案吗?还是今天就帮他把问题解决了,让他可以开心地回家吃个晚饭?

现在的我选后者。毕竟开心地吃晚饭是件很重要的事情。

具体实现上倒也不一定是丑陋的(不要一上来就想着全是写死的代码)。我的做法是把「问题域」剪枝到一个足够小的空间,既解决了当前这个客户的特定问题,又留下了一定的扩展性。这种方法尤其适合细分领域的小众产品。因为这类产品的总用户数并不多,可能也就几千个或几万个。这时候我们花费大力气开发一套完美方案就不是很必要。一来是开发时间长,二来可能维护成本也会高很多。

这些年我用这个方法做了一些产品功能,不仅客户开心,我也开心。而且你猜怎么着?我留下的那些扩展性,一个也没用上!

生意即艺术

很早我就有这种感觉,即做生意(商业)是一门艺术。在看了 Derek Sivers 的两本书后:Anything You WantYour Music and People,我更加确信这一点。

做生意和做艺术一样,你可以在各个环节发挥自己的创造力。你可以借鉴、融合别人的创意,也可以打破常规,尝试一些别人从来没试过的事情。

你可以在开发产品时,发挥创造力,打造一个独特有个性的产品;你可以在营销产品时,发挥创造力,比如写一封有趣的营销邮件(而不是套用死气沉沉的模板);你可以策划一场怪异的活动;也可以给客户送去一份意料之外的礼物。你可以和客户聊天聊成朋友,然后让他帮你推广产品;也可以给客户发 10 刀现金,让他给你一个好评。

做生意过程中的每一个环节都像一张白纸,没有任何金科玉律规定你该怎么做,你可以按你喜欢的方式去书写它。这与创造一部小说,一幅画作,一个雕塑,一首诗歌,并无本质区别。

当然,前提是你得喜欢做生意,正如你得喜欢写作,绘画,雕塑或诗歌,否则这项活动对你来说就是枯燥无味且煎熬的。

最好的时代

这是最好的时代,还是最坏的时代,取决于我们从哪个维度哪个切面去看这个时代。

就全球化 SaaS 及独立创造者而言,我觉得这是最好的时代。借助那一行行运行在全球各地机器上的代码,个体仿佛获得了阿基米德口中的杠杆,撬动了从未敢想之巨物。

代码永不眠。

我时常心怀感恩,能生在这样的时代。可以按自己喜欢的方式去创造并以此为生,我做的产品可以轻松到达世界任何一个接入了互联网的角落。而素未谋面的陌生人,用我的产品改善了他们的生活及工作质量,并回馈以金钱和感谢。其中一些客户,还和我成了朋友。

身处这样的时代,我尽量让自己不要浪费时代提供的丰富资源和机会,尝试去做点什么,去创造点什么,通过创造出的产品/作品,与这个世界产生联结,并最终与有趣的人和事相遇。

倘能做到以上这一点,大概连王小波先生都会认为我的一生是成功的。

交个朋友

王小波说,「我活在世上,无非想要明白些道理,遇见些有趣的事。倘能如我所愿,我的一生就算成功」。在这里,我想把「有趣的事」改成「有趣的人」,因为我觉得人比事有趣,而有趣的事大概率也是有趣之人所为。

当我耗费力气写这篇文章时,Roy 老师问我是不是准备卖课了?哈哈哈,据我观察,卖课确实是很多独立创造者的最终归宿,无论国内外。但我暂时没有这个想法,如果非要说有什么私心的话,那就是把自己扔出去,看看宇宙有什么回响。

Derek Sivers 在自己的网站首页留有这么一句话,

I love meeting new people, and I reply to every email, so say hello.

我很是喜欢他的做法。

那么在文章结束前,我也效仿一下吧(拒绝 IM,让我们用邮件连接

我喜欢和有趣的人交朋友,邮箱 [email protected],来 say hello 吧!

更新一:在收到几十封邮件后,我觉得有必要在这里加一条更新。如果你最终决定给我发邮件,请不要真的只写一个 Hello 就发过来=。=多少写点东西吧。你叫什么?在哪个城市?做着什么工作?有什么爱好?最近在看什么书?听什么音乐?看什么电影?有什么心得体会?如果还能分享一个关于你的小故事,那就再好不过了。

更新二:在收到一些访谈邀约邮件后,我觉得有必要再加一条更新。我最近没有接受播客或访谈邀约的计划,所以提前在这里说明一下,以防有人在邮件中问起。

题图

Credit to Hawstein

不上班的 613 天

2020-02-17 08:00:00

前言

人生充满了随机性,很多事情都不在我们的计划之中

比如此刻的我,于 2020 年的情人节,坐在苏黎世联邦理工学院(ETH),回顾着从离职到现在的 613 天,准备写下这段时间的经历和感受。今天既不是旧一年的结束,也不是新一年的开始,从我完成的事情来看,也并没有到达一个值得写篇恢宏长文以纪念的里程碑。决定坐下来开始写这样一篇文章,仅仅是因为某位老板希望我的宣传文章能把内容再丰富一些,于是我决定完全推倒重写,感谢那位老板。人生就是这样,绝大多数时候我们开始做一件事情,以当时的目光去看,都不是做那件事情的最好时刻

成为 Indie Hacker

2018 年 6 月 11 日,是我在 GrowingIO 的最后一天。当天和 CEO 进行了短暂而真诚地沟通后,我离开了这个工作了两年多的地方,开始了我的 Indie Hacker 之旅。对我来说,辞掉工作成为一名 Indie Hacker,并不是一件早早就在心里计划好的事情。一直以来,我的计划都要更稳妥一些。先以副业起步,待副业收入稳定后,再辞掉工作全职做。但回顾过去几年的工作经历,这个看起来稳妥的计划,对我来说可执行性却非常低。其中最重要的原因是,我的个性也好我的直觉也罢,总会在我面临选择时让我选择 Hard 模式。放弃那些安逸的选择,加入最有挑战的公司,并且全身心投入其中。这种实际行为和我原本「聪明」的盘算(主业+副业模式),基本上是背道而驰的。所以「工作之余搞副业」这种看起来万无一失的计划,在我这就没有真正地执行过。

于是当日子一天天地临近 last day,我改变了主意。与其换一份所谓「轻松的」主业,然后工作之余搞副业。不如一步到位,直接副业转正。辞掉工作时,我的所谓「副业」还只是脑中的一个想法。我花了几天时间做了一个 slides,向老婆做了非常正式地展示,陈述了我这么做的底层逻辑和可行性分析,并且给出了一个粗略的计划,希望得到她的支持。我觉得,组建家庭后,另一半相当于就是你的人生合伙人。在很多重要决策上,得到合伙人的支持,才能走得更好走得更远。现在回忆起来,那天的 pitch 进行得并非一帆风顺,场面一度十分激烈(此处略去 128 字)。不过,最后我还是得到了合伙人的支持。在这里要特别感谢一下老婆,以及当初那个坚持的自己。

从 0 到 1

得到老婆的支持后,接下来就要开始着手做事了。当时我有几个可选的方向,综合对比评估后,我选择了「算法教学视频」。由于我个人很喜欢 RailsCastsLaraCasts 的视频以及他们的商业模式,为了向它们致敬,我给网站及产品取名 AlgoCasts(Algorithm Screencasts 的缩写合并)

我之前没有做过教学视频,于是在网上搜索调研,要怎么制作一个好的教学视频。这是这个时代特别好的一点,只要你懂得在 Google 键入恰当的关键词,就能找到几乎任何学习资料,或是找到那个可以让你学习的人。经过一番调研学习,我大概总结出了从零制作一个视频的步骤,然后就开始制作我的第一个算法教学视频。现在回过头去看第一个做出来的视频,可以说是惨不忍睹。但在当时,却给了我很大的信心。对我来说,只要可以按照一个固定的模式进行重复生产,那么产出的数量和质量就会变得可预期,或者说控制在一个预期范围。后来,这套视频制作流程也随着制作视频的增多而不断优化,成为一套为我自己量身定制的流程。

视频制作的工具和流程都有了,并制作出第一个样例视频后,我就开始写 AlgoCasts 网站。写网站对我来说并不难,更何况对于 MVP 版本,我觉得只需要完成几个页面就可以上线了:首页、视频列表页、视频详情页(也就是播放页),注册/登录页,支付页。网站的 MVP 版本很快就开发完了,点播服务在做了一番详尽的调研后选择了保利威,支付则是简单粗暴地使用了微信/支付宝的静态收款二维码。很多人有一个误区,总想着自己出来做一件事的时候,第一步是注册一家公司,去搞定各种资质。而我觉得,第一件重要的事情是快速地把产品的 MVP 发布出来,送到终端用户面前,看他们是否愿望为你的产品或服务买单。否则的话,大概率会陷入这样一种局面:在把所有不重要的事情做完后,要么发现产品开发严重滞后,要么产品发布后无人问津。而一开始花在那些不重要的事情上的时间、金钱和精力,就都打水漂了。另外还有一点,虽然在做事上我是乐观派,但我心里始终都有这样一个信念:大多数时候我们做的事情都是以没有结果告终的,这才是常态

网站 MVP 完成后就上线了,就在那放着不做宣传,并让几个朋友有事没事上去帮忙测试一下。接下来的时间,就是完成第一批少量视频,然后就可以正式对外公开宣传。那段时间正好赶上新买的房子在装修,于是我这个「无业人员」就承担了大部分装修相关的工作,时不时要到新家和工长尬聊一会儿,看看装修进度什么的。而当年的 9 月底则是我和老婆的婚礼,整个婚礼都是我们自己操办,因此也多少还有些事情。正如文章开篇说的,当时那种时间节点,怎么看都不是辞职瞎折腾的好时候。幸运的是,从事后回头看来,结果也不算太差:)

第一批 40 个视频做完时,已经临近婚礼。于是我暂时把 AlgoCasts 的事情放下,专心准备婚礼。婚礼一结束,我就把已经准备好的宣传文章以及视频发布在我的博客、Twitter 以及微博上。我记得很清楚,宣传文章是在 9 月 25 号下午发布出去的。发布第二天,就陆陆续续地开始有人付费购买。

至此,AlgoCasts 完成了它从 0 到 1 的转变。

从 1 到 100

在宣传这种事情上,我一直都偏于保守(乃至于这篇文章都写了这么长的篇幅,我却还在想现在可能不是写这篇文章的好时候)。正如 AlgoCasts 完成从 0 到 1 的转变时,我的宣传也都尽量避开了身边的亲朋好友。因为我觉得它还没有好到可以向我的亲朋好友们展示,我觉得还需要再等等,等到 AlgoCasts 从 1 变成 100。

AlgoCasts 从 1 到 100 的计划里,最最重要的就是把视频数量做上来。于是,网站正式对外接客后的两个月,成了我当年最高效的两个月。在那两个月里,视频每日更新,制作视频之余还完成了许多网站功能的开发。为了保证视频日更,那期间发生了许多有意思的事情。比如朋友婚礼前夜,在陪朋友喝得酩酊大醉后,想起当天视频还没有发布。赶紧从酒店床上爬起来,在意识模糊双眼朦胧的状态下,把提前制作好的视频发布了。再比如,凌晨两点钟为了不吵到家属睡觉,躲在次卧蹲在地上,电脑和麦克风放在一个小小的床头柜上,压低音量进行录音。凡此种种,都是那两个月里留下的有意思回忆。

两个月一晃而过,一共做了 60 个视频,加上最初的 40 个,彼时的 AlgoCasts 上已经有 100 个算法讲解视频了。于是我做了一张海报发到了朋友圈,算是正式向亲朋好友们公布了这个事情。从那个时间节点开始,我就没有把所有的时间都拿来做视频了。而是开始花一些时间来做 AlgoCasts 周边的一些事情,比如说市场 & 运营。现在回头去看我做市场 & 运营的成果,可以说是非常一般。每次做活动或是推广,感觉都要花掉我不少时间,而收效也并不是太好。这一块我估计还有很长的路要走。

不断 Say No

AlgoCasts 步入正轨后,我也慢慢地做了一些其他事情。比如作客 teahour 录了一期播客;上线了终身会员 Plan 并提供额外的增值服务;上线测试完备的算法项目;在北京高校地推;做了 AlgoCasts 的配套论坛;网站改版并上线 AlgoCasts 2.0;接入支付 API;提供美金支付方式;不定期地搞搞活动;每月写一篇灌水小结。AlgoCasts 的动作虽小,但也算频繁。慢慢地,就有各种各样的机会找上门。有希望投资我组建团队成立公司的,有在线教育平台希望我去讲课或是把我的视频放到平台上去分销的,有出版社找我出书的,有希望购买网站源码的,有找我当合伙人一起创业的。类似的机会可能时不时就会来一个,并且不少的合作意向初听起来都挺诱人的,像我这种没见过大世面的人难免心动一下。但夜深人静时,仔细想想当初自己为什么要一个人出来做一名 Indie Hacker,这些机会是不是与自己的初衷背道而驰,我就有了非常明确的答案。

于是,截止目前为此,此类机会或是合作意向,我都婉拒了。过去一年多,是一个不断 say no 的过程。在不断 say no 的过程中,我越来越明确自己想要什么以及不想要什么。我希望 AlgoCasts 可以保持独立,start small & stay small;并且不要投资,不要办公室,不组建团队(也许在未来,会有其他合作方式)。

搬家到瑞士

2019 年 3 月底的一封邮件打破了原本平静的生活,老婆工作上有机会 transfer 到瑞士。如果我们之前没有来过瑞士,或是我还没有辞职,可能这样一封邮件就会和绝大多数邮件的命运一样:看一眼然后直接归档。但偏偏在这封邮件到来的前一年,我们去了趟瑞士旅行,而且都非常喜欢这个地方,并且还半开玩笑地说以后有机会要来这里生活几年。而我也已经辞职自己单干,工作完全不受地点限制。感觉就像老天知道了我们的情况,然后送了个机会给我们。

一开始我们都不以为意,去与不去大概各占一半。不过随着时间的推移,我们慢慢地倾向于出去。并且在某天做了决定后,就开始着手工作签证的申请。瑞士的签证申请起来比较麻烦并且时间比较长,如果我没记错,整个过程应该是花了两个多月才办妥。签证办下来后,定好机票,慢慢打包好要带走的东西,等待出发的那一天。

2019 年 7 月 21 号,飞机落地苏黎世,要在这里开启一段新生活了。

Routine 的重要性

来到瑞士后,除了租房办卡办证学德语,对我来说,还有一件非常重要的事情需要做,就是重建 daily routine。对于上班人士,这是一个不必过于操心的概念。因为公司或组织自然就会有一套 routine,大家只要和其他人一样,按要求去做就行。几点需要上班,几点可以下班;周一到周五哪天有例会,哪些时间可以专心工作;午饭晚饭是在公司食堂里吃还是和同事在周围的饭店吃,烟党们大概会在一天什么时候下楼抽个烟吹个风,哪天又该出去喝杯咖啡和上级或下属聊聊天。凡此种种,不一而足。每个人的生活都有一定的模式,这让你对今天会见到什么人做什么事有一定的预期。这种不断重复的模式,让人可以处于一种稳态,有利于持续地做事和输出

我想不少人对自由职业者或 Indie Hacker 有一定的误解,以为成为自由职业者就可以逃离公司里那种不断重复的日子,365 天过得多姿多彩不带重样。有这种想法的人往往自己不是自由职业者,于是会对未知的事情产生过分天真的幻想。事实上,我认为一个优秀的自由职业者或是 Indie Hacker,都会有非常明确的 daily routine、非常明显的生活模式。我这里说的 routine 或模式,并不代表每天要过得一模一样或一整天都让自己淹没在工作中。而是指大部分的日子里,有一些核心的模式是不变的。举个例子,有的自由职业者喜欢在城市里寻找不同的咖啡厅办公。这里不变的模式就是在咖啡厅办公,点上一杯咖啡,然后完成今天的工作。再比如说数字游民(digital nomad),听起来好像在全世界一边旅游一边工作,好不快活。但事实上,数字游民一旦选择在某个地方待上几个月或更长时间,就会开始倾向于在每天差不多的时间去固定的一个或几个地方,以便更高效地完成他们的工作(比如知名数字游民 Pieter Levels,他在巴厘岛常去的就是 dojo 联合办公场所)。

自由职业者不是拿着钱到处挥霍的富二代,如果想真正做出点什么东西来,routine 必不可少。至于我,在北京的时候喜欢去固定的一家咖啡厅工作。而来瑞士后,经过一段时间的探索,工作日我会到苏黎世联邦理工学院办公,我喜欢在朝气蓬勃的学子与和蔼谦恭的教授当中工作:)此外,学校的食堂对外开放,因此工作日的午餐和晚餐也解决了。我觉得对自由职业者来说,ETH 可以说是一个相当不错的工作场所。以此类推,如果你是一个自由职业者,除了咖啡厅或图书馆,也可以到当地高校去探索一下,说不定会发现一片新天地。

Indie Hacker 的困境

我觉得 Indie Hacker 常常会面临以下几个困境,第一个是 Burnout,也就是投入过多用力过猛,快速地把自己的热情燃烧殆尽。大多数 Indie Hacker 选择的是自己喜欢的事情来做,所以容易在一开始用力过猛,仿佛好不容易有了这么多可自由支配的时间,恨不得把所有的时间都花在喜欢的事情上面。或者是对自己的能力没有做出正确的评估,给自己安排了过于激进的计划。又或是产品有了越来越多的客户后,开始要投入更多的时间去服务客户。不管出于什么原因,不管你有多热爱你做的事情,一旦长时间满负荷地投入在一件事情里面,迟早有一天会把热情和动力都消耗殆尽。而作为一个缺少外在约束的 Indie Hacker,那一天很可能就意味着停滞与放弃。

接着上文,引出第二个困境:Indie Hacker 要说放弃实在太容易了。如果是在一家公司上班,有外在和内在两个因素可以持续推动一个人去工作。一个是来自公司、老板或同事的外在约束,你可以在不喜欢这份工作的同时,把手里的工作完成了(暂且不管输出质量如何)。第二个是来源于自己内部的驱动力。动机可以五花八门,但内驱力让你从自身出发,想去工作并且把工作做好。而 Indie Hacker 主要靠自己的内驱力来推动自己持续工作。一旦内驱力不足,很容易就会在遇到困难的时候放弃,导致在一番折腾之后,产品无疾而终。说起这个困境,顺手推荐 indiehackers.com 的创始人 Courtland Allen 的一期播客:Your Whole Goal Is to Not Quit

Indie Hacker 的第三个困境在于 indie。人类终究是社会性动物,我们需要社交,并且在人和人的交互中学习以及得到心理上的满足。在一家公司上班,自然而然地我们就会有一群共事的同事。每天可以和一群人协作去完成共同的目标,互相学习,交流八卦,这些都是健康生活中必不可少的事情。而 Indie Hacker 则主动选择离开这样的环境,难免会带来一些问题。不过好在,这个时代可以让我们比较容易找到同类,因此 Indie Hacker 们也可以找到属于自己的社区,并和其他 Indie Hacker 交流或是协作。不过线上虚拟社交无法取代线下真实的社交生活,因此我觉得,Indie Hacker 们若是想让自己的生活更健康一些,还是要积极创造和朋友们线下交流的机会。一起去撸个串,吃个火锅,或是喝杯咖啡谈谈心吹吹牛,这些是美好生活的重要组成部分。

Indie Hacker 还会面临其他困境,比如说怎么处理那么多的自由时间,比如说怎么让自己保持干劲(keep momentum);或是回到产品与商业本身,怎么从 0 到 1 做一款可以盈利的产品;怎么把产品从 1 做到 100,等等。有许多困境并非 Indie Hacker 特有,这里也不再做过多展开。这个话题很大,足够单独写一篇文章来阐述。

故事还在继续

时间快得令人不敢细想,不知不觉间,我已经辞职 600 多天了。在这 600 多天里,我做了一些事情,虽然并不是那么值得一提,但每一件事情都见证了我作为 Indie Hacker 的每一天,于我来说都是珍贵的。在这 600 多天里,我换了一个国度生活,学习一门全新的语言。在这 600 多天里,我比以前读了更多的书,写了更多的文字,去了更多的地方,结识了更多的朋友,并看着 AlgoCasts 一天天长大。我很高兴踏上这样一条少有人走的路,希望后面可以收获更多有意思的风景(当然也会发现更多坑),并讲给大家听。

福利时间

Plan 200 完结后做什么

2020-01-13 08:00:00

近期 AlgoCasts 完成了网站上 5 个系列(https://algocasts.io/series),共 211 个算法视频的制作。算是一个小小的 Milestone 吧,接下来会做以下几件事情:

录制 Plan 250

视频制作仍然会是 AlgoCasts 的主要工作,根据近期收到的反馈,接下来的算法视频中,会提高以下两类题目的录制频率:

录制专题:好玩的数据结构

之所以要录制这个专题,是由于在讲解算法题目的过程中,有的算法需要用到一些高级一点的数据结构。但如果在讲解题目的视频中,花大篇幅讲某个通用数据结构,会有以下问题:

因此,我决定做个单独的专题来讲解这些好玩又好用的数据结构。

制作极简题解

如果说算法新手需要一个详尽的视频来讲解每个算法题目,那么对于已经有一定经验的小伙伴或者说基础本来就不错的小伙伴,一份类似 Cheat Sheet 的极简题解,就会是一份更加称手的学习资料或复习资料。即使对于同一个人,前期看视频讲解,后期通过 Cheat Sheet 复习,这种组合学习法,也往往会比单一学习法更加高效。

开发神秘 Chrome 插件

目前没有更多信息可以透露。不要问,问就是否认三连:我不是/我没有/别瞎说啊。

Indie Hacker 笔记 | 第 14 期

2020-01-08 08:00:00

又到了码字的时间,这一次的心情很轻松。我想主要原因是,「AlgoCasts 每月小结」系列的完结,带来一个新系列的开始。人总是喜欢新的开始,正如新年给人以希望一样。新系列的名称定为「Indie Hacker 笔记」,由于它的前身「AlgoCasts 每月小结」已经出了 13 期,因此「Indie Hacker 笔记」就从第 14 期开始(关于新系列的命名考虑,文末会给出解释)。

AlgoCasts

距离上次小结已经过去一个多月了。这期间,终于把 AlgoCasts Plan 200 剩余的视频录完。至此,目前网站上 5 个系列共 211 个视频也就全都完结了。昨天把最后一个视频发布出去后,心情有点像来到了一个马拉松补给站。停下来喝点水,吃点东西,然后望望后面的路,想想接下来怎么跑得更好。小伙伴们也开始问我接下来的录制计划,这个在接下来几天之内应该就会发布到论坛公告上。时间过得可真快,不经意间,AlgoCasts 已经上线一年多了。而这一年多,我的工作生活模式也发生了很大的变化,遇到了不少有意思的人和事,感觉也差不多是时候写个跨度大一点的回顾了:)2020 年,我仍然会把大部分时间和精力放在 AlgoCasts 上,感谢一路陪伴与支持我的小伙伴们!除此之外,可能还会稍微分出一点时间来,开发一个小产品。这个小产品我已经构思挺久了,不过去年一直克制自己不要轻易开新坑,希望今年它能顺利面世。

德语

圣诞节后,我们把德语课的频次从一周一节提高到了一周两节。主要基于两个考虑,一个是多留点时间给我准备德语考试,二是德语老师在三月底要去维也纳进修他的语言学专业,我们想在他离开瑞士前把课程上完。到目前为止,我觉得德语学习的效果还是不错的。加之每天就生活在一个讲德语的城市,在学校里,在超市里,在车上,在路上,满目可见的都是德文,耳濡目染,进步也就快一些。我时不时会把这些生活中看到的德文拍照保存,然后再翻译学习。也经常为能看懂生活中某处出现的德文,为能听懂旁人聊天中出现的一些词汇或句子而小小高兴一下。在外买东西时,我也总是优先使用仅有的一点德语和对方交流,直到对方突然说出一个生词或是一个陌生的句子,使我露了馅,对方才会意并礼貌地切换成英语。这样有趣的例子可以说不胜枚举。零基础开始学习一样新东西的好处就是,进步是以一种可见的速度在发生的,毕竟起点太低了,越往后提升就越困难。不过关于德语学习,我还不需要考虑到「往后」这个阶段,毕竟还有段距离。

阅读

12 月看完了一本书,来自 James Clear 的《Atomic Habits》。这本书是从别人推荐书的推文回复里看到的(最近书架上至少有 4 本书是从别人的推文中挖掘出来的,我管这叫 steal from the tweets),于是在网上看了看评价后果断下单。书挺薄的,每天通勤路上看,很快就看完了。我觉得这本书写得还不错,书中倡导的「Build Your System」让我联想到福柯的「自我技术」。简言之就是,你的外在或内在系统,都可以由你精心设计,设计成适合你自己的系统,让你在这样一个系统内沿着理性为自己设定的路径前行,并且阻力尽可能小。整体上来说,我还是挺推荐去看一看这本书的。我有不少观点和书中观点一致,但如果让我写,现阶段应该没办法像作者写的那么好。关于输入和输出,我自己有一个理论,就是你永远只可能输出你所有输入的一个很小占比。所以,为了写一本还不错的书,为此要储备的知识与素材,要远多于书中所呈现的东西。当然了,输出垃圾不受这条理论约束:)

顺便提一句,关于「Build Your System」或者说「自我技术」,目前我了解到的人中,Stephen Wolfram 可以说是做到了极致。Stephen Wolfram 是计算机科学、数学、理论物理方面的著名科学家,是 Wolfram Research 的创始人,Mathematica 的首席设计师,《一种新科学》的作者。多年来,他一直在 hack his personal infrastructure。关于这些,强烈推荐阅读一下他写的长文:Seeking the Productive Life: Some Details of My Personal Infrastructure,相信你会获得一些启发。

App

最近在朋友的推荐下,开始使用一个 App 来记录时间,叫「Now Then」。一开始我对这个 App 并不抱多大预期,但在用了一小段时间之外,竟然发现意外地简单好用。如果你像我一样有记录时间的习惯,可以考虑下载来使用一下。关于记录时间这件事,最早我是从「奇特的一生」这本书看到的。这本书讲述了主人公「柳比歇夫」是如何使用他的时间统计法,对他做的事情以及花费的时间进行记录和研究的。后来我就开始探索适合自己的时间记录方式,也简单搜索过相关的辅助工具,不过都没遇到特别好用的。所以后来我干脆就用 Wunderlist + Evernote 的方式手工记录。我记录时间的方式不像「柳比歇夫」那样严苛,主要是围绕工作以及一些比较花时间的事情,碎片时间一般不记。这个不经意的习惯已经跟随我多年,目前我觉得带来的好处是,除了知道自己大量的时间花费到了哪里,对时间的敏感度也提升了。另外,在开始做一件事前,我会预估一个时间,做完后再记录下实际使用的时间,进行对比。如果这件事是需要经常反复做的,一般经过一段时间调整后,时间预估就可以达到一个比较准确的程度。

旅行

旅行方面,12 月份去了趟布达佩斯。在把所有细节都忘记后,目前我脑里关于布达佩斯的记忆就剩下:好看的建筑,好吃的美食,舒服的温泉。吃的方面,无论是食材的丰富程度还是做法的多样性,都比瑞士要高出不少。当然了,如果你是从国内出发去布达佩斯,请忽略我关于美食的那句评价。一月份还有两次出行:米兰和湾区。来瑞士后,出行变多了,所以最近还在思考以及需要解决的一个问题就是,如何在出行期间不打断我的某些 routine。所幸,我工作或生活中做的大部分事情,都不太需要依赖很「重」的外部条件。因此,大部分的事情即使在旅行中,也一样可以做。目前唯一的影响可能是我的录音设备不太方便携带,因此目前我决定出行期间就不录音,转而撰写/积累视频素材,或是做产品开发。(其实,非要携带上那台小小的 Blue Yeti 倒也不是不可以,最近我在推上还看到 Marques Brownlee 在去 CES 期间,把一整套非常专业的录音设备布置到了他入住的酒店里,为了消音,还把被子/毯子挂到了房间的墙上。虽说他是专业做这行的,但我也不禁小小感慨了一下,很多事情只要你想做,自然就会有办法。)

结语

前段时间接受了一个科技媒体采访,其中有一个问题的回答我自己还挺喜欢的,就用它作为这一期的结束语吧。问题是:工欲善其事,必先利其器。无论是工作还是生活,有什么特好的“磨刀法子”是你巴不得大家都知道的?面对这个问题,我第一反应并不是想到要分享什么「利器」,因为我发现当代社会影响我们「善其事」的首要因素,往往已经不是我们手中的「器」了,而是别的东西。以下是我的回答:

删掉抖音以及其他同类产品;把绝大多数的微信群(或者其他 IM 群)都设置成消息免打扰;所有的 App 都默认禁用通知,除非你认为这个通知重要得不能禁用;把所有的 IM App 都放到二级文件夹,或者移到首屏之外。

这个世界太嘈杂,嘈杂得想好好安静下来做一件事都特别困难。办法只有主动去关闭一些与世界的连接。相信我,这个世界大多数时候都只是在发出噪声而已,关闭一些连接的好处远远大于坏处。

完。

关于新系列的命名考虑

我受国外 Indie Hackers 影响挺大的,并且从 2018 年辞职成为一名 Indie Hacker,而我写的东西一般都围绕我的工作和生活展开,所以很自然地就想到用「Indie Hacker 笔记」这样的题目了。

为什么这个系列的名字要中英混用呢?因为至今我也没在中文世界找到 Indie Hacker 的合适对应词。Indie Hacker 直译是「独立黑客」,但中文里「黑客」的定义过于狭义了,刻板印象总会觉得「黑客」一定是和计算机或编程相关,但 Indie Hacker 覆盖的范围比这要大得多。事实上,我觉得在英文世界里,hack 这个词的泛化程度已经和中文里的「搞」有得一拼了。几乎什么都可以 hack:hack your life, hack your mind, hack your project, hack your iPhone…自然地,Hacker 的含义也就不再仅仅是一群搞计算机和写代码的人了。关于 Indie Hacker 这个词,indiehackers.com 的创始人(也是提出这个词的人)在 About 页面是这样描述的:

You’re an indie hacker if you’ve set out to make money independently. That means you’re generating revenue directly from your customers, not indirectly through an employer. Other than that, there are no requirements! Indie hackers are often solo founders, software engineers, and bootstrapped, but it’s totally okay if you have cofounders, can’t code, and have raised money.

由于实在没有合适的对应词,于是就决定保留英文原词了。另外再附上 IndieHackers 网站的一个相关讨论帖子:

[Discussion] What is an Indie Hacker?

AlgoCasts 2019 年 10 月小结

2019-11-30 08:00:00

现在是瑞士时间 11 月 30 日晚上 9 点多,还有几个小时就 12 月了,踩在 11 月的尾巴上,得赶紧把我的每月小结写一写。这篇可能是「AlgoCasts X 年 Y 月小结」系列的最后一篇了,所以嘛,先来首淡淡忧伤的音乐定定基调:Flightless Bird, American Mouth。

So,是 AlgoCasts 要倒闭了么?这个倒没有,事实上 AlgoCasts 第一年的表现已经超出了我的预期,今年的营收目标也在 9 月份的时候就已经达到了。那为什么「AlgoCasts X 年 Y 月小结」要停了呢?Hmm,这个嘛,因为在这个框架下我的写(扯)作(淡)才华发挥不出来。事实上,每月围绕 AlgoCasts 写小结都让我绞尽脑汁。因为,每月围绕 AlgoCasts 做的事情其实是很简单的,就是录制算法视频。就算我一个月出 100 个视频,我觉得也没什么可写的。因为它的事件类型其实是单一的,就算我写出花来,说的也是我在做视频,对不对?嘛,自由职业者的生活就是这么朴实无华且枯燥:)因此,当我回顾过去一年写的 AlgoCasts 小结,除开做视频,我都会尽量在一个月里做点功能开发、搞点营销活动,这样写起小结来就还能扯点别的。不过最近几个月我已经不怎么做网站功能开发了,因此,我的AlgoCasts 月小结真的就要没有素材可写了。

当然,这个月我还是做了一次营销活动,非要写的话,整个 800 字,再升华一下随手谈谈人生,对我来说也不是什么难事。但我已经有点倦了。每月小结最主要是写给我自己看的,我要按我喜欢的方式来写。因此,「AlgoCasts X 年 Y 月小结」这个系列要停了,但是,一个涵盖范围更大,讨论主题更广的每月小结将就此产生。Surprise!

以后的每月小结将不会再只谈 AlgoCasts,而会是过去一个月工作/学习/生活中值得记录的内容。当然,能升华的我还会继续升华 233。毕竟生活中不只有柴米油盐,还有智人透过柴米油盐 YY 出来的哲学道理。由于 AlgoCasts 会继续占据我生活很大的一部分,因此每月小结肯定还是会有它的身影。不过,再次谈起来,我希望可以更感性一些。而不是冷冰冰的本月录了 X 个视频,修改了 Y 份简历,做了 Z 场模拟面试。听起来就很冷,缺少温度。

那么,「AlgoCasts X 年 Y 月小结」的最后一篇是不是还得守好最后一班岗,交待一下本月做的事情呢。其实上面我就已经说了:录了 X 个视频,修改了 Y 份简历,做了 Z 场模拟面试。哦,还搞了次促销活动。

你看,自由职业者的生活就是这么朴实无华且枯燥:)

完。

AlgoCasts 2019 年 9 月小结

2019-10-29 08:00:00

这个月除了做视频,还去了趟土耳其,回了趟国,生了场病,做了三场模拟面试,研究了一下 Super Memo 算法。本月的小结音乐要祭出我的私人收藏:这是你想要的生活吗,来自「房东的猫」。

出行方面,按计划去了趟土耳其和中国,希望今年剩下的日子不用再长途旅行,不然今年的 OKR 就要凉凉了。你懂的,年末的日子就是留给突击用的。

本月还生了场病,本来生个小病这种事情实在不应该拿出来讲。不过讲真,每次生病对我的情绪影响都挺大的,进而影响工作和生活,而且我生病的频繁还挺高。于是我决定给自己制作年度生病日历,看看到底自己一年有多少天是在生病中渡过的。另外,我还准备锟斤拷锟斤拷录冶锟斤拷。嗯,为了避免公开立 flag 导致事情没有执行,此处就保留一下。关于立 flag 这一点,我是真的很迷信,哈哈哈。

另外,本月做了三场模拟面试。大家准备的充分程度不太一样,但整体上来讲,算法这一块还是要多练,练习到经典题目上来直接 bug free 解题就差不多了。有小伙伴模拟面试完就去某名厂真实面试,算法题要么是原题要么就是经典题的微小变体。变化有多微小呢?这么讲,微小到我觉得你可以直接叫原题。。。我常常在思考,我只提供算法教学视频可能对有些小伙伴来讲确实是不太够的,因为有的小伙伴连视频都懒得看,或者就只是看视频,而不愿意多花费一些力气去思考或总结。我学习一个东西的时候,总是希望从尽可能多的维度和方向去学习同一个东西,或者用不同的方法去学习同一个东西。虽然做起来没有说的那么容易,但这种意识总能让我学习的稍微好一些。另外我也常常会为学习的知识总结出一个属于自己的版本,我觉得这几乎是将知识内化到自己的知识体系中最好的方法了。你可能会遇到一个牛逼的老师,他可以把一个知识用极其易于理解的方式讲给你听,但归根结底那些东西还是那个老师的。他能做到的就是在那一刻,用他的方式让你理解某个知识。但只要你没有用属于你自己的方式把那个知识内化到自己的知识体系中,过不了多久,你就会把那个知识忘个精光。用属于自己的方式(或者说用自己的话,自己的书写)来阐述一个知识,远远比阐述本身的优劣性要重要得多。也就是说,你做笔记也好,写博文也好,归纳总结也好,只要是你自己思考的产物,那么这个产物对你而言,就会比任何一个牛逼老师帮你总结的要好得多。别人的解读永远是别人的,哪怕你去背下来,也还是别人的。为何不去创造一个属于你自己的版本呢?现在很多知识付费平台,为了赚钱,喊的口号都会让你以为,交点钱然后跟着他们的安排就可以躺着把知识学了。这世上真有这种好事么?当然是没有的!除了你自己,所有的人和工具,都只是提供辅助,希望大家在下次掏钱之前,能认清这一点。

对了,以上讲的道理,我觉得不限于学习,放在人生这个尺度上同样适用。人生中各个主题,只有你自己经历的,才是属于你的。

最后,本月还抽空研究了一下 Super Memo 算法,感觉非常有意思,在未来的某个产品中,也许会用得上。

你看我,又立 flag 了。

完。

AlgoCasts 2019 年 8 月小结

2019-09-28 08:00:00

每月小结又拖出新高度了,看了眼 Wunderlist 和 Google Calendar,过去一个月可以概括如下:搬新家,装家具,修电器,做视频。网站方面加了个简单的 EDM 功能,大家肯定没兴趣听;做视频呢,也没什么好讲的。那就简单讲讲生活上的事情吧。本月的小结音乐来自人称西班牙版彭于晏的帅气男歌手 Álvaro Soler:「Sofía」。

过去一个月最主要的事情就是搬进了新租的房子。在瑞士,一个多月就找到房子并入住,应该算是比较快的了。我们租的是不带家具的房子,所以跑了几次宜家,才慢慢把家具买全。然后把安装家具这件事分摊到过去一个月,隔三差五安装几件,包括安装大件的家具和钻孔装灯。在国内,这些都花钱雇人搞定。但考虑到这边人工比较贵且我们对这边还没那么熟悉且德语还达不到自由交流的地步,所以就自己动手了。在瑞士,一般上一个租客走的时候会把他的家具都清理掉,包括灯(除非你和他提前商量好要保留)。所以我们刚搬进来时,客厅、走廊和几个房间的房顶上都只有几根裸露的电线=。=作为一个经常吐槽自己生活经验为 0 的人,在房顶上钻孔装灯对我来说还是比较困难的。。不过在安装了一两个灯找到感觉后,就越来越顺手,可以说是指哪钻哪,钻哪装哪。

除了自己安装家具,租来的房子里有几件电器坏掉了。于是开始了和本地 3 家公司的打交道过程,分别是 Service 7000 AG,Clean Up AG,Baumann Koelliker AG。每次预约上门修电器时,机智如我都是快速用英语介绍一下自己,希望对方知道我并不会讲德语。然而这并没有什么用,对方还是一定会用德语回答我。然后我会再显式地问一下对方,会不会讲英语。这基本上成了我在这边接打电话的基本 Pattern 了。Service 7000 公司的工作人员大多会讲英文,无论是电话里的客服,还是上门维修的师傅。烘干机是 Service 7000 上门维修的,换了一个中控板子,一次搞定。洗碗机就没那么幸运了,目前已经上门维修 3 次了,第 4 次约在国庆节后。橱柜灯目前上门维修 2 次,第 3 次也约在国庆后。它们分别由 Clean Up 公司和 Baumann Koelliker 公司负责,这两个公司的人员基本上不会讲英语。他们一般上门后,一句 Hello 打过招呼就开始干活。有什么问题也是直接用德语问我,然后我通过当时的上下文和他的肢体动作猜他在说什么,再用英文回答他。我估计对方会使用同样的方法猜我回答的是什么=。=当然,关键场合还少不了 Google 翻译,对方会把一些重要的信息在手机上用 Google 翻译后给我看。比如说,是哪个部件坏了需要换。

工作上,如果看过我上个月小结,就知道我目前是去 ETH 办公。又过了一个月的时间,目前可以说是轻车熟路了。我自己对比了一下之前在北京咖啡厅办公时的状态,整体上在大学里办公的状态比咖啡厅里还要更好一些。随着搬入新房子慢慢安顿下来,这个月视频产出的数量也慢慢恢复上来了。希望这个状态可以保持下去。

过两天就是国庆长假了,预祝大家假期快乐!

十大经典排序算法视频讲解

2019-09-16 08:00:00

代码仓库

代码仓库包含了完整的代码实现和测试,其中 Java 版本是官方实现,其他语言版本来自社区贡献。对于每一种排序算法,如果有多种实现方法,都会尽量提供。另外代码仓库中还提供了完备的测试用例,以此确保实现的排序算法覆盖了每一种可能的情况。

GitHub 链接:https://github.com/HawsteinStudio/algocasts-sorting-algorithms

冒泡排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/AEpoDvWQ

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class BubbleSort {

  private void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }

  // Time: O(n^2), Space: O(1)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    for (int end = n-1; end > 0; --end) {
      for (int i = 0; i < end; ++i) {
        if (arr[i] > arr[i+1]) {
          int tmp = arr[i];
          arr[i] = arr[i+1];
          arr[i+1] = tmp;
        }
      }
    }
  }

  // Time: O(n^2), Space: O(1)
  public void sortShort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    for (int end = n-1; end > 0; --end)
      for (int i = 0; i < end; ++i)
        if (arr[i] > arr[i+1])
          swap(arr, i, i+1);
  }

  // Time: O(n^2), Space: O(1)
  public void sortEarlyReturn(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    boolean swapped;
    for (int end = n-1; end > 0; --end) {
      swapped = false;
      for (int i = 0; i < end; ++i) {
        if (arr[i] > arr[i+1]) {
          swap(arr, i, i+1);
          swapped = true;
        }
      }
      if (!swapped) return;
    }
  }

  // Time: O(n^2), Space: O(1)
  public void sortSkip(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    int newEnd;
    for (int end = n-1; end > 0;) {
      newEnd = 0;
      for (int i = 0; i < end; ++i) {
        if (arr[i] > arr[i+1]) {
          swap(arr, i, i+1);
          newEnd = i;
        }
      }
      end = newEnd;
    }
  }

}

选择排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/Z5mzdwpd

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SelectionSort {

  private void swap(int[] arr, int i, int j) {
    if (i == j) return;
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }

  // Time: O(n^2), Space: O(1)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    for (int i = 0; i < n; ++i) {
      int minIdx = i;
      for (int j = i+1; j < n; ++j)
        if (arr[j] < arr[minIdx])
          minIdx = j;
      swap(arr, i, minIdx);
    }
  }

  // Time: O(n^2), Space: O(1)
  public void sortFromEnd(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    for (int i = n-1; i > 0; --i) {
      int maxIdx = i;
      for (int j = 0; j < i; ++j)
        if (arr[j] > arr[maxIdx])
          maxIdx = j;
      swap(arr, i, maxIdx);
    }
  }

}

插入排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/dbGY9eG5

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InsertionSort {

  // Time: O(n^2), Space: O(1)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    for (int i = 1; i < arr.length; ++i) {
      int cur = arr[i];
      int j = i - 1;
      while (j >= 0 && arr[j] > cur) {
        arr[j+1] = arr[j];
        --j;
      }
      arr[j+1] = cur;
    }
  }

}

希尔排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/zbmKZgWZ

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ShellSort {

  // Time: O(n^2), Space: O(1)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    for (int gap = arr.length>>1; gap > 0; gap >>= 1) {
      for (int i = gap; i < arr.length; ++i) {
        int cur = arr[i];
        int j = i - gap;
        while (j >= 0 && arr[j] > cur) {
          arr[j+gap] = arr[j];
          j -= gap;
        }
        arr[j+gap] = cur;
      }
    }
  }

  // Time: O(n^2), Space: O(1)
  public void insertionSort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    for (int i = 1; i < arr.length; ++i) {
      int cur = arr[i];
      int j = i - 1;
      while (j >= 0 && arr[j] > cur) {
        arr[j + 1] = arr[j];
        j -= 1;
      }
      arr[j + 1] = cur;
    }
  }

}

快速排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/kVG9Pxmg

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class QuickSort {

  private void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }

  // [ ... elem < pivot ... | ... elem >= pivot ... | unprocessed elements ]
  //                          i                       j
  private int lomutoPartition(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = low;
    for (int j = low; j < high; ++j) {
      if (arr[j] < pivot) {
        swap(arr, i, j);
        ++i;
      }
    }
    swap(arr, i, high);
    return i;
  }

  // lomuto partition 的另一种实现,可以把最后的 swap 合并到循环中。
  private int lomutoPartition2(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = low;
    for (int j = low; j <= high; ++j) {
      if (arr[j] <= pivot) {
        swap(arr, i, j);
        ++i;
      }
    }
    return i-1;
  }

  private void lomutoSort(int[] arr, int low, int high) {
    if (low < high) {
      int k = lomutoPartition(arr, low, high);
      lomutoSort(arr, low, k-1);
      lomutoSort(arr, k+1, high);
    }
  }

  private int hoarePartitionDoWhile(int[] arr, int low, int high) {
    int pivot = arr[low + (high-low)/2];
    int i = low-1, j = high+1;
    while (true) {
      do {
        ++i;
      } while (arr[i] < pivot);
      do {
        --j;
      } while (arr[j] > pivot);
      if (i >= j) return j;
      swap(arr, i, j);
    }
  }

  // [ ... elem <= pivot ... | unprocessed elements | ... elem >= pivot ... ]
  //                         i                      j
  private int hoarePartition(int[] arr, int low, int high) {
    int pivot = arr[low + (high-low)/2];
    int i = low, j = high;
    while (true) {
      while (arr[i] < pivot) ++i;
      while (arr[j] > pivot) --j;
      if (i >= j) return j;
      swap(arr, i++, j--);
    }
  }

  private void hoareSort(int[] arr, int low, int high) {
    if (low < high) {
      int k = hoarePartition(arr, low, high);
      hoareSort(arr, low, k);
      hoareSort(arr, k+1, high);
    }
  }

  // Time: O(n*log(n)), Space: O(n)
  public void lomutoSort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    lomutoSort(arr, 0, arr.length-1);
  }

  // Time: O(n*log(n)), Space: O(n)
  public void hoareSort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    hoareSort(arr, 0, arr.length-1);
  }

}

归并排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/M0G2k7pz

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MergeSort {

  // sorted sub-array 1: arr[low ... mid]
  // sorted sub-array 2: arr[mid+1 ... high]
  private void merge(int[] arr, int low, int mid, int high, int[] tmp) {
    int i = low, j = mid + 1, k = 0;
    while (i <= mid && j <= high) {
      if (arr[i] <= arr[j]) tmp[k++] = arr[i++];
      else tmp[k++] = arr[j++];
    }
    while (i <= mid) tmp[k++] = arr[i++];
    while (j <= high) tmp[k++] = arr[j++];
    System.arraycopy(tmp, 0, arr, low, k);
  }

  private void mergeSort(int[] arr, int low, int high, int[] tmp) {
    if (low < high) {
      int mid = low + (high - low) / 2;
      mergeSort(arr, low, mid, tmp);
      mergeSort(arr, mid + 1, high, tmp);
      merge(arr, low, mid, high, tmp);
    }
  }

  // Time: O(n*log(n)), Space: O(n)
  public void sortRecursive(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int[] tmp = new int[arr.length];
    mergeSort(arr, 0, arr.length - 1, tmp);
  }

  // Time: O(n*log(n)), Space: O(n)
  public void sortIterative(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length;
    int[] tmp = new int[n];
    for (int len = 1; len < n; len = 2*len) {
      for (int low = 0; low < n; low += 2*len) {
        int mid = Math.min(low+len-1, n-1);
        int high = Math.min(low+2*len-1, n-1);
        merge(arr, low, mid, high, tmp);
      }
    }
  }

}

堆排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/jwmBqnW8

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class HeapSort {

  private void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }

  // Time: O(log(n))
  private void siftDown(int[] arr, int i, int end) {
    int parent = i, child = 2 * parent + 1;
    while (child <= end) {
      if (child+1 <= end && arr[child+1] > arr[child]) ++child;
      if (arr[parent] >= arr[child]) break;
      swap(arr, parent, child);
      parent = child;
      child = 2 * parent + 1;
    }
  }

  // i 从 end/2 开始即可,因为在二叉堆中,更大的 i 是没有子节点的,没必要做 siftDown
  // Time: O(n)
  // Reference:
  // * https://www.geeksforgeeks.org/time-complexity-of-building-a-heap/
  // * https://www2.cs.sfu.ca/CourseCentral/307/petra/2009/SLN_2.pdf
  private void buildMaxHeap(int[] arr, int end) {
    for (int i = end/2; i >= 0; --i) {
      siftDown(arr, i, end);
    }
  }

  // Time: O(n*log(n)), Space: O(1)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    buildMaxHeap(arr, arr.length - 1);
    for (int end = arr.length - 1; end > 0; --end) {
      swap(arr, 0, end);
      siftDown(arr, 0, end - 1);
    }
  }

}

计数排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/XOp19ap2

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class CountingSort {

  // indexes 最后存储的是排序后,相同数字在结果数组的开始位置,相同数字会依次向后(右)填充。
  // Time: O(n+k), Space: O(n+k)
  public void sortLeft2Right(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int max = arr[0], min = arr[0];
    for (int num: arr) {
      if (num > max) max = num;
      if (num < min) min = num;
    }

    int k = max - min;
    int[] indexes = new int[k+1];
    for (int num: arr) ++indexes[num-min];

    int start = 0;
    for (int i = 0; i <= k; ++i) {
      int count = indexes[i];
      indexes[i] = start;
      start += count;
    }

    int[] tmp = new int[arr.length];
    for (int num: arr) {
      int idx = indexes[num-min];
      tmp[idx] = num;
      ++indexes[num-min];
    }
    System.arraycopy(tmp, 0, arr, 0, arr.length);
  }

  // indexes 最后存储的是排序后,相同数字在结果数组的结束位置,相同数字会依次向前(左)填充。
  // Time: O(n+k), Space: O(n+k)
  public void sortRight2Left(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int max = arr[0], min = arr[0];
    for (int num: arr) {
      if (num > max) max = num;
      if (num < min) min = num;
    }

    int k = max - min;
    int[] indexes = new int[k+1];
    for (int num: arr) ++indexes[num-min];

    --indexes[0];
    for (int i = 1; i <= k; ++i)
      indexes[i] = indexes[i] + indexes[i-1];

    int[] tmp = new int[arr.length];
    for (int i = arr.length-1; i >= 0; --i) {
      int idx = indexes[arr[i]-min];
      tmp[idx] = arr[i];
      --indexes[arr[i]-min];
    }
    System.arraycopy(tmp, 0, arr, 0, arr.length);
  }

}

桶排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/VBpL2omD

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class BucketSort {

  private void insertionSort(List<Integer> arr) {
    if (arr == null || arr.size() == 0) return;
    for (int i = 1; i < arr.size(); ++i) {
      int cur = arr.get(i);
      int j = i - 1;
      while (j >= 0 && arr.get(j) > cur) {
        arr.set(j+1, arr.get(j));
        --j;
      }
      arr.set(j+1, cur);
    }
  }

  // 每个桶的大小,由于桶内使用插入排序,因此桶的大小使用一个较小值会比较高效。
  //
  // 一般来说,当处理的数组大小在 5-15 时,使用插入排序往往会比快排或归并更高效。
  // 因此在桶排序中,我们尽量让单个桶内的元素个数是在 5-15 个之间,这样可以用插入排序高效地完成桶内排序。
  // 参考链接:https://algs4.cs.princeton.edu/23quicksort/
  // 参考段落:
  // Cutoff to insertion sort. As with mergesort,
  // it pays to switch to insertion sort for tiny arrays.
  // The optimum value of the cutoff is system-dependent,
  // but any value between 5 and 15 is likely to work well in most situations.
  private int bucketSize;

  public BucketSort(int bucketSize) {
    this.bucketSize = bucketSize;
  }

  // Time(avg): O(n+k), Time(worst): O(n^2), Space: O(n)
  public void sort(int[] arr) {
    if (arr == null || arr.length == 0) return;
    int max = arr[0], min = arr[0];
    for (int num: arr) {
      if (num > max) max = num;
      if (num < min) min = num;
    }

    int bucketCount = arr.length / bucketSize;
    List<List<Integer>> buckets = new ArrayList<>(bucketCount);
    for (int i = 0; i < bucketCount; ++i)
      buckets.add(new ArrayList<>());

    for (int num: arr) {
      int idx = (int)((num - min) / (max - min + 1.0) * bucketCount);
      buckets.get(idx).add(num);
    }

    int idx = 0;
    for (List<Integer> bucket: buckets) {
      insertionSort(bucket);
      for (int num: bucket)
        arr[idx++] = num;
    }
  }

}

基数排序

视频链接:https://algocasts.io/series/sorting-algorithms/episodes/q2m595mz

截图:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class RadixSort {

  /**
   * @param arr  待排数组
   * @param bits 每次处理的二进制位数(可选值:1, 2, 4, 8, 16)
   * @param mask 每次移动 bits 个二进制位后,使用 mask 取出最低的 bits 位。
   */
  // b 表示每次处理的二进制位数
  // Time: O(32/b * n), Space: O(n + 2^b)
  private void sort(int[] arr, int bits, int mask) {
    if (arr == null || arr.length == 0) return;
    int n = arr.length, cnt = 32/bits;
    int[] tmp = new int[n];
    int[] indexes = new int[1<<bits];
    for (int d = 0; d < cnt; ++d) {
      for (int num: arr) {
        int idx = (num >> (bits*d)) & mask;
        ++indexes[idx];
      }

      --indexes[0];
      for (int i = 1; i < indexes.length; ++i)
        indexes[i] = indexes[i] + indexes[i-1];

      for (int i = n-1; i >= 0; --i) {
        int idx = (arr[i] >> (bits*d)) & mask;
        tmp[indexes[idx]] = arr[i];
        --indexes[idx];
      }

      Arrays.fill(indexes, 0);
      int[] t = arr;
      arr = tmp;
      tmp = t;
    }
    // handle the negative number
    // get the length of positive part
    int len = 0;
    for (; len < n; ++len)
      if (arr[len] < 0) break;

    System.arraycopy(arr, len, tmp, 0, n-len); // copy negative part to tmp
    System.arraycopy(arr, 0, tmp, n-len, len); // copy positive part to tmp
    System.arraycopy(tmp, 0, arr, 0, n); // copy back to arr
  }

  // 基数为 256,每次取 8 个二进制位作为一个部分进行处理,32 位整数需要处理 4 次。
  // 每次取出的 8 个二进制位会作为计数排序的键值,去排序原始数据。
  // 每次处理 8 个二进制位,是时间/空间上比较折衷的方法。
  // 如果一次处理 16 个二进制位,速度会稍微快一些。但需要额外的空间是 2^16 = 65536,远大于每次处理 8 个二进制位所需空间。
  // 如果一次只处理 4 个二进制位,速度则会慢很多。
  public void sort4pass(int[] arr) {
    sort(arr, 8, 0xff);
  }

  // 基数为 16,每次取 4 个二进制位作为一个部分进行处理。32 位整数需要处理 8 次。
  // 时间上比起 sort4pass 要差很多。
  public void sort8pass(int[] arr) {
    sort(arr, 4, 0x0f);
  }

  // 基数为 65536,每次取 16 个二进制位作为一个部分进行处理。32 位整数需要处理 2 次。
  // 时间上比 sort4pass 要稍微好一些,但额外要使用多得多的空间。
  public void sort2pass(int[] arr) {
    sort(arr, 16, 0xffff);
  }

  // 基数为 2,每次取 1 个二进制位作为一个部分进行处理。32 位整数需要处理 32 次。
  // 时间上比快排要差很多。
  public void sort32pass(int[] arr) {
    sort(arr, 1, 1);
  }

  // 基数为 4,每次取 2 个二进制位作为一个部分进行处理。32 位整数需要处理 16 次。
  // 我是打酱油的。
  public void sort16pass(int[] arr) {
    sort(arr, 2, 3);
  }

}

AlgoCasts 2019 年 7 月小结

2019-08-26 08:00:00

AlgoCasts 的这个工作月正好是完整在瑞士度过的,坐在 ETH(苏黎世联邦理工学院) 的自习区,抬头是一位年过花甲的老教授在认真地工作,听着耳旁的「Stay Alive」,接下来我就要开始流水账式扯淡了。

苏黎世第一个月过得并不算轻松。办各种卡、看各种房,然后焦灼地等待邮递员叔叔给我们寄来各种文件/合同/银行卡和最重要的 Permit。瑞士的邮政处于相当主流的地位,各种重要的文件和卡直接就发挂号信给你,感觉这边的邮政相当于国内邮政加上各个快递公司,相当有存在感。幸运的是,虽然有些波折,但各件事情也都慢慢办妥了。包括租到一个还算满意的房子,预计 8 月底搬进去。这里我就不立什么 flag 了,希望后面的流程顺利。

换了一个环境后,我原来那套在北京运行得如丝般顺滑的 daily routine 就需要重建。考虑到瑞士这边在咖啡厅工作并不像国内那么主流,且咖啡更贵,我一开始的计划是在家单独弄一个书房来工作。但后来一个机灵想到 ETH 不就在苏黎世嘛,可以混到大学生当中工作呀。所以我找了一天探索 ETH,主要看都有哪些适合办公的地方。一番调研探索下来,ETH 比我想象的还要更适合当工作场所。除了图书馆,每一层都有自习区域,而且电脑室也是对外开放的。网络方面,我办的套餐是无限 4G 高速流量,用电脑连上手机热点后,上网体验和在家使用 Wi-Fi 几乎没有区别。这个月制作的一部分视频,就是在 ETH 连着 4G 热点上传到国内服务器的。吃的方面,ETH 食堂是对外开放的,因此午餐和晚餐我都在食堂里吃。虽然价格是学生的两倍,但其实也还是可以接受的。这么难吃的食物我都能接受了,两倍的价格有啥不能接受的 233。由于制作一个视频已经被我拆分成几个步骤,目前除了录音这一步要在家里进行,其他的都可以在 ETH 完成。经过一段时间的实践和调整,基本上在苏黎世的 daily routine 就可以确定下来了。几点起床;坐什么车,走哪一条路线;哪里有便宜好喝的咖啡;哪个区域的工作环境和 4G 信号更好并且采光合适,不会过于刺眼也不会过于昏暗;几点去食堂吃饭比较合适,人不太多,不用浪费时间在排队上;家附近超市的开门关门时间,以及卖货特点等等这些问题,都有了明确的答案。

这个月除了做视频,网站方面还把 PayJS 集成了进来,支持微信和支付宝。这个服务是去年某个小伙伴推荐给我的,我都一直把它放在 waiting list,直到最近收款异常(微信大佬表示要开始收保护费了!),于是我以迅雷不及掩耳盗铃儿响叮当之势,完成了申请、开通、接入、测试及上线。当然这方面也要大力赞一下 PayJS 的工单处理速度以及文档完善程度。截止目前为止,PayJS 的使用体验可以用两个字形容:真香。

另外,在 PayJS 回调并开通完相应套餐后,顺便让 Slack Bot 给我推送一条消息,把用户信息和购买信息发给我。这样每一笔成功交易后,都会有微信/支付宝给我发送一条交易信息外加 Slack Bot 给我发送一条套餐开通信息。嗯,这样应该比较稳妥了。

上周某一天还经历了水逆 4 连击。其中之一是在 ETH 工作时,F 键惊喜脱落,打断了我愉快的工作。在我短暂的 hot fix 后(把键帽底下倒 u 型的一个部件用力按缩小开口),虽然可以勉强使用。但我相当确信,只要我一奔放地码字或码码,它肯定还会掉下来,毕竟 F 键对我来说是极其高频的按键。另外一点,虽然我的 Apple Care 是到 2021 年,但查了一下,看起来只能在中国使用。事情发展到这一步,可以说是相当影响心情的了(如果你重度依赖你的笔记本电脑吃饭,应该能体会我的心情)。所幸!我把在家吃土好久的 HHKB 随身带来了瑞士。嗯,现在我每天背着它来 ETH 工作。确实是有点浮夸,可是没办法,自带键盘出问题了呀。我有个朋友(人称键少)爱好收集各种键盘,外出常年自带键盘。我依稀记得第一次面基见到他时,他正在咖啡厅外接着自带的机械键盘工作,当时我觉得大概除了他没人会这么做了。没想到今天我也已经习惯这么做了。人啊,有时候适应起新习惯来还真是快呵。

这个月还开始了德语学习。目前在 Anki 上自制 Deck 进行学习。Anki 会根据遗忘曲线,将学习内容在不同的时间段后召回,促使你重复学习,目前使用体验还不错。如果正好你也在学习需要记忆的知识,推荐一下这款应用。

流水账写完,如果你竟然耐心地看到了这里,那我就再推荐这个月看的两部电影吧。第一部是黑泽明的「生之欲」;第二部是「The Secret Life Of Walter Mitty」,中译名叫「白日梦想家」,名字虽然译得烂,但电影还可以。开头提到的音乐「Stay Alive」就出自这部电影,这也算是首尾呼应了吧。

完。

AlgoCasts 2019 年 6 月小结

2019-07-26 08:00:00

本月小结配乐是「A Quiet Departure」,很应景。这个月我们静静地离开了北京,来到苏黎世开启一段新生活。当然,我还是会继续全职做 AlgoCasts,大家请放心,我没有跑路。

由于本月搬家到苏黎世,AlgoCasts 的产出并不高。除了算法视频的制作,额外花时间拍了一支 vlog,讲述我自由职业状态下,一天是怎么度过的。另外之前不少小伙伴也问我算法视频是怎么制作的,在这支 vlog 里也一并讲了。这是很早以前就计划做的事情,只不过优先级不高一直被拖延。但一想到马上就要离开北京,再不拍就讲不了这个故事了,于是花了几天时间,从写 script 到拍摄到剪辑,把这支讲述我普通一天的 vlog 拍了出来。同时发到了 YouTube/B 站/公众号,反响还不错。这样,我也算是一个入门 vlogger 了 233。

老样子,这个月帮几位终身会员看了看简历,聊了聊职业规划。我深知每个人的差异是非常大的,于是大部分时候,我只会说,我自己的职业发展路径以及选择是怎样的;对于这种情况,我曾经是这样做的;如果我遇到那种情况,我会那样做。并给出我的理由。这世上没有放之四海而皆准的所谓最优路径,我觉得如果在聊天的过程中,能让对方厘清自己底层核心价值观是什么,看重什么,并根据这些去做出适合自己的规划与选择,那对每一个人来说就是最好的。不要去听信那些什么「跟着我左手右手一个慢动作,然后就可以怎么样」的言论。那些动不动就说「我的成功可以复制」、「达到 A 的 M 条建议」、「实现 B 的 N 种方法」,这样的人,要么是在认识这个复杂世界这方面过于无知,要么就是要割你韭菜:)

另外,按照最早我定下的规则,终身会员达到第一阶段人数,于是在本月小幅涨了一次价。有小伙伴让我直接把价格 double。Hmm,我实在不好意思一次涨那么多。。。

最后,随着 AlgoCasts 后台数据越来越多,我就越来越担心什么时候一个小心,因为天灾人祸导致数据丢失(毕竟即使是像 AWS/阿里云这样的大厂,都出过幺蛾子)。虽然现在已经在香港和日本两个机房互备,但我觉得还是不稳妥,于是这个月再额外做了一层措施,把数据 sync 到本地。这样一来,心里才觉得稳妥些。

好了,这个月小结就写这么多。下一个视频,将会就着苏黎世的美景与清新的空气出炉,希望大家会喜欢。