<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    
    <title>IT夜航船</title>
    
    
    <description>This website is a virtual proof that I'm awesome</description>
    
    <link>https://rbee3u.github.io/</link>
    <atom:link href="https://rbee3u.github.io/feed.xml" rel="self" type="application/rss+xml" />
    
    
      <item>
        <title>写给自己看的Paxos/Raft笔记</title>
        <description>
          
          这篇文章主要记录自己对Paxos/Raft的一点不成体系的理解，不是写给别人看的，所以不会有专门的图表。起因是学习它们的过程比较痛苦，啃过论文也看了不少别人的理解，但问题始终没有解决：1. 想编程实现它们”似乎”比较简单；2. 想知道它们为什么正确却很困难；3. 第2点导致了第1点”实际”并不简单。网上的文章大部分是基于”抢锁”或者”选主”这类角度来剖析的，虽然方便鸟瞰其大致过程，但是不利于理解其正确性。 Basic Paxos 先说Basic Paxos，它的第一阶段(Prepare)如果理解成”抢锁”或者”选主”，那么你会发现实际上同时可能存在多个提案者抢到了锁或者当上了领导。既然可能同时存在多个，那这个”锁”或者”主”的抽象意义其实也就没那么大。从上帝视角来看确实最多只有一个提案者”真正”抢到了锁或者当上了领导，其他的只是由于消息滞后自以为是。但是即使这样我们依然很难往后走，推导出算法为什么是正确的。 举一个浅显的例子：A以提案ID p1 抢到了”锁”，旋即执行第二阶段(Accept)将值决议成了 v1；此时B以提案ID p2 抢到了锁，旋即执行第二阶段(Accept)将值重新决议成了 v2。如果只从”锁”的角度看这个过程似乎没有问题，但实际上并不会发生这种情况，最关键的地方其实正是这句”但实际上并不会发生这种情况”。 其实Lamport在《Paxos Made Simple》里面的证明思路反而是更好理解的，但我需要换种方式来陈述： 如果一系列交互过程中曾经产生过共识(不管是一次还是多次)，那么这些共识里面肯定存在一个最小的提案ID和对应的提案值，不妨记为(A, V)，也称这次提案为提案A； 我们怎么能保证共识不变呢，一个最直观的想法就是考虑所有参与了第二阶段(Accept)的提案，找出提案ID恰好比A大的那一个，不妨记为B，同时也称这次提案为提案B。如果算法能保证提案B选用的提案值也是V，那么根据归纳法，1推2、2推3……就能一直推下去了； 因此如果我们把第二阶段(Accept)理解为”写”的话，在”写”之前还需要一次”读”，需要让提案B”读”到提案A的提案值V(注意提案B”读”到它的时候有可能还并没有形成决议，但这不影响最终结论)； 根据反证法可以知道第3点是必然的：假如提案B”读”不到提案A的提案值V，意味着有超过半数的接受者本身还没接受提案A，而且承诺不再接受提案ID小于B的提案，那最后提案A的提案值V一定不会形成决议，和假设矛盾。需要强调一点是这里不要用分类讨论，甲情况乙情况什么的，就是B一定能”读”到提案A的提案值V。 整个思路并不是完全严格的，但是循着这个方向走把边角补齐，就会是一个完整的证明。所以回到最开始的问题，用什么角度来看待第一阶段(Prepare)？我个人觉得用”先读后写”的思路来同时看待第一阶段(Prepare)和第二阶段(Accept)更合理一些。先”读”已经形成的决议值，再用它来”写”。理解这个思路以后其实就会知道并不一定需要像论文里面说的”超过一半”。假设”写”需要的数量是W，它确实需要过半，即 W &amp;gt; N / 2，但是”读”需要的数量R其实并不一定得过半，只需要保证 R + W &amp;gt; N 即可。 Raft 以Basic Paxos的视角来看Raft，就会发现真的没有太大不同，区别只在于Basic Paxos需要决议的是一个单值，所以”读”到决议值以后必需”写”相同的决议值。而Raft呢，只是把这个单值拓展到了日志序列。在这种情况下，我们就把”决议不变”的含义从”数值相等”拓展为”只能追加”，剩下的事情就完全顺理成章了。 Raft的选主实际上就是Basic Paxos的第一阶段(Prepare)，所以选主的目的也是相同的：拿到之前已经形成的决议值。还记得Basic Paxos里面是怎么拿到这个值的不，是从所有的Prepare Response里面挑出最大的提案ID对应的提案值。但现在提案值是日志序列了，再全量拷贝必然存在性能问题，所以Raft取巧的地方在于缩小候选范围，只让拥有决议值的节点当选，从而避免了拷贝问题。 同时Raft的日志同步，对应的就是Basic Paxos的第二阶段(Accept)，当然这里也需要用拓展的观点来看待，就是Raft里面的Accept实际上是可以一次又一次地追加的，而并不是只能调用一次。 Multi Paxos 想想之前Raft里面比较取巧的点：缩小候选范围，只让拥有决议值的节点当选。这方面Multi Paxos没有取巧，而是选择了直接硬刚：每个节点都有机会当选。但是由于当选的节点可能并不包含完整的决议值(即单机上并不包含全部已经决议好的日志序列)，所以它有一个反复调用Basic Paxos的过程来将日志空洞”补齐”。你可以认为，在”补齐”的那一刻，它自己拥有了完整的决议值，它当选了。...
        </description>
        <pubDate>Sun, 31 Oct 2021 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2021-10-31-paxos-raft/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2021-10-31-paxos-raft/</guid>
      </item>
    
      <item>
        <title>聊聊一看就会一写就跪的二分查找</title>
        <description>
          
          图书馆自习的时候，一个女生背着一堆书走进阅览室，结果警报响了。阿姨让女生看是哪本书把警报弄响了，女生把书倒出来，一本一本地测。阿姨见状急了，把书分成两份，第一份过了一下，响了。又把这一份分成两份接着测，三回就找到了。阿姨用鄙视的眼神看着女生，仿佛在说 O(n) 和 O(log n) 都分不清。 要说哪个算法的知名度较高，二分查找一定排得上号。以至于在面试的时候，如果应聘者简历写了擅长算法或者参加过ACM竞赛之类，我都会让他现场写一道二分查找的题目： // 已知array数组的元素为单调非递减，给定一个target，返回array数组里面 // 第一个大于或等于target的元素下标，如果没有合法结果则返回len(array) func FirstGreaterOrEqual(array []int, target int) int { // TODO: write your code here return len(array) } 一开始我觉得如果对方真的擅长算法，这题应该很基础并不算为难应聘者(纸上写只需要伪代码即可/电脑上写则需要通过编译)，直到几十轮面试之后几乎90%的人都在这道题上败北。 后来我认真反思了这个事情，发现从思路上来讲，绝大部分人都是没有问题的：用l和r来表示一个待查找区间，将区间中点m对应的元素同target相比较，根据比较结果来决定继续查找哪一半。也就是说，大家几乎都能写出如下的“骨架”： func FirstGreaterOrEqual(array []int, target int) int { // 初始化区间左端点： -1 || 0 || 1 ？ l := 0 // 初始化区间右端点： len(array)...
        </description>
        <pubDate>Fri, 08 Jan 2021 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2021-01-08-binary-search/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2021-01-08-binary-search/</guid>
      </item>
    
      <item>
        <title>斐波那契数列第一千万项怎么求</title>
        <description>
          
          众所周知，斐波那契数列的定义是：f(0) = 0, f(1) = 1, 且当 n &amp;gt; 1 时 f(n) = f(n-1) + f(n-2)。今天讨论个相对简单的问题：斐波那契数列第一千万项怎么求？ 暴力算法(V0) 最容易想到的是直接翻译递推式，二话不说撸出代码： func fibV0(n int) Number { defer logElapsed(&quot;fibV0&quot;)() a, b := Num1, Num0 for i := 0; i &amp;lt; n; i++ { a, b = NumAdd(a, b), a } return b } 很可惜当输入为...
        </description>
        <pubDate>Tue, 17 Dec 2019 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2019-12-17-fibonacci/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-12-17-fibonacci/</guid>
      </item>
    
      <item>
        <title>关于AVL树和红黑树的一点看法</title>
        <description>
          
          AVL树和红黑树是两类重要的二叉查找树，关于它们孰优孰劣，人们往往众说纷纭莫衷一是。本文从空间开销、实现难度和时间开销(最坏/均摊/期望)等多个维度进行了细致而具体的比较，最终得出结论：AVL树和红黑树在不同的场景下均有各自的微弱优势，但几乎不可能形成碾压。工程实践本身是个充满了 trade-off 的过程，与其武断(却毫无底气)地说哪一个更好，深入探究它们底层隐藏的奥秘恐怕会更有趣一些。 准备阶段：二叉树高度的定义 AVL树和红黑树的自身定义以及操作算法，教材或者网上都有非常多的资料，由于本文不是科普文，所以就不花大量篇幅去复制粘贴这些大家都能很方便查到的东西了。不过有一点特殊的需要指出，二叉树的高度总的来说能见到两种定义(以及若干种表述方式)： 根节点到其所有叶节点的最长路径包含的边数。 根节点到其所有叶节点的最长路径包含的节点。 这些定义(和表述)无本质区别也都没错，只是数值上可能会相差一点，但在讨论前最好能达成统一认识。我比较相信 Knuth 的审美，因此采用他在 TAOCP 里面推荐的描述方式。 上图中一棵二叉树由圆形的真实节点构成，我们将所有真实节点缺少孩子的地方均补上方形的虚拟节点，构成一棵扩充的二叉树。在这种语境下我们又称圆形节点为内部节点(数量为 n )，方形节点为外部节点(数量为 s )。顺带说一下它们之间有个显著的关系： s = n + 1 。在后面的讨论中我们就是针对这种扩充的二叉树来讨论，而且很多时候外部节点都可以省略不画。在这基础上重新定义树的高度(记为 h )：根节点到其所有外部节点的最长路径包含的边数。 准备阶段：二叉树高度的下界 高度为 h 的二叉树内部节点数为 n ，假设 n &amp;lt;= 2^h-1 ，利用数学归纳法： 当 h = 0 时，有 n = 0 &amp;lt;= 2^h-1 = 2^0-1 = 0 成立；...
        </description>
        <pubDate>Sat, 23 Nov 2019 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2019-11-23-avl-vs-rb/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-11-23-avl-vs-rb/</guid>
      </item>
    
      <item>
        <title>An Erlang Syntactic Sugar Library</title>
        <description>
          
          Motivation 我大概是在2014年的时候认识Erlang的，并且深深爱上了这个为我解决许多工作难题的”微型操作系统”。但在和别人的交流中，我也听到一些对于Erlang的抱怨，其中最多的竟然是关于它的语法！直到有一天我发现一个开源日志库Lager，里面有一个技巧就是利用编译参数parse_transform对源代码按照自己的意愿进行转换。于是我去查阅了parse_transform的相关资料，并且写下了这个项目ESugar：一方面它作为DEMO展示了原来利用parse_transform我们还可以做许多很酷的事情；另一方面你也可以把它当做一个Erlang的语法糖库拿去使用或者扩展。 Pipe Operator 很多语言(比如F#和Elixir)都支持管道操作符，它将前一个表达式的运算结果作为参数传给后一个函数(调用)，形成一个调用链。比如我们要用Erlang来实现一个md5函数的话，传统的写法也许会是这样： -spec md5(Data :: iodata()) -&amp;gt; Digest :: string(). md5(Data) -&amp;gt; _Digest = string:to_lower(lists:flatten(lists:map(fun(E) -&amp;gt; [integer_to_list(X, 16) || X &amp;lt;- [E div 16, E rem 16]] end, binary_to_list(erlang:md5(Data))))). 如果你厌倦了上面这种嵌套调用，那么我们将允许如下的管道写法，它和前一种传统写法是等效的： -spec md5(Data :: iodata()) -&amp;gt; Digest :: string(). md5(Data) -&amp;gt; _Digest = pipe@(Data ! fun erlang:md5/1 !...
        </description>
        <pubDate>Fri, 22 Nov 2019 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2019-11-22-erlang-sugar/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-11-22-erlang-sugar/</guid>
      </item>
    
      <item>
        <title>Y Combinator (续前一篇Lambda)</title>
        <description>
          
          这篇文章是对前一篇An Online Lambda Interpreter的续写，搞清楚 Y Combinator 这个概念的来龙去脉。当然这两篇文章里的 lambda 演算语法定义实际上并不是标准的定义，具体的标准定义与实现我在这个目录做了单独实现。 Y combinator 问题的提出 如果让你用自己最熟练的编程语言写一个fibonacci(暂不考虑效率问题)，这一定不是问题，下面是一个Python的例子： def fib(n): if n &amp;lt; 2: return n else: return fib(n-1)+fib(n-2) 我们现在考虑把它翻译成ELambda的语法： \n. (((cond ((less n) 2)) \_. n ) \_. ((add (fib ((sub n) 1))) (fib ((sub n) 2))) ) 当我们在考虑一个问题，如果不用关注其具体细节，可以把它们抽象一下，这样可以在一定程度上减轻思维负担。比如关于上面的整个函数体，本质上就是一个过程中包含了(fib x)这种形式的调用，因此我们可以把整个函数简记为： \n.&amp;lt;(fib x)&amp;gt; 现在问题一下就清晰起来了：fib是啥？在我们的理解中它应该就是这个函数自身，但是lambda演算中并没有绑定函数名这种说法。没关系我们可以通过传参的方式来绑定，这是lambda演算的一个常见技巧： (...
        </description>
        <pubDate>Wed, 20 Nov 2019 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2019-11-20-y-combinator/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-11-20-y-combinator/</guid>
      </item>
    
      <item>
        <title>知乎备份：一个有趣的点集最小直径问题</title>
        <description>
          
          在欧氏平面上排布n个点，这n个点两两之间距离的最小值和最大值的比值有无最大值？ 我的回答 我来做个搜索党，首先这个问题可以重新陈述为： 欧几里得平面上有 $n$ (其中 $n \geq 2$ )个点，它们两两之间的距离均不小于 $1$ ，用 $D(n)$ 表示这个点集的最小直径，求解 $D(n)$ 。 显而易见的结论： \[D(2) = 1\] \[D(3) = 1\] 1951年Paul Bateman和Paul Erdös在Geometrical extrema suggested by a lemma of Besicovitc中证明了： \[D(4) = \sqrt{2} \approx 1.414\] \[D(5) = \frac{1+\sqrt{5}}{2} \approx 1.618\] \[D(6) = 2\sin(\frac{2\pi}{5}) = \sqrt{\frac{5+\sqrt{5}}{2}} \approx 1.902\]...
        </description>
        <pubDate>Mon, 18 Nov 2019 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2019-11-18-zhihu356184280/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-11-18-zhihu356184280/</guid>
      </item>
    
      <item>
        <title>知乎备份：Joe老爷子离开的一点感慨</title>
        <description>
          
          Erlang 之父 Joe Armstrong 逝世，如何评价他在计算机领域的贡献？



我的回答
我只是一个半路出家的程序员，可能没有能力评价Joe老爷子在计算机领域的贡献，不过可以从自己的角度出发讲讲Erlang究竟带给了我什么。

在大二的时候我被模拟电路、数字电路、信号与系统、通信原理、数字信号处理……这一大堆专业课程折磨得要死要活，自己对它们一点也不感兴趣，产生了“叛逃”的念头。那个时候学校的ACM/ICPC集训队正好在面向全校举办讲座，过去听了几场而且跟着练了一阵子的题，发现原来写代码的感觉是如此地酸爽。虽然没有继续加入校队跟神牛们一起搞竞赛，但是后面的几年时间学习之余我都一直“不务正业”去各种OJ刷题。当时其实并不知道将来能做什么，隐隐约约觉得以后可能会写代码吧，但具体是个什么目标我也说不上来，整个人活得就像个梦虫儿一样。

到了毕业季才发现自己的菜，除了会刷几道简单或者中等的算法题以外，别的什么都不会。不懂什么是进程线程，不懂什么是网络编程，不懂数据库的基本使用……我都怀疑自己是不是选错了路自己压根就不适合。这些问题其实很基础并不难搞懂，当初自己为什么这么菜确实是不可思议，但是对于一个半路出家而且技能树点偏的人来说，它确确实实就这么发生了。最后一家眼瞎公司收了我，公司的手游服务器是用Erlang来写的。于是一边学Erlang一边码业务，我慢慢开启了程序员的入门之旅。

Erlang在一开始给我的印象是“自然”。什么是进程什么是线程？不管，反正我先spawn出来一个东西，它能够执行你写的一个函数，你可以给它发消息它可以给你发消息。然后你可以不停地spawn一大堆的东西它们之间靠着消息传递相互协作最后完成任务。这就是我这种菜鸟一开始的认识，虽然很浅显，但是完全无障碍而且很自然。好像你什么都不需要做，自然而然就会写并发程序了，代码是有些烂，但是上线以后你发现它竟然就神奇的支撑起了单机七八千的玩家在线。总之我还是不懂什么是并发编程，但是以Erlang这个点为突破口，后面再去了解什么是Actor，什么是CSP，怎么fork怎么new thread，怎么做互斥同步，就发觉不像上学时候那么懵逼了。再到后面我学会了很多写并发的套路，但还是觉得Erlang写起来最自然。

Erlang接下来给我灌输的观念是“容错”。在接触Erlang以前，我也尝试着思考过代码出错该怎么办，然而越想越发现这是个极其复杂的问题。我该返回错误码还是抛异常呢，即使返回了错误，我的调用者又该怎么处理呢？甚至更可恶的是还有一些错误压根就不在你的掌控范围以内，那么它们发生了又该怎么办呢？而我从Erlang学到的并不只是表面上的”let it crash”，而是你在设计任何系统的时候都要考虑以什么样的方式监控每一个子系统，它们出问题的时候又应该以什么样的策略重启。而我一开始纠结的那些问题并不是真正的核心，当你有心里对系统的容错有具体地方案以后，你就很容易知道在哪些地方应该做检查哪些地方不应该，哪些地方应该抛出异常哪些地方不应该。

Erlang然后也将我引进了“FP”的大门。一开始写Erlang的时候是很别扭的：凭什么不能让我修改一个变量的值？连循环都没有你让我怎么写代码？不过写得多了也就慢慢习惯了，觉得本来不就该如此吗？其实Erlang里面真正的函数式特性并不算多，不过顺着这个藤再摸后面的瓜就容易多了。学Erlang以前我也看Haskell，看不懂，学了以后再去看，至少能看懂那么一点点了。
一晃工作了四五年，项目也做了四五个，工作之余也会挑一部分源码来读，在这个过程中越来越觉得Erlang就是一个取之不尽用之不竭的大宝库。也看过一些比较出名的项目比如Nginx和Redis之类，发现还是Erlang的源码难啃啊，毕竟说Erlang是个微型操作系统也不为过。

现在已经不写Erlang了。后面我接触过不少概念：Golang、区块链、微服务、docker、k8s……发现想要上手它们其实并不难，而且能够很自然地找到具体的路径，不像刚毕业时那种懵逼状态了。我想我这个半路出家的程序员应该算是真正入门了吧，而这个过程是在Erlang的陪伴下完成的。

感谢Joe老爷子创造了Erlang。

        </description>
        <pubDate>Sun, 21 Apr 2019 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2019-04-21-zhihu321011169/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-04-21-zhihu321011169/</guid>
      </item>
    
      <item>
        <title>知乎备份：线性时间能求出逆序对数吗</title>
        <description>
          
          如何用 O(n) 的时间复杂度求数组的逆序对数 ? 以前听某个神牛说有 O(n) 算法的，但是好像没有流传的资料。。 我的回答 先说结论：基于比较操作的统计逆序对算法最坏时间复杂度下界是 $\Omega(n\log{n})$ 。 解决这个问题最核心的一点是理解一次 “比较” 操作的本质是什么，我先放一张大图 为了讨论方便起见，我们不妨假设数组 $a_1$ , $a_2$ , $a_3$ , $…$ , $a_n$ 中没有重复元素，那么它们的相对大小关系一共就有 $n!$ 种可能排列。而对于一次 “比较” 操作，本质上就是把当前的的可能排列划分成了两部分。因此想要确定最终排列，就必须反复 “比较” 来划分可能排列，选择相应的分支继续直至可能排列只有一种。这是算法导论上的内容，它由此证明了基于比较操作的排序算法最坏时间复杂度下界是 $\Omega(n\log{n})$ 。 对于排序来说这是很直观的，因为它的解空间大小就是 $n!$ 。但统计逆序对却没有那么显然，因为它的解空间大小是 $n(n-1)/2 + 1$ ，我们很有理由质疑说万一我不需要将可能排列划分到只有一种呢？也就是说有没有可能划分解空间到某一步以后，当前的所有可能排列(大于一种)都对应着相同的逆序对数，那样我们也就可以不用继续划分而直接返回这个统一值就好了？ 很遗憾这是不可能的，参考我放的大图。“比较” 操作其实对应了对可能排列(解空间)的划分，而这个在图上看就是从根节点一层一层往下走。椭圆形节点代表了当前的可能排列还能继续划分，长方形节点代表了已经确定好了最终排列。这里值得注意的是绿色的椭圆形节点，如果我们把所有椭圆形节点看作一棵树的话，那么白色的椭圆形节点是内部节点，而绿色的椭圆形节点则是叶子节点。 那么对于每个叶子节点，一个很明显的特征就是它包含了两个可能排列，并且可以通过一次比较来划分它们。更重要的是，这两个可能排列的逆序对数相差了一个，它们不可能拥有相同的逆序对数。因此不管你停留在哪一个椭圆节点，都不可避免地包含至少一个叶子节点，也就不可避免地包含两个逆序对数不相等的可能排列，你就得继续往下划分。最终也就证明了想要基于比较操作来统计逆序对，最坏时间复杂度下界和排序一样，也是 $\Omega(n\log{n})$ 。 最后补充说一点，通常见到的统计逆序对算法(譬如扩展归并排序、扩展二叉搜索树或者树状数组)不单单能统计一个总的逆序对数，还能分别统计以每个元素开头的逆序对数，这是一个更强的结果，因为知道了后者，就必定能在线性时间复杂度内将原数组排序。 至于 $O(n)$ 复杂度的传言，我大致去搜索了一下应该是这样的： 1989年，Dietz...
        </description>
        <pubDate>Tue, 19 Mar 2019 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2019-03-19-zhihu27209480/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2019-03-19-zhihu27209480/</guid>
      </item>
    
      <item>
        <title>知乎备份：如何在七天之内学会Erlang</title>
        <description>
          
          如何在七天之内学会Erlang?
众所周知，上海的冬天实在是太冷了。

我穿棉袄开空调盖两层被子却依然每天都冻得瑟瑟发抖。

所以我决定学Erlang, 年后去温暖的广州做页游服务端。

那么，我应该如何在七天之内学会Erlang呢？

求教，真诚的。



我的回答
“学会”是个比较模糊的概念，有人把erlang/otp源码通读了一遍依然满脑子都是问题，有人只是随便百度百科了一下大致概念就觉得自己已经掌握了。不过看问题其实你是想速成然后去广州写页游服务器，这样就简单很多了，即便对于一个从来没有接触过函数式编程的人来说七天也完全足够了。下面是我建议的一条学习路线：

第一天和第二天：找一本基础的入门书籍通读，了解Erlang的基本语法和概念（诸如变量绑定、模式匹配、高阶函数、进程并发、消息队列、OTP架构、ETS表、Mnesia数据库、分布式节点等等等等）。内容看着很多，其实你真正着手看的话就会发现，一边看一边打开Erlang Shell实践，两天时间完完全全足够了。这里随便推荐两本：

第一本是Erlang之父Joe老爷子写的 Programming Erlang，这本书的地位就相当于C语言里面的 K&amp;amp;R；至于另一本则是 Learn You Some Erlang for Great Good，学过Haskell的朋友是不是对这个书名有种似曾相识的感觉？这本书的作者就是看了Haskell那本书后兴奋地难以入睡，觉得好的入门书就该当如此然后自己写了本Erlang的。

反正这两本书随便挑一本看看就可以了，没必要把它们（甚至其它更多的入门书）都看一遍。

第三天和第四天：好了，有了前两天的语言基础，你就可以尝试着自己写点儿东西练手了。因为Erlang本身就是为解决服务器并发编程设计的，而你正好要想用它去写游戏服务器。所以你不妨徒手撸一个类似QQ这样的聊天服务器试试，比如实现用户的注册登录好友群聊等功能。为什么不直接撸一个游戏服务器呢，因为没有客户端的话玩起来太无聊了，还不如聊天更直观。在这两天的练习之中，你一定会遇到很多比较棘手的点，随着它们的解决，你对之前书上的一些知识点会有了进一步的理解。

第五天和第六天：有了理论基础以及亲身实践，你还缺什么呢？缺真正的工程实践。公司里一个真正的Erlang项目(对你来说就是游戏公司)应该是什么样子的呢？它们的代码怎么组织、数据库怎么连、日志怎么打、配置怎么读……等等等等，这里推荐两本书：

Erlang and OTP in Action 和 Erlang in Anger，前一本对Erlang的核心框架OTP讲解得比较深入，看了它你之前的不少疑问都会有醍醐灌顶的感觉，后一本的话则是实打实的实战经验了，可以说是老司机把他在项目中积累的一些非常有用的知识点手把手教会你。

当然只看这两本书是不够的，你可以配合这套远古级别的Erlang页游服务器框架源码(MGEE)一起看，然后和书本里的知识相互佐证。为什么说它远古，因为后面几年全国各地的Erlang游戏服务器源码基本上都是发源于这一套，虽然大家根据自己的业务情况做了不同的优化，基本上随便哪一套拿出来也许都能屌打庆亮他们这套。但是这套作为开山祖师的地位是不可动摇的，而且就我后面看过的四五套别的游戏源码，发现框架还是差不多的。

这两天的任务可能稍微比较重，所以你必须能够抓住重点，捡一些至关重要的看，否则是不太可能完成的。

第七天：最后一天，什么也别干了，放松一下紧绷了六天的神经，网上到处浏览浏览博客之类的吧。看看别人的一些经验之谈，来对自己之前建立的知识结构进行查漏补缺。这里我推荐两个人的博客：

坚强2002  坚强哥的博客对新手入门帮助很大，他以自己超强的毅力记录了自己从零开始学习使用Erlang的一点一滴，而且每一篇都不是生搬照抄，而是自己真真实实的思考和总结；

系统技术非业余研究 因为余锋本身就是阿里的大佬，其水平比一般人要高出很多，所以他的博客会相对艰深不少，但是如果能够看懂某些篇章，对你的帮助应该是挺大的。

第七天以后：七天的时间足够你了解Erlang并且用它在实际项目组写一些简单业务逻辑了，但是想在七天内精通Erlang应该还是很难办到。那么七天以后应该做什么呢，如果你以后还想继续学习，我的建议是多看一些开源的Erlang项目。比如 cowboy、lager、rabbitmq 之类的，看看别人是怎么用Erlang来做项目的，中间有不懂的问题，网上都无法解决的，不妨直接去扒erlang/otp源码，源码下面无秘密。顺便最近区块链炒的异常之火，这里安利一个用Erlang开发的区块链项目 aerernity/epoch，做这个项目的技术人员都是一群经验丰富的Erlang大佬，如果看好的话赶紧囤上一些AE币吧，长期持有别管短线，赚了不说，赔了就当是购买情怀吧




        </description>
        <pubDate>Mon, 12 Feb 2018 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2018-02-12-zhihu267031426/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2018-02-12-zhihu267031426/</guid>
      </item>
    
      <item>
        <title>知乎备份：抛硬币游戏蕴含的博弈论</title>
        <description>
          
          两个人一人一枚硬币，如果同正，我给你三块，如果同负，我给你一块，如果一正一负，你给我两块，公平吗？ 我的回答 先说结论：不公平。 因为如果你以 $3/8$ 的概率出正面，那么如果我出正面，我的期望收益是： \[\frac{3}{8} \cdot 3 + \frac{5}{8} \cdot (-2) = -\frac{1}{8}\] 相反如果我出反面呢，我的期望收益是： \[\frac{3}{8} \cdot (-2) + \frac{5}{8} \cdot 1 = -\frac{1}{8}\] 也就是说如果你选择了上述的出币策略，不论我怎么出，我的期望收益都是负的。 这个 $3/8$ 怎么算出来的呢？用 $(p, q)$ 表示我以概率 $p$ 出正面你以概率 $q$ 出正面。同时用 $U(p, q)$ 表示我的期望收益， $V(p, q)$ 表示你的期望收益。那么有没有一种稳定的组合 $(p^{*}, q^{*})$ 使得我们之间任何一个人都不可能单方面通过改变策略来提高期望收益呢，如果有，用数学的语言描述就是： \[U(p^*, q^*) \geq U(p, q^*)...
        </description>
        <pubDate>Mon, 05 Feb 2018 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2018-02-05-zhihu266656338/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2018-02-05-zhihu266656338/</guid>
      </item>
    
      <item>
        <title>知乎备份：绝地求生赢家的期望人头</title>
        <description>
          
          n个水平相同的人参加大逃杀游戏，最后赢家所消灭玩家数的期望值是多少？ 记X为每局最后胜利者在本局游戏中所击杀的玩家数，求X的期望。 在以上条件下X的期望可求吗？如果不可求，那么至少要补充什么条件？ 在X期望可求的条件下，“玩家水平相同”的严格定义是什么呢？ 我的回答 如果只建个相对简单的模型还是可以分析的（至于真实情况分析起来可能要麻烦不少，比如说你杀了别人舔包增强你的装备补给会提升胜率，但是和别人交火中受伤之类的又会降低胜率，如果要建立合理的模型恐怕是比较复杂的了）。 $k$ 个玩家水平相同，到游戏结束只剩 $1$ 人，那么中间过程有 $k-1$ 步，每一步都是从剩下的所有人中随机选择两人，让第一个人消灭掉第二个人即可。 现在来考虑期望问题，我们用符号 $E_k$ 表示只有 $k$ 个玩家时最后赢家消灭玩家数的期望，不难得到如下递推式： \[E_{k}=\frac{1}{k-1} \cdot (E_{k-1}+1)+\frac{k-2}{k-1} \cdot E_{k-1}\] 这个递推式可以这样理解：当有 $k$ 个玩家时，我们从最后赢家的角度考虑，在接下来的一步中首先要选出一个被害者，只能在其余的 $k-1$ 个人中挑一个，接着才是挑选杀人者，赢家被挑中的机会是 $1/(k-1)$，就有了上述公式。将公式整理一下得到： \[E_{k}=\frac{1}{k-1} + E_{k-1}\] 结合边界条件不难求得： \[E_{n} = \sum_{i=1}^{n-1}{\frac{1}{i}}\] 最后跑了个模拟和理论的对比图做了下验证： # -*- coding:utf-8 -*- from random import randrange import matplotlib.pyplot as plt def simulate(n):...
        </description>
        <pubDate>Thu, 01 Feb 2018 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2018-02-01-zhihu266401948/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2018-02-01-zhihu266401948/</guid>
      </item>
    
      <item>
        <title>记一道有趣的CodeReview题目</title>
        <description>
          
          这道题目是在千里码(不知道这野鸡网站现在还在不在)，大意就是这段代码会因为某个特殊的随机种子而抛出异常，需要你找到这个随机种子。 如果只是想要过题 这也是我写这篇学习资料的初衷，如果只是想要过题的话这再简单不过了。根据题目的描述，是由于seed变量取了某个特殊值导致了异常。那么最直接的方法：遍历seed的所有可能取值不断try..catch，接着坐下来喝杯咖啡估计结果就出来了。我相信稍微有点编程基础的人都能够顺利解决，但是仅止步于此的话未免太可惜了。 是哪里抛出的异常 好好好现在我们提高一下自身追求，去找找异常究竟是哪里抛出的。从main函数入口一步一步往下看：第一个可能抛异常的是 new Gen()，但立即可以发现这是不可能的，因为Gen并没有自己实现构造函数；那么第二个可能抛异常的就应该是rand.srand(seed)了，因此需要去检查srand函数，检查后发现这也是不可能的；排除了前两种可能，异常的抛出点只可能是在arr[rand.next()%100]这里了，因此可以确定很大可能是数组下标越界。 由于数组下标越界，其实我们完全可以很武断地认为rand.next()%100是个负值，然后就判定是rand.next()返回了负值。关于这道题这种判断确实是对的，但未必所有的语言都是这样其实，举一些例子（当然你还可以去尝试更多）： // JavaScript =&amp;gt; -1 console.log((-11) % 5); -- Lua =&amp;gt; 4 print((-11) % 5) -- Haskell =&amp;gt; 4 show $ mod (-11) 5 # Python =&amp;gt; 4 print((-11) % 5) % Erlang =&amp;gt; -1 io:format(&quot;~w~n&quot;, [(-11) rem 5]). -- MySQL =&amp;gt; -1...
        </description>
        <pubDate>Sat, 16 Dec 2017 00:00:00 -0500</pubDate>
        <link>https://rbee3u.github.io/2017-12-16-code-review/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2017-12-16-code-review/</guid>
      </item>
    
      <item>
        <title>知乎备份：8个小球用天平称16次排序</title>
        <description>
          
          八个重量不相等的小球，每次只能放两个球到天平上，现如何只用16次将这八个球重量排序？ 16次是可行的，因为8个球最多有8！中排序。然而测量16次，最多产生2^16种分支。8！&amp;lt; 2^16，故理论上可行。但是究竟要如何去称？ 我的回答 很有意思的一个问题，通过16次比较确实是有解的，《计算机程序设计艺术》(俗称TAOCP)里面提到了一种叫做”合并插入排序”的方法： 首先将8个小球划分成4对，通过 4 次比较我们可以得到 $b_1 \leq a_1, b_2 \leq a_2, b_3 \leq a_3, b_4 \leq a_4$； 对 $a_1$, $a_2$, $a_3$, $a_4$ 排序，通过 5 次比较（这里应该没有问题，用归并排序就可以），假设最后我们得到 $a_1 \leq a_2 \leq a_3 \leq a_4$，此时集合 $\left\{ b_1,a_1,a_2,a_3,a_4 \right\}$ 的所有元素顺序确定; 现在考虑插入 $b_3$ (对的你没看错，不是 $b_2$ )，因为 $b_3 \leq a_3$， 所以只需二分查找它在集合 $\left\{ b_1,a_1,a_2...
        </description>
        <pubDate>Sun, 29 Oct 2017 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2017-10-29-zhihu67325258/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2017-10-29-zhihu67325258/</guid>
      </item>
    
      <item>
        <title>知乎备份：两个百万富翁如何安全比富</title>
        <description>
          
          两富人比谁钱多。如何能实现互相保密但可以比出谁钱多？ 两个富人想比较谁的钱多，他们之间可以发送任何信息，但不能告诉对方自己有多少钱。比较苛刻的是，他们也不能说自己大致有多少钱（诸如大于1000小于2000）。请问要如何实现？方法是否有优劣？这种保密的算法或者通信方法应用前景如何？ 我的回答 姚期智在1982年提出的百万富翁问题应该是开创了安全多方计算的先河，后面涌现了茫茫多的协议算法，比较常见的是利用同态加密来实现。 举个实际例子，比如Paillier加密算法就具有比较好的同态属性： \[E(m_1,r_1) \cdot E(m_2,r_2) = E(m_1+m_2,r_1+r_2)\] \[E(m,r)^C=E(m \cdot C,r \cdot C)\] 我们可以用它来设计具体的比富协议(假设 Alice 财富是 $a$ ，Bob的财富是 $b$ )： 第一步：Bob生成两个非常大的随机正整数 $x$ 和 $y$ ，但是并不公开只有他自己知道； 第二步：Alice生成一对属于自己的密钥(公钥是pub，私钥是pri)，用公钥加密自己的财富的到 $E(a)$ ，并将它和公钥一起公布出去； 第三步：Bob得到Alice公布出来的数据以后，首先用Alice公钥计算出 $E(b \cdot x+y)$，然后用Paillier算法的同态属性计算出 $E(a \cdot x+y) = E(a)^x \cdot E(y)$，并将这两个结果也公布出去； 第四步：Alice得到Bob公布出来的计算结果以后，用自己的私钥分别反解出 $A = a \cdot x+y 和 B =...
        </description>
        <pubDate>Wed, 18 Oct 2017 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2017-10-18-zhihu66376147/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2017-10-18-zhihu66376147/</guid>
      </item>
    
      <item>
        <title>An Online Lambda Interpreter</title>
        <description>
          
          在计算机装逼界我们经常能听到类似这样的对话 师妹：师兄，你说是不是所有非递归算法都能写成递归形式呀？ 师兄：对啊，否则的话图灵机和lambda演算岂不是不等价了！ 其实对于大部分人而言lambda演算这个概念并不算陌生，很多现代的编程语言(C++/Java/Python/…)都或多或少地支持一些函数式编程特性。因此当你在讨论匿名函数的时候，其实你已经在和lambda演算打交道了。可以这样说，是lambda演算构成了函数式编程的基石。 那么究竟什么是lambda演算呢，我们可以用Backus-Naur Form来定义它的文法: &amp;lt;expression&amp;gt; ::= &amp;lt;name&amp;gt; | &amp;lt;function&amp;gt; | &amp;lt;application&amp;gt; &amp;lt;function&amp;gt; ::= λ&amp;lt;name&amp;gt;.&amp;lt;expression&amp;gt; &amp;lt;application&amp;gt; ::= (&amp;lt;expression&amp;gt; &amp;lt;expression&amp;gt;) 当然你可能会问&amp;lt;name&amp;gt;怎么定义的呢，好吧其实这只是个无关紧要的问题，更为详细的关于lambda演算的资料可以参考 Lambda calculus。等你搞清楚 α-conversion、β-reduction、η-conversion、currying 、call-by-name、call-by-value 这些概念后，你也可以自豪地向别人吹逼说自己”精通”lambda演算了。 不过在吹逼之前可不可以先帮我解决一个问题？ 问题是这样的：作为函数式编程语言学家(渣)的 Harry Harte 自从学习了lambda演算后心潮澎湃久久不能平静，于是花了一下午的时间发明了lambda编程语言并且强行撸了一个call-by-value的lambda语言求值器。同样可以用Backus-Naur Form来定义它的文法: &amp;lt;expression&amp;gt; ::= &amp;lt;name&amp;gt; | &amp;lt;function&amp;gt; | &amp;lt;application&amp;gt; &amp;lt;function&amp;gt; ::= \&amp;lt;name&amp;gt;.&amp;lt;expression&amp;gt; &amp;lt;application&amp;gt; ::= (&amp;lt;expression&amp;gt; &amp;lt;expression&amp;gt;) 可以发现唯一的一个区别是λ &amp;lt;-&amp;gt; \，这个主要是为了让我们可以直接用ascii来编码。为了把大家从繁琐的低级构建任务中解放出来，勤劳又勇敢的 Harry...
        </description>
        <pubDate>Thu, 07 Sep 2017 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2017-09-07-lambda-qlcoder/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2017-09-07-lambda-qlcoder/</guid>
      </item>
    
      <item>
        <title>知乎备份：斗地主至少一人有炸弹的概率</title>
        <description>
          
          斗地主一局中至少有一人有至少一个炸弹的概率是多少？ 斗地主大家都会的吧，一副扑克三个人玩，四个相同的或两张王都算炸弹。那么斗地主一局中至少有一人有至少一个炸弹的概率是多少？qq游戏出现炸弹的概率是不是故意调大了？ 我的回答 斗地主一局中炸弹出现的概率(≈0.544668)确实挺大，下面是我的推导思路(动态规划)： “至少一人有炸弹”与“每个人都没炸弹”互为对立命题，方便起见我们先求“每个人都没炸弹”的概率。 假设当三个玩家(A、B、C)的座次选定，一副牌随机打乱发完时，A得到1-17号、B得到18-34号、C(地主)得到35-54号。现在不妨把这个发牌方式转化为另一种等效的方式：先发4张1，从A、B、C的54个牌位中随机挑选4个位置，再发4张2，从剩下的50个排位中随机挑选4个位置……因此整个发牌过程就划分成了14个阶段(13*4普通牌+1*2Joker)。 现在我们需要每次发的4张普通牌(或者2张Joker)不能落到同一段(1-17、18-34、35-54)，因为同一段的牌位属于同一个人，这样就构成炸弹了。 定义$d_{ijk}^{(m)}$表示在第$m$阶段、A获得$i$张牌、B获得$j$张牌、C获得$k$张牌时没有出现炸弹的概率，那么在阶段与阶段之间应该有如下递推公式： \[d_{ijk}^{(m)} = \sum p_{(i'j'k' \rightarrow ijk)} \cdot d_{i'j'k'}^{(m-1)}\] 这里的$(i’,j’,k’)$是$m-1$阶段可以转移到$m$阶段$(i,j,k)$的合法状态，$p_{(i’j’k’ \rightarrow ijk)}$是其对应的转移概率，再注意考虑好边界条件应该就可以做了。 其实你仔细观察会发现，表示阶段这个上标实际上是多余的，在我们重新确定的发牌过程中$(i,j,k)$本身就足以定义好状态了，所以上面的公式可以重新改写成下面这个最终版： \[d_{ijk} = \sum p_{(i'j'k' \rightarrow ijk)} \cdot d_{i'j'k'}\] 简单写了段C/C++的求解代码： #include &amp;lt;iostream&amp;gt; #include &amp;lt;vector&amp;gt; const double eps = 1e-9; // 浮点数允许误差 const int PLAIN_ROUND = 13; // 普通发牌回合数 const int TOTAL_ROUND...
        </description>
        <pubDate>Mon, 06 Oct 2014 00:00:00 -0400</pubDate>
        <link>https://rbee3u.github.io/2014-10-06-zhihu25405635/</link>
        <guid isPermaLink="true">https://rbee3u.github.io/2014-10-06-zhihu25405635/</guid>
      </item>
    
  </channel>
</rss>
