普通视图

发现新文章,点击刷新页面。
昨天以前Hawstein's Blog

写给 YF:自由生活与创造

2025年7月14日 08:00

To: YF / Jul 20, 2023
关键词:自由生活 / 裸辞 / 独立创造者 / 艺术家的生活方式

Hi YF,感谢来信。

真诚且有意思的一封邮件。所以我决定先回复你的邮件,哈哈。

邮件中你描述的心里的想法,以及那一句「我也喜欢自由的生活」,其实是现在很多城市里的人的想法。现在是一个高度讲究效率,分工极度细化的社会,大部分人每天机械而重复的工作生活,意义感渐渐缺失,所以愈发地渴望自由,一种属于个体的自由。再加上现在发达的互联网,就让这种看似小众的声音,也被越来越多的人听到。

你那句「偶尔会抬头看院子里高墙上的四角的天空」,让我想起上一份工作的一个瞬间:我住在公司附近,每天走路去上班。那一天走路去上班,北京的天气特别好,我经过一家咖啡厅门口,当时我特别想,点一杯咖啡,坐在外面,晒晒太阳,什么也不想。可是当时手机里的工作群已经一条一条消息在滚动了,客户又遇到什么问题了,系统哪里又有什么问题了。我不能随性地坐下晒太阳,我得去公司处理我的事务。

就是这种瞬间,让我特别想拥有属于自己的自由。想停就停,想走就走的自由。

后来我就裸辞了,并且和朋友开玩笑说,我从资本家手中夺回了属于自己的时间。正如我在《一个独立创造者的五年》中所写,我认为这是我人生中最棒的决策之一。当然,这并不是件容易的事。我当时带着一个团队,负责一条业务线,title 好听,收入颇丰。这些年来,和我聊过想自己辞职做点事情的人也不少了。最终都还是无法放弃职场中带来的「好处」。继续一如既往的生活。

当然,我是不鼓励盲目裸辞的。想做点事情,也未必需要辞职。大部分的艺术家,都有一份 day job 养活自己,然后工作之余才搞自己的艺术创作。至少,我们可以从当一个这样的「艺术家」开始。

想要一样东西,至少得做点什么。就比如说,我想要当一个创造者,我想做属于自己的产品,甚至能用它赚钱养活自己和家庭。那第一步,得在每天分配些时间来做些和这个目标有关的事情吧?如果这一份工作天天加班到晚上 10 点,回到家疲惫得什么都不想干,那可能就一直无法开始。那么,是否可以做点什么,比如换个不加班的工作?如果 Top 1 的目标是最终自己做事,拥有属于自己的时间,那么换一个不加班的工作,哪怕收入少一些,工作内容无聊一些,在我看来都能接受。因为我们是要优化一种生活,可以让自己有时间做自己的事。而不是去赚钱最多的公司,或做最有意思项目的公司。

当然,这都是我的价值观下的观点。甚至我做得极端些,比如说裸辞。但我并不会鼓励其他人这么做。每个人都有自己的生活选择,并且每一种生活都没有好坏之分,适合自己最重要。我分享我的故事,只是提供了一个样本点。

回答一下你的问题。我的成功是不可「复制」的,你需要去走出属于你自己的路。我认为没有哪一份成功是可以复制的,连我自己上一份的成功,我也无法复制到下一份。个中太多变量,而那些起决定因素的,很可能是我们以为的不重要的东西(比如朋友一句话,一篇文章,一个故事,甚至是我们不知道的东西)。关于这个话题,我觉得阐述得最好的是 Kapil Gupta 和 Naval 的一段对话。如果有时间,我推荐你把这个播客听完,我设置了起始时间,所以你可以直接听到 Kapil 的阐述。这段播客我听了非常多次,推荐给你。

Kapil Gupta: Conquering the Mind @ 510s

这个时代,当一个创造者确实不难了。尤其对程序员,因为代码就是一种杠杆,可以四两拨千斤。你可以写一个 App,做一个网站,就让全世界的人都来用。

加油!

P.S. 你有博客吗?有的话可以把链接发我。

Best,
Hawstein

写给 XP:半桶水与做事

2025年7月13日 08:00

To: XP / Jul 22, 2023
关键词:ChatGPT / 技术乐观派 / Indie Hacker / 行动力 / 自我提升

Hi XP,感谢来信!

邮件写得很真诚。真诚的邮件我都会回复的。

既然你会 build 也会 sell,那其实就可以开始探索着做点什么。没有人是全才的,我卖东西也一般,但只要去做了,多少也能有些收获。

我觉得,半桶水也好,一桶水也罢,和做成事也没什么必然联系。因为我看到过很多半桶水,做成了事,赚到了钱,收获了成就感。而且在不断做事的过程中,自己也会不断精进。桶里的水慢慢也会多起来。但如果什么事情都不做,半桶水就永远是半桶水。

我是技术乐观派,ChatGPT 的出现,让我稍微瞥见了未来人手一个 JARVIS 的时代。NLP 从未达到这样一个高度。当然,现阶段还是有许多不足,但是它在我的日常工作中已经发挥了不少作用。省下了不少时间。很多人会去试探 ChatGPT 哪些事做得不好,并以此嘲笑那些鼓吹 AI 的人。我觉得大可不必。正如我在文中所说,这个时代有着极度丰富的资源,ChatGPT 也算其中之一,而我宁愿去发现它擅长的那些点,并好好利用起来。一个普通人,坐在一台廉价的电脑后,联上网就能使用那些天才大脑耗费极大智力,投入几十上百亿打造出来的基建。想到这些,我只有兴奋。正如乔布斯当年和 Wozniak 打造了 Blue Box,调用了美国用几十上百亿打造的电话基建,免费打了个电话给欧洲的教皇,那种兴奋。

能做的事情还是很多的,就看一个人是否有想做事的心。虽然,我觉得自己做产品自己去卖,以此为起步,是一个更好的点。但是,如果你觉得某一方面比较欠缺,找到另一个人来补足,两人同样也可以轻量起步。在 indie hacker 圈,这样的例子也不在少数。

不知道你未来是否会自己去做点事情。但不管怎样,先关注起这个圈子,看看别人都在做些什么,应该也没有什么坏处。

Best,
Hawstein

写给 YK:系统与规则

2025年7月12日 08:00

To: YK / Jul 25, 2023
关键词:系统与规则 / 研究生 / 自主创造 / 技术成长 / 选择与行动

Hi YK,感谢来信!

人类社会存在着无数的系统,个体一旦进入一个系统,难免就得遵守其中的游戏规则。进入社会,加入一家公司,进入一个系统。每个公司有自己的规则与制度,大家就是在这样一个游戏规则框架下,努力工作,获得系统中其他个体的认可,获得这个系统里杜撰出来的名号与荣誉,获得「升职」与「金钱」。你现在在学校读研究生,是在另一个系统,这个系统里的游戏规则就是研究新东西,发表论文,获得导师认可,然后毕业。既然你在这个系统中,那就得遵守这个系统的游戏规则,等哪天你离开这个系统,你完全可以把这里的一切都抛之脑后。

我也是这样过来的,没什么不一样。

我读研究生的时候,我们实验室的游戏规则是,既要发表论文,又要帮助导师做工程项目赚钱。我还经常出差,去和别的公司对接。这是这个系统的游戏规则,既然我想从这拿到一个毕业证,那么当我还在这个系统时,就得遵守这里的规则。当然了,一旦我清楚这一点,那么就可以慢慢探索一些出路。在未来,逃离别人创造的系统与游戏,转而自己创造一个系统,自己书写一个自己喜欢的规则。正如我现在的工作生活。

你还年轻,缘分使然让你读到我的文章,然后又让你给我写了邮件。我也正好有些想法回复你。如果这些能在你心里种下一个种子,让你时不时可以想一想,那我觉得,就比你没看过我这篇文章,要好。

你说你不太喜欢现在的生活,可是你有勇气辍学吗?没有吧。即使有,你辍学后你知道你要干什么吗?也不知道吧。既然如此,你现在最好的选择,肯定就还是完成当前的学业。潦草毕业也无妨,反正你也不喜欢这条路,未来也不搞学术,目标就是读完这 3 年,拿到研究生毕业证/学位证,就行。

当然,学习之余,如果能想想未来的出路,那更好。比如你想做厉害的技术人,那你精通什么框架吗?你能自己写个 App 上架吗?你能自己做个网站实现点什么功能吗?如果不能,不就有事可做了嘛。这些是实现你的「厉害的技术人」这个目标需要做的。厉害的技术人,是可以坐在电脑前,用代码构建产品解决现实世界问题的。也不用恐惧,从最简单的事情入手就可以。

不知不觉写了这么多,希望没让你觉得我在说教。我不喜欢说教,但这里我也没有相应的故事可以分享,所以只能干讲理论。

我希望自己能做更多有意思的事,这样就能有好故事分享。我相信,好的故事更能带给人力量。

加油,年轻人!

Best,
Hawstein

写给 YC:日间谋生,夜里逐梦

2025年7月8日 08:00

To: YC / Feb 20, 2024
关键词:艺术家的生活方式 / day job + night dream / 平衡生存与热爱 / 创造与副业

Hi YC,感谢来信!

最近带娃很忙,回复邮件很慢,不过我想 better late than never :)

看了你的邮件,我突然想到以前艺术家的生活方式(正好最近又在重读 PG 的《黑客与画家》)。不知道现在的艺术家是不是这么生活,但在以前,热爱艺术的人为了平衡生存与自己热爱的艺术,大多会选择 day job + night dream 的模式生活。为了能有面包吃,活下去,白天需要找一份有收入的工作。这份工作不需要赚多少钱,但是一定不能让自己加班加点,这样才能在 day job 之余还有精力和热情去做艺术创作。

PG 在黑客与画家中其实也已经提到,对于热爱创造的黑客,这也是一个非常好的出发点。我觉得你可以尝试一下。

即,先找一份工作让自己有收入,这种确定性可以避免给自己带来过多焦虑。这份工作的特点应该是不需要加班,工作轻松,能摸鱼最好!反正这份工作不应该大量地消耗你的精力能量。至于收入多少,反而是次要的。这样的工作,无论是找的本地工作,还是远程工作都行,你可以把所有的要求都降低,来优先确保一点:这份工作要足够轻松。

然后工作之余(甚至上班期间),你就可以开始自己的追梦之旅。无论是创造自己的产品,写书,做 Up 主,为出国做准备,blabla,<在这里插入你想要做的事情>。如果有一天,你的其他收入来源已经足够,再把主业辞掉。

这种模式,我觉得是一种比较平衡的模式。

我自己的话是:辞职,给自己半年时间搞产品,如果达到预定目标就继续,否则就回去找工作。但我并不推荐别人这么做。

加油!有什么更新欢迎回复邮件给我。

Best,
Hawstein

写给 Mogi:职场焦虑与人生选择

2025年7月5日 08:00

To: Mogi / May 19, 2024
关键词:裸辞 / 职场焦虑 / 职业规划 / 独立创业 / Atomic Habits

Hi,感谢来信!

// 来信片断:一是您裸辞时候想过重启人生这种的吗,因为我现在就寄希望于重新读研究生获得应届身份,重新开始。

我没有想过重启人生这种事情。裸辞后并不会把我以前经历的所有给抛弃,我做过的事情,合作过的同事,过去的每一步,对辞职时那个我以及后面的那个我,都是有影响的。我觉得裸辞只是说,站在某一个点时向前看,在众多选择中的一种,只不过可能只有少数人会选择。而且,裸辞后我也做了最坏打算,即半年后如果产品上没有达到一个预期,我可能还是要回去找工作的。这些都想好了,其实就不会有明显的重启人生这种感觉。说不定回去工作,就又和以前的同事一起了。

你现在大概也是,在一个觉得不太喜欢的状态的时间点,那你向前看,有许多选择:

  1. 换个组(现在领导对你工作不认可)。
  2. 换个公司。
  3. 读个书,以应届生再找工作。
  4. 出国读个书(不仅能当应届生,还换了个国家)。
  5. 找个国外的 onsite 工作。
  6. 找个国外的 remote 工作。
  7. 辞职做点自己的事情。
  8. 还有许多。。。

具体怎么选,其实就看你要什么,要什么样的生活。看重什么,可以舍弃什么。我觉得我做决定不会那么痛苦,就是这些事情,我其实想得很清楚。所以可以比较容易做决定。

// 来信片断:二是对于这种即将 30 岁的年龄焦虑,对自己职场上的失败,总有一种“我的人生完蛋了,我以后不知道该怎么办”的这种心理,您有什么调节建议吗?

说不上建议,但我觉得我会换一个视角看这个问题。你在大厂做工程师,在 14 亿中国人里,在 70 亿地球人里,我觉得都不能用「职场的失败」来形容。在这一点上,你可能是对自己的要求太高,或者身边看到很多优秀的人,所以自动地就把要求提高了。对自己有高要求倒没有什么问题,就说我自己吧,我对自己也高要求,但是我也能接受自己下限也很低。什么意思呢,就是我会继续向上追求,无论是做更牛逼的产品,还是产生更多的收入,这些追求让我每天有事可做,甚至充满期待与激情,并与这个世界产生联结。但是,如果突然经济大崩溃,很多人都找不着工作,我的生意与产品也突然都死掉。那我不仅能接受自己拿比以前低得多的薪水的工作,甚至我能接受自己技能点以外的工作。说得夸张一点,如果我做包子很好吃,我不会排斥从一个技术牛人,变成一个卖包子的人。当然了,这种事大概率不会发生,毕竟就算薪资开得再低,我做技术应该也是会比卖包子赚钱。但我就想表达那个意思。

你是大厂工程师,我不知道你为什么会有「我的人生完蛋了,我以后不知道该怎么办」这种心理。我的一些亲戚,以及我以前的一些中小学朋友,他们有的一个月赚几千块钱,都活得很乐观。生得起娃,人生也没完蛋。

// 来信片断:三是也考虑做一些自媒体的事情,想问下您当初独立创业是怎么坚持下来的呢?

我之前和别人开玩笑,说的是:看看自己的银行账户余额,然后就坚持下来了,哈哈。

但实际好像不是这样。我就是给自己定了一个时限,想好了最坏情况,然后就开始做。也有做得厌烦的时候,比如当时录制算法视频。每天每天地做,难免也会有厌烦情绪。但有时候就是对自己说,今天好像状态比较一般,那我就只写 script,不去录音或录视频。甚至 script 就只写几段,只写 1 段。反正不管喜欢不喜欢,去做,哪怕只是一点。比如我最高效时,是一天完成一个视频的所有制作。最低效时,我就告诉自己,今天把这个算法题搞懂,然后写下一句 script,就算完成工作了。哈哈。

我记得我看 James Clear 的《Atomic Habits》时,他好像也用过这类小技巧。就是,他是每天都运动,但今天实在不想去,那就和自己说,我就走到健身房,举 2 下哑铃,就算达成了。然后往往结果是,一旦你真的走到健身房,举了 2 下哑铃,你可能就会接着再举到 10 下。或者做个 10 分钟,甚至到 30 分钟。人一旦启动起来,后面可能就会像有惯性一样,至少再行进一段距离。

我觉得过去 5 到 6 年,我可能是时不时会用到这个小技巧。就是动起来,改个 typo,写个函数,都行。每一次行动,哪怕再小,可能也是对自我身份认知的一个强化。

推荐你看看《Atomic Habits》,我觉得写得不错,说不定对你也会有点启发。

Best,
Hawstein

从一期播客说起

2025年8月13日 08:00

最近作客「捕蛇者说」,录了一期播客。万万没想到,主播 laike9m 为此还写了篇文章,叫 我最喜欢的一期《捕蛇者说》。作为一个友善的人,我觉得我也应该回应一篇文章。我愿把这种行为叫做「创造者之间的礼尚往来」。

既然决定坐下来写篇文章,那就写长一点吧。

我是一个年更博主,最新的一篇公开的文章《一个独立创造者的五年》是 2023 年 7 月发表的,然后我仿佛就从网络上消失了。这句话不是我说的,是我最近随机漫步在 w2solo Products 群里突然说了一句话,一位群友说的。他说,Hawstein 你消失两年了吧。这句话不完全对,因为我偶尔也在推特上发点什么,还发起过一次孤独开发者吃饭活动。

不过整体上来说,过去两年我在网络上确实不怎么活跃。我的主要活动在线下,在生活,在养娃,在做产品。这不仅不奇怪,甚至说这才是主流,大部分人的生活在线下,N 年也不会在网上说一句话。

那么,我怎么又突然在网络上「炸尸」并且竟然还录了一期播客呢?因为我在我的生活里卡住了。我开始怀疑我做的事情,感觉像是陷入「存在主义危机」。

觉知到这一点后,我决定停下来。脱离原来既定的轨道,做点不一样的事情。正如 2019 年的文章《人生不可 DP,但别永远贪心》中所写的,我决定给目前的生活添加一点随机扰动。我找朋友长谈,整理以前的读者来信,重读以前喜欢的书,陪太太一起看电视剧(一般情况下我不看电视剧),疯狂听播客,在社交媒体上表达自己的困惑,还有分享自己写的一些读者回信。// 也许我也该学习 61 刻意去玩玩游戏

然后 laike9m 就在推特上 dm 我,问我要不要上播客随便聊一聊。仔细想想,这种「连接」最是恰到好处。为此,在播客录制结束后,我还发了一条推:

最近处于一种不想做产品的状态,于是决定做点产品以外的事:比如找朋友聊天,整理读者来信,发推。这股强烈的表达欲,被 laike9m 捕捉到了,然后就收到他的私信,约我到播客聊天。

对于播客邀约我一般持谨慎态度。《一个独立创造者的五年》这篇文章发布后,我收到不少播客邀约,其中还有我挺喜欢的播客。但写完那篇文章后,我确实没有太多表达欲了,所以当时的播客邀约我都婉拒了。不是因为别的什么,电台很好,主播也很棒,就是那个时期的我真的没什么想说的了。

而最近表达欲溢出,正好被另一个人捕捉到,在和主播确认了对于我不想谈的内容可以不谈后,就欣然接受这次在线闲聊了。这次闲聊大概 2 小时,非常愉悦,全程 in the flow。最后是因为我这边家人在等着我去吃饭,所以我不得不下线了。不然的话,我感觉以当时的状态,应该还能再聊 2 小时。

我和 laike9m 说,最近我处于一种不想做产品而想找人聊天的阶段,不早不晚你正好就 reach out 了,这种感觉就像是缘分到了。时机正好,水到渠成,我喜欢这种感觉。

这一期播客我们没有提前准备什么,没有列提纲,也没有说固定要聊什么主题。我的心态完全是,我要去和两个朋友聊天,这一次与平时的区别就是要把这个过程录下来。

对话的过程非常享受,录出来的效果也很不错。GeekPlux 评论说「即兴的总是最好的」;孟岩则在 E10 让万物穿过自己 这一期播客中说:好的内容是生长出来的,不是计划出来的;而 Naval 则在 Conquering the Mind 中表述为:

I find that when I’m speaking, I do best when I’m not thinking about what I’m going to say and I won’t even hear what I’m going to say until it comes out of my mouth.

当然,我觉得,「即兴的总是最好的」这里的「最好」是针对参与者本人的最好。即,只与自己比。同样是即兴,我与孟岩和 Naval 可能还差了很远。

不过话说回来,当我回去听这一期播客,能发现许多不太完美的地方。因为听播客的时候,更多的理性与思考参与了进来。

录制播客时,我无法进行过多的自我审查。因为我们处于对话当中,我不能在每次说话前都停顿几分钟对要说的话进行审查。在这种多人对话情境下,自然而然地,我会想让对话流畅地进行。而想要对话流畅地进行,我几乎不能进行过多的思考,而是让本来在我脑里的东西蹦出来。因此,在播客中的表达往往是「不完美的」。录制完以后去听,往往会觉得,嗯,这个地方我可以举一个更好的例子;那个地方我可以给出一个更好的回答,等等。但正好是这种「不完美」的形式,这种想要保持对话流畅性的倾向,让所有参与者跨过「表达障碍」,进行了表达,不管表达水平是高还是低,内容是精彩还是无聊。录制播客,可以让所有参与者都完成表达。而「完成」对表达来说非常重要。

同样的内容,如果以写作作为表达方式,可能就死在「草稿箱」里了。非常惭愧,我的草稿箱里有许多这样「未完成」的表达。而「未完成的表达」在我看来,就等于没有表达。

从这一点看来,播客真是一种很好的表达方式。

话说回来,这期播客只是我脱离既定轨道后,所做尝试的其中一件,我还会继续其它探索。最后就以孟岩在 E30 让奇迹发生 这一期播客中的一句话来结束这篇文章吧:

人生是旷野,不是所有的游荡都意味着迷失。

P.S. 如果你还没听这一期播客,可以去听听 👉 小宇宙链接捕蛇者说链接

一个独立创造者的五年

2023年7月12日 08: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,文章中也有写。

Setting up an US Delaware LLC and bank account fully remotely, as a non US citizen/resident

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

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

美国公司报税小知识

微型公司与超级个体

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

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

这一点在互联网行业尤其明显。如果有一天你突然想做一个 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,让我们用邮件连接

我喜欢和有趣的人交朋友,邮箱 tomhawstein@gmail.com,来 say hello 吧!

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

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

题图

Credit to Hawstein

不上班的 613 天

2020年2月17日 08: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 一天天长大。我很高兴踏上这样一条少有人走的路,希望后面可以收获更多有意思的风景(当然也会发现更多坑),并讲给大家听。

福利时间

  • 网站地址:https://algocasts.io
  • 活动优惠码:613
  • 活动折扣:8 折
  • 截止日期:2020 年 2 月 29 日 23 时 33 分(北京时间)

Plan 200 完结后做什么

2020年1月13日 08:00

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

  • 录制 Plan 250
  • 录制专题:好玩的数据结构
  • 制作极简题解
  • 开发神秘 Chrome 插件(不要问,问就是否认三连:我不是/我没有/别瞎说啊)

录制 Plan 250

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

  • 动态规划
  • 贪心

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

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

  • 喧宾夺主。(讲算法题目的视频就应该专注讲题目,数据结构是辅助工具)
  • 通用知识耦合到一个解决具体问题的视频中。

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

制作极简题解

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

开发神秘 Chrome 插件

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

不上班的 613 天

2020年2月17日 08: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 一天天长大。我很高兴踏上这样一条少有人走的路,希望后面可以收获更多有意思的风景(当然也会发现更多坑),并讲给大家听。

福利时间

  • 网站地址:https://algocasts.io
  • 活动优惠码:613
  • 活动折扣:8 折
  • 截止日期:2020 年 2 月 29 日 23 时 33 分(北京时间)

AlgoCasts 2019 年 9 月小结

2019年10月29日 08:00

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

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

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

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

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

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

你看我,又立 flag 了。

完。

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

2019年9月16日 08: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年8月26日 08: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年7月26日 08:00

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

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

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

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

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

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

❌
❌