Random Tech Thoughts

The title above is not random

Cashback

很久没有看电影了,昨天才看了 Cashback,感觉不错。

很喜欢影片的叙事手法,拍摄也很不错。主人公的独白也许有人会觉得罗嗦,但是我很喜欢。配角十分搞笑。影片最后男女主人公终于走到一起那段十分浪漫。不多说什么了,网上对这部电影的介绍不少,豆瓣的评论也很多。记录下影片最后的那段话吧。

Once upon a time, I wanted to know what love was. Love is there if you want it to be. You just have to see that it’s wrapped in beauty and hidden away between the seconds of your life. If you don’t stop for a minute, you might miss it.

现在喜欢看平淡但有些内涵的电影,纯粹视觉享受的美国大片实在腻了……

Fibonacci 数列 续

先记录一段流水帐,要不然会不记得自己是怎么找到这些东西的,没兴趣看流水帐的跳过。

我想看看用 Common Lisp 怎么写插入法排序(我对语言本身还不熟悉,所以想看别人怎么写的),于是 Google “common lisp insertion sort”,来到了这里,LiteratePrograms,一个记录了很多程序的 Wiki,很不错的地方。看了一会儿就想也许 Fibonacci 数列的算法也会有,于是 Google “common lisp fibonacci”,这次同样也有 LiteratePrograms 上面的文章出现在结果中,不过位置比较靠后,而且上面的算法也很普通。第三个结果来自 CLiki,文章在这里,这里的算法除了那个 generator 以外我在上一篇文章里面都有给出,包括那两个 Θ(lg(n)) 的算法。而最后在 LiteratePrograms 上逛的时候又看到了一个用矩阵乘法的算法。

利用 generator 来求 Fibonacci 数列

利用 generator 的算法比较特别,有点像 continuation。看过 Wikipedia 的解释以后才知道什么是 generator,而 continuation 可以用来实现 generator。代码如下:

(let ((a 0)
      (b 1))
  (defun fib ()
    (prog1 a (psetf a b b (+ a b)))))

这里的实现使用了 closure,let 返回了一个函数 fib,fib 中使用到的 a, b 在 let 创建的环境中,每一次调用 fib 都会返回 Fibonacci 数列中的下一个值,并且更新环境中的 a, b。prog1 对第一个 form 求值,然后对后面的 form 进行求值,最后返回第一个 form 的值。而 psetf 在赋值之前对所有的需要求值的 form 进行求值,然后在赋值,这样就避免了要使用一个变量来记录 a 的值的需要。

这个算法只是用函数的形式表达迭代而已,除此以外没有什么特别的。但是注意使用 generator 我们相当于得到了一个长度无限的 Fibonacci 数列,我们方便地可以控制迭代的过程,在需要的时候才进行迭代。

关于 closure 再多说两句。这个实现里的 fib 函数实际上带有自己的状态,其状态就是用 let 定义时候的 a,b 变量,注意这两个变量在函数之外是无法访问到的。使用 C++ 当然也可以实现相同的效果,但是你得定义一个类,里面封装这两个状态变量,然后重载括号符,使这个类成为一个 functor,实在是麻烦,有兴趣的可以去 Wikipedia 的 generator 条目里面看看 C++ 的实现。而使用 closure 的话,创建带状态的函数一切都是那么自然,不知道 Java 7 里面最终是否会加入 closure。

Dijkstra 的笔记,In honour of Fibonacci

在 CLiki 上发现我所使用的 Θ(lg(n)) 的算法原来 Dijkstra 也有给出过,在他的一篇名为 In honour of Fibonacci 的笔记中记录着。去看了看他的笔记,原来我用的算法和 Dijkstra 记录下来的一样,不过他在计数的时候从 1 开始而不是从 0 开始。(Dijkstra 是不是也是数学家?)Dijkstra 还把这个关系式泛化了,不过有几处推导过程没有看懂,也不知道泛化以后有什么用。

用矩阵乘法求 Fibonacci 数列

另外在 LiteratePrograms 上看到求 Fibonacci 数列的 Ruby 版本中,有一个使用矩阵的算法。方法是对矩阵 ((1 1) (1 0)) 做 n – 1 次乘方,即可得到 Fibonacci 数列第 n 项。代码如下:

require 'matrix'

FIB_MATRIX = Matrix[[1,1],[1,0]]
def fib(n)
    (FIB_MATRIX**(n-1))[0,0]
end

这个算法的优化就可以交给矩阵乘法去做了,矩阵乘法如果是 Θ(lg(n)) 的时间复杂度那么这个算法也是 Θ(lg(n)) 的时间复杂度了。

后来又到 Wikipedia 上搜了一下,在 Fibonacci number 条目下有关于 Fibonacci 数列的非常之多的数学关系和等式的证明,还列举了非常多的参考资料,包括 Dijkstra 的笔记。矩阵形式在上面也有,而且通过矩阵乘法的性质很容易的推导出了复杂度为 Θ(lg(n)) 算法中利用的等式。众人编辑的力量果然强。

为什么不直接用通式求 Fibonacci 数列的第 n 项

Fibonacci 数列的通式我当然知道,但是直接使用它来求数列的第 n 项在精度上会出现问题。通式中有无理数,因此计算过程中会产生误差。根据 CLiki 上的说明,在 CLISP 2.33.2,32 bit 的机器上,对单精度浮点数,当 n < 32 时,使用 truncate 可以得到正确结果;对双精度浮点数,n < 76 时,使用 rounding 可以得到正确结果。超出这个范围都将在整数级别上出现错误。

除了计算上的问题,我觉得这个问题本身也比较有意思。就像 MIT 的教授在上算法导论时说的,”Speed is fun!“,我也这么觉得。而且,当发现自己做着 Dijkstra 曾经也做过的事情的时候还是会小有成就感的。

求斐波那契 (Fibonacci) 数列第 N 项的算法

最近看算法导论的习题时联想到 SICP 的一条习题后想到的,折腾了下,得到了几种算法,不同的算法时间复杂度有 Θ(an), Θ(n), Θ(lg(n)) 几种。时间复杂度为 Θ(lg(n)) 的算法 比较有意思。下面一个个说,具体算法用 Common Lisp (CL) 实现,至于为什么用 CL 我在文章最后说明。

约定

符号 ^ 表示乘方运算。F(n) 表示 Fibonacci 数列的第 n 项,n 从 0 开始。由此, Fibonacci 数列可以由如下定义给出:

F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2)

这个定义本身是递归的。要是可以用在 Wordpress 里面使用 LaTeX 输入数学公式的插件就好了,下文中还会出现 _ 来表示下标,这些习惯跟 LaTeX 中数学公式的输入一样,不过看起来就会比较丑了。

最简单的递归定义和迭代实现

递归算法

根据定义可以很方便的得到一个求 F(n) 的算法:

1
2
3
4
5
(defun fibonacci-rec-simple (n)
  (cond ((= n 0) 0)
  ((= n 1) 1)
  (t (+ (fibonacci-rec-simple (- n 1))
              (fibonacci-rec-simple (- n 2))))))

defun 定义一个函数,参数是 n,cond 类似于 C 中的 switch,t 表示真,函数体的最后 一个 form 作为返回值返回。另外注意数学表达式都是前缀表达式的形式。就算不懂 CL 应该也能看懂吧。

这个递归的版本应该是最容易写的了,直接翻译定义就行。不过可惜的是这个算法因为太多的重复计算导致时间复杂度非常大,计算 F(n) 时需要计算 F(0), F(1) 共 F(n + 1) 次( 可以用数学归纳法很容易的证明),而 F(n) 是最接近 ((1 + sqrt(5))/2)n/sqrt(5) 的 整数,所以这时计算 F(n) 的时间复杂度为 Θ(an)。在我的笔记本上,当 n = 50 时,我就已经不愿意等待到结果输出了。

迭代算法

如果定义变换 T(a, b) 为:

T(a, b) = (a + b, a)

那么,从 (1, 0) 开始,进行 n 次变换后,序对中的第二个元素就是 F(n) 了。由此可以 得到求 F(n) 的迭代算法:

1
2
3
4
5
(defun fibonacci-iter-simple (n)
  (loop repeat (+ n 1)
     for b = 0 then a
     and a = 1 then (+ a b)
     finally (return b)))

这个算法的时间复杂度为 Θ(n),没什么好说的。稍微解释下语法吧,loop 是 CL 中构造循 环的一个宏,原来我是用 do 来进行循环的,但发现用 loop 更容易让人看懂,所以这里就写了个 loop 的版本。

这一段不少内容抄袭了 SCIP 1.2.2 中的分析,尽情鄙视我吧。

对递归定义的变形提高算法性能

抱歉,上面的内容没有什么新意,非原创。下面的应该会有点新意了。

最初的递归定义每次递归都只是将问题规模从 n 变到 n – 1 和 n – 2,我的想法是把递归定义进行变换,使得每次递归能够将问题规模一下子减小许多。因此我不断将 F(n) 进行递归展开,

F(n) = 1 * F(n - 1) + 1 * F(n - 2)
     = 2 * F(n - 2) + 1 * F(n - 3)
     = 3 * F(n - 3) + 2 * F(n - 4)
     = 5 * F(n - 4) + 3 * F(n - 5)
     = 8 * F(n - 5) + 5 * F(n - 6)
     = ...
     = F(p + 1) * F(n - p) + F(p) * F(n - (p + 1))

上面的关系式可以很容易使用数学归纳法证明。很自然的想到希望能够在每一次迭代的时候将问题的规模减小到原来的一半。

当 n 为奇数时,令 p = (n – 1) / 2,则:

F(n) = F((n + 1)/2)^2 + F((n - 1)/2)^2

当 n 为偶数时,令 p = n / 2,则

F(n) = F(n/2 + 1)*F(n/2) + F(n/2)*F(n/2 - 1)
     = F(n/2)^2 + 2*F(n/2 - 1)*F(n/2)

上面两个式子合起来可以写出下面的形式:

F(2*n) = F(n)^2 + 2*F(n)*F(n - 1)
F(2*n + 1) = F(n + 1)^2 + F(n)^2

有了上面的递归定义,可以得到下面的算法:

1
2
3
4
5
6
7
8
(defun fibonacci-rec (n)
    (cond ((= n 0) 0)
    ((= n 1) 1)
    ((evenp n)
           (let ((tmp (fibonacci-rec (/ n 2))))
             (* tmp (+ tmp (* 2 (fibonacci-rec (- (/ n 2) 1)))))))
    (t (+ (expt (fibonacci-rec (/ (+ n 1) 2)) 2)
      (expt (fibonacci-rec (/ (- n 1) 2)) 2)))))

这个算法在每次递归调用时都会将问题划分为两个规模为原先一半的子问题,表示该算法运行时间的递归式 (recurence) 为:

T(n) = 2*T(n/2) + c

其中 c 为进行乘法运算和加法运算需要的时间,将其看作常量。而 n/2 代表 (n / 2) 得 到的数的上或下取整得到的整数。由主方法 (master method) 和替换法进行分析都得到这 个算法的时间复杂度应该为 Θ(n),从算法导论上的例子看这个递归式的时间复杂度也确实是 Θ(n),但是从实际的运行情况来看其效率比上面的迭代算法快不少,使用的内存也少许多,与下面的迭代版本的 Θ(lg(n)) 的算法差距不大。(性能比较是在 SBCL 中使用 time 进行的,通过求 F(50000) 进行,这个数值在我的机器上除了 fibonacci-rec-simple 以外 不会进行垃圾回收,因此避免了垃圾回收导致的性能下降。)是不是我的递归式弄错了,请知道的人请告诉我。

避免重复的计算

上面的递归算法中其实还是进行了不少的重复计算,而它的复杂度也因此不能降到 Θ (lg(n))。下面分两种情况考虑上面的递归算法。

如果递归过程中问题划分为求 F(2n) 和 F(2n + 1),F(2n) 会通过 F(n), F(n – 1) 求得, 而 F(2n + 1) 通过 F(n), F(n + 1) 求得。这里的关系如下:

F(2n)     --> F(n - 1), F(n)
F(2n + 1) --> F(n),     F(n + 1)

而如果递归过程中问题划分为求 F(2n – 1) 和 F(2n) 时,有如下关系:

F(2n - 1) --> F(n - 1), F(n)
F(2n)     --> F(n - 1), F(n)

上面的关系式中,F(n) 和 F(n – 1) 都重复出现,但实际上上面的算法都对它们进行了重复计算,减少这些重复计算就可以提高算法的性能。

上面的第二中情况需要计算两个子问题,而第一种情况里面要计算三个子问题,但实际上可以利用 F(n – 1), F(n) 来得到 F(n + 1),因此实际上也只需要计算F(n – 1), F(n) 两个 子问题就可以了。由此我们得到下面的关系式:

F(2n - 1), F(2n) --> F(n - 1), F(n)
F(2n), F(2n + 1) --> F(n - 1), F(n)

注意这里的关系式与前面式子的区别,前面的式子中,每个问题划分为两个子问题,而这里我们将求解两个 Fibonacci 数列中的项看成一个问题,每个问题被划分为一个更小的子问题。利用上面的关系式就可以避免算法执行中的重复计算。由此得到下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defun fibonacci-rec-2 (n)
  (cond ((= n 0) 0)
  (t (multiple-value-bind (x) (fib-two n) x))))

;; Returns the n'th and n-1'th fibonacci number.
(defun fib-two (n)
  (cond ((= n 1) (values 1 0))
  ((evenp n)
   (multiple-value-bind (i i-1) (fib-two (/ n 2))
     (values (* i (+ i (* 2 i-1)))
         (+ (* i-1 i-1) (* i i)))))
  (t
   (multiple-value-bind (i i-1) (fib-two (/ (- n 1) 2))
     (values (+ (* i i) (expt (+ i-1 i) 2))
         (* i (+ i (* 2 i-1))))))))

fib-two 返回的是 F(n) 和 F(n – 1),利用了 CL 中函数返回多个返回值的特性。这个算法的递归式如下:

T(n) = T(n/2) + c

c 代表加法乘法运算的时间,n/2 的含义与上面的相同。这个递归式的运行时间为 Θ(lg(n))。 实际测试的结果,这个算法的性能是最好的,内存开销也最小(为什么内存开销小不明白,可能跟我使用的 Common Lisp 实现有关),比下面要介绍的复杂度为 Θ(lg(n)) 迭代算法更快。

对迭代的变形提高算法性能

上面的递归算法都是从改变递归关系出发的,通过修改迭代过程也可以得到性能更好的算法。这里的思路是从 SICP 的习题 1.19 中看来的。有兴趣的直接去看原书好了。

考虑之前提到的 T 变换(不是递归式里面的 T),实际上求 F(n) 就是对最初的数值做 n 次 T 变换。

F(n) = T(T(T(...T(1, 0)))) = T^n(1, 0)

改变迭代过程的思路其实使用下面的关系式求乘方是一样的:

a^n = (a^2)^{n/2} = (a^4)^{n/4} = ... = (a^n)^1

具体算法实现的时候,当 n 不是偶数的时老老实实多乘一个 a,是偶数的话就平方。对 Fibonacci 数列,我们希望能够找到一个变换序列 {T_i}(下划线表示下标),使得:

T_1(a, b) = T^2(a, b)
T_2(a, b) = T^4(a, b)
T_3(a, b) = T^8(a, b)
...
T_{k-1}(a, b) = T^{n/2}(a, b)
T_k(a, b) = T^n(a, b)

对任意的 i,下式成立:
T_{i+1}(a, b) = T_i^2(a, b)

SICP 上面用的办法是定义 T{pq}(a, b) = (bp + aq + ap, bp + aq),然后寻找变换 T{p’q’},使得:

T_{p'q'}(a, b) = T_{pq}^2(a, b)

通过计算可以得到

p' = p^2 + q^2
q' = q^2 + 2pq

这个式子与之前改变递归定义得到的关系式形式完全相同,不过我没有看出来它们是如何联系起来的。最后给出具体的算法实现,我把书上 Scheme 的实现转换成 CL 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
(defun fibonacci-iter (n)
   (fib-iter 1 0 0 1 n))

(defun fib-iter (a b p q count)
  (cond ((= count 0) b)
  ((evenp count)
   (fib-iter a b
         (+ (* p p) (* q q)) ; p
         (* q (+ q p p))     ; q
         (/ count 2)))
  (t (fib-iter (+ (* b q) (* a q) (* a p)) ; a
           (+ (* b p) (* a q))         ; b
           p q (- count 1)))))

函数 fib-iter 虽然递归调用了自己,但其实是尾递归,实际的思想是迭代。我另外写了一个没有递归调用的版本,但是用了多个 setf 赋值,不够优雅,所以还是给原来的版本。按理说这个算法也是 Θ(lg(n)) 的时间复杂度,但是实际运行中,在求 F(50000) 时仅比 fibonacci-rec 快了没多少,比 fibonacci-rec-2 慢了不少。比后者慢可能是因为这个算法在输入为奇数时只能前进一小步,而 fibonacci-rec-2 不管输入是奇数还是偶数都可以将问题规模缩小一半。

关于递归中的重复计算

为了解决递归中的重复计算问题其实有另外的解决方案,将已经计算过的值存入某个缓存中。函数调用开始计算之前先到缓存中查,如果已经计算过则可以直接利用之前的结果,否则才进行计算。如果实现了这样的缓存,那么即使使用最初的 fibonacci-rec-simple,函数的时间复杂度也会降低到 Θ(lg(n)),而 fibonacci-rec 的时间复杂度也会降到 Θ (lg(n))。这个想法也是看 SICP 时看到的,在 1.2.2 的脚注 34 中提出,练习 3.27 给 出了一个实现。

为什么用 Common Lisp

用 CL 不是为了装 B,我会的语言中用 C/C++, Java 来研究算法显然太麻烦,不支持大整数,性能测试不方便。而 Ruby 的实现会导致一个程序使用不同的语法而导致巨大的性能差异。比如 inject 加 block 的性能比 while 的性能差很多,但是前者在实现累加之类的操作时显然会更优雅。而我对 Scheme 的了解仅限于 SICP 上用到的那些。这样下来就只剩下 Common Lisp 了。

而事实上,用 CL 研究算法真的是非常方便。在 SLIME 和 Emacs 的支持下,我可以使用一个 功能强大的 IDE,编译、调试、代码编辑都非常方便,更重要的是与系统交互式的开发过程。我可以在写完一个函数之后非常方便的测试这个函数,简化测试过程大大的减少了我实现一个算法的时间。另外语言本身的特性也使得算法的实现变得简单,比如让函数返回多个值,CL 本身就有特性支持这个功能。大整数使得我可以使用大的输入来测试性能,time 方便 的让我得到函数运行时间、内存的统计信息。使用其他语言我很难享受到这些好处。

CL 也有不爽的地方:数学式子得转换成前缀表达式,不过这个也可以通过 macro 来解决,Google 到了几个现成的,暂时还没有研究怎么用。

浦口大学的黑出租和马自达

今天早上出校门的时候看到门口停着两辆车,一辆小型卡车,上面拉着几条标语,一辆桑塔纳,后备箱里装着三个大喇叭,开足功率在放什么东西。开始觉得很奇怪,以为是哪个经营惨淡的电影院来宣传某些情色歌舞团的节目来了,但果真如此的话为什么对着浦口大学的校门放呢,浦口大学学生素质之高怎可能去看那种东西,应该到其他地方去边开边放,也许还能招揽到一些看客。好奇心驱使之下听了听,看了看,才知道是在宣传打击非法营运的黑出租、马自达(就是三轮摩托)之类的政策,那两辆车都是交通部门的(我一开始根本看不出来),而校门口确实是看不到其他任何一辆黑出租和三轮了,立竿见影啊,政府政策果然是行之有效。唱赞歌同时还是要指出一点不足,这种方式实在是太傻了,让人想起爸爸妈妈讲文革时候的故事了……

不是浦口大学这边的人也许会觉得奇怪,先解释下浦口大学这边的状况。浦口大学地处偏远的南京浦口高新区(如果是高薪区的话就好了),占地面积大概好像是二千亩,第一批到此上学的前辈们是在 90 年代初来到这里。虽然地处偏远,但还不致于到鸟不拉X的田地,好歹还有 131, 159 两路公交车可以让我们在有需要的时候体验一下乡下人进城的感觉(我不鄙视乡下人,因为自己就是乡下人),这两路公交车由于行车路线所占的地理优势,集众多浦口大学学子和浦口市民的宠爱,常常严重超载。(抱歉,犯错了。似乎公交车没有超载一说,只要能挤得上人就上好了。)于是乎,为了谋取私利,也为了在谋取私利时让每个人的生活变得更美好,浦口大学的校门口出现了一些私家车和马自达。这些私家车主真是好心银,用自家的车以昂贵的价格载那些有钱腐败不愿宠爱 131, 159 的人们进城(正所谓一个愿打、一个愿挨),但人们还是习惯把他们称为黑出租而不是一个更好听些的名字“私家出租车”;而马自达则是短途的理想选择,价格公道又方便,只是人身安全可能得不到保证,不要命的可以随便坐。到了五一十一回家时那种不是人过日子,在 131, 159 迟迟不出现就要错过火车的时候,黑出租们就是救命的了,这种时候任人宰割也是没有办法的。错过火车的总成本比坐出租被宰的成本要高,理性的经济人当然选择低坐出租了(排除其他因素考虑)。

我自己没有坐过“私家出租车”,因为他们实在太“黑”,也难怪别人要叫黑出租。刚来浦口大学,没有打过正规出租车的师弟师妹们可能有不少被宰的。南京火车站打车到浦口大学 40 元左右,而黑出租车主会向坐他车的每一个人收 40,你知道行情的话就可以跟他谈价钱。曾经在火车站看到师弟师妹被宰,想上前告诉他们,但因为周围有一群黑出租车主而没有敢出头,惭愧……即使可以砍价到比出租便宜我也不愿去坐,懒得跟他们罗嗦。从火车站回学校的时候宁愿找自己学校的人一起打正规出租车,找不到就宠爱 131, 159 去。马自达是坐过几回的,在东南、信息工程大学还有南京审计学院的时候也都坐过。对信息工程大学那边的马自达车主印象比较深,因为竞争激烈,车主们都比较热情,价钱也挺公道。不过始终觉得不太安全,听说某个大学的两个女学生坐马自达出车祸去天堂的事情以后就更加不敢坐了。(似乎还是大一的新生,第一个学期十一回家的时候出的车祸,真是太可惜了……)

不过存在的就是合理的吧。浦口大学这地方交通实在不便,学校也不以人为本让校车方便广大学生进城,而浦口这边的市民也有着进城的需要。有需求就有市场,黑出租和马自达如果没有人坐的话怎么可能还会存在?就算存在也造成不了多少危害吧。我们能吃到面包师的面包然后感觉生活美好不是因为面包师的怜悯,而是因为面包师为了自己的私利而做出美味的面包来吸引顾客。每个人都为自己谋取私利还是会使我们的生活变得更美好,当然极端情况除外。(看曼昆的《经济学原理》时看到的例子,觉得很有道理。)我们不能因为人们谋取私利而阻止别人。黑出租和马自达车主都是为了谋取自己的私利,但是他们都使我们,至少说部分人的生活更加美好了一些。其实有更多的人去坐黑出租的话就会有少一些人宠爱 131, 159,对 131, 159 的乘客来说也是好事。

废话这么多,其实我的意思就是说治理非法营运之类的政府行为其实不是最好的做法。基本上可以肯定的是过几天没有交通局的人来的话黑出租和马自达又会出现的,我们的生活还是会很美好的。如果说黑出租、马自达影响市容因此要治理的话是说不过去的,估计领导们不会傻到领着贵宾来参观浦口这边的市容,真的要改善市容的话还是先从焚烧垃圾开始好了。(插些题外话。一到江北,就几乎看不到南京作为省会城市的影子了。浦口大学附近的垃圾竟然会通过焚烧来处理,真是无语到极点。可能因为这个地方本来环境就差,这些焚烧的垃圾对环境破坏的影响看起来就不那么大了吧。)真的要使黑出租、马自达消失的办法应该是改善这边的交通,使这边的人们感到没有黑出租和马自达生活反而更美好了,有的话反而起反作用。当然这个办法成本比较高,难度也大,有魄力和能力搞定的人没几个。搞不定就算了吧,别折腾了,今天唱一下要治理非法营运,明天就没影的话只是让这里的部分人不方便而已,生活变得恶劣是会让人骂娘的。能天天治理非法营运的话我佩服你的毅力,但那样更糟,生活恶劣的时间更长久而已,浪费了纳税人的钱还解决不了实质问题。当然,如果是因为觉得自己这么多时间没有做过什么事位子不保要折腾一下的话也可以原谅,但也不要想出这么个傻不拉唧形式,有点创意好伐。有个好的创意让人眼前一亮从而觉得生活中果然还是有点美好的事情,即使给生活带来不便也不会立马骂娘了。

黑出租和马自达跟我自己基本上没有什么大的关系,居然不小心还说了那么多。唉,多数是废话而已。窃想如果真的有人能改善浦口这边的交通状况,使我的生活更加美好的话该多好。不过我可能很快就要离开这里,也享受不到了吧,但至少以后这里的市民还有师弟师妹们可以享福了。这是为他人谋取私利,总该会让生活更美好吧?

对黑出租再发表一点看法。出租车营运费非常昂贵,从某种角度上来说它造成了黑出租的出现。但黑出租车主并不是在城里营业,是不是可以让他们用一个非常非常便宜的费用得到特殊的营业证,然后仅仅在城郊地区营业,仅仅在某些特别的情况下允许进城或者限制进入城区的范围。这样可以将这些黑出租也纳入城市出租车管理中,将“黑”出租变“白”,是不是会更好一些。(我不想说合法化,我不懂法律,但不认为原先的就是非法的,逃税除外。)这样的方案可能很天真吧,但我不在其位,而某其政,想怎么说都行。我也并不认为这样的方案就合理,如何控制城郊出租车进城?凭什么城郊的出租车就不能在城区运营?前一个问题我不知道,后一个很显然是因为城区的出租车是交昂贵的运营费的,让城郊的出租进城对他们来说不公平。昂贵的出租车运营费也许本来就不是合理的东西,但它已经存在,一下子没有办法消除。为了让生活更加美好,同时又要维持这个不合理,也就只能再创造一些不合理了。

Linus 对 GPL V3 的看法

关心自由软件的人应该都知道 Linus 对 GPL v3 不是非常支持。今天看了 Linus 的这封邮件以后大概明白了他的理由。

… Laws (like copyright law) and legal issues, on the other hand, are fundamentally *not* about “personal” things, they are about interactions that are *not* personal. So laws need to be fundamentally different from morals. A law has to take into account that different people have different moral background, and a law has to be _pragmatic_. So trying to mix up a moral argument with a legal one is a fundamental mistake. They have two totally different and separate areas. The GPLv2 is a *legal* license. It’s not a “moral license” or a “spiritual guide”. Its raison-d’etre is that pragmatic area where different peoples different moral rules meet. In contrast, a persons *choice* to use the GPLv2 is his private choice. Totally different. My choice of the GPLv2 doesn’t say anything about my choice of laws or legal issues. …

License 是 legal issue,它会对许多人产生影响,必须考虑实际的问题。而 RMS 在 GPL v3 加入道德上的东西,但道德是很个人的事情,法律跟道德完全是两码事。Linus 在邮件的后面还举了纳粹士兵、甘地、罗宾汉的事例来说明 moral 的未必就是 legal 的,因此在 license 中混入道德上的东西从根本上来说就是一个错误。

还没有看过 GPL v3,有时间要去看看。对这些争论以我的阅历没有资格去做什么评论,能做的只能是关注而已,并做些思考。

Donald E. Knuth 对科学和艺术的评论

从负暄琐话的新文章里面看到的,这段话是 Knuth 老大给 A=B 这本书做的序言,看了很令人激动,忍不住想摘下来。原文网址在这里,我无耻的抄了……

Science is what we understand well enough to explain to a computer. Art is everything else we do. … Science advances whenever an Art becomes a Science. And the state of the Art advances too, because people always leap into new territory once they have understood more about the old. This book will help you reach new frontiers.

Common Lisp 中使用 ASDF 安装类库和程序

虽然要做项目,还要参加比赛,时间不算宽裕,但是就是阻挡不了 Lisp 对我的吸引力。这两天看算法导论,实现算法时打算用 Common Lisp 了,边开发边调试这种环境真是太棒了!

安装 Edi Weitz 的 cl-ppcre (一个可移植的 Perl 兼容正则表达式库) 时稍微看了下 ASDF 的使用,原来还是很简单的。我用的 Common Lisp 实现是 SBCL,ASDF 是自带的,所以安装 ASDF 这一步就省了。下面解释下安装过程。

ASDF 用 asd 文件描述包的依赖性等安装信息,通过这个文件就可以安装某个类库了。为了方便管理,可以把要使用的所有的库的 asd 文件在某个目录下建个符号链接(注意,硬链接不行),ASDF 安装时会到这个目录下去找 asd 文件,如果发现是软链接就会转到目标文件所在的目录开始执行安装动作。使用下面代码可以指定这个存放 asd 文件链接的目录:

(require 'asdf)

(setf asdf:*central-registry*
      '(*default-pathname-defaults*
    #p"/path/to/asd/file/directory/"))

安装库时把解压以后的库代码目录中的 asd 文件在上面的 centeral-registry 目录中建个链接:

$ cd /path/to/asdf/file/directory
$ ln -s ../cl-ppcre/cl-ppcre.asd .

然后就可以通过下面的代码安装这个库,第一次加载时会编译成 fasl 文件,以后加载的时候就不需要编译了。

(asdf:operate 'asdf:load-op 'cl-ppcre)

如果每次启动都会用到这些库的话把这些代码写在 Lisp 实现的启动配置文件中,SBCL 的话就是主目录下的 .sbclrc。就这些,OK 了。

又一次考完试了

昨天 19:55 的时候,提前 5 分钟交了卷,大三的期末考试到此结束。

考完才突然有点失落,因为有些事情一直想做,却一直没有时间。等有时间的时候却已经没有了合适的机会。

考试期间,每天都要复习,会在西平的自习教室泡上一整天,从 9:00 到 17:00,去掉吃饭时间,然后 18:00 去考试。很专注的复习,除了第一门考试以外其他的也都还不错。喜欢上了这种专注的感觉,不用面一直对着电脑然后感慨浪费时间,也不会有太多的时间去胡思乱想导致神经衰弱。

可笑的是这是三年来第一次喜欢上自习,喜欢上西平的自习教室。而整个大三所有的自习时间基本上也就集中在这 5 天时间。如果从大一大二就这样的话很多事情都会发生改变,当然,这只是无意义的假设而已。时间之所以会让人觉得神奇可能就是因为它没有办法倒退吧。

有了一点空闲的时间,萧伯纳说”人们之所以会忧虑是因为有时间思考自己是否幸福”,所以现在又会开始胡思乱想,会有忧虑。考试之前看到大四学长离校的帖子,然后想到自己的一些事情就会突然变得伤感,考完之后却又开始忧虑其他的事情,唯有考试的时候时间紧迫,如此专注而不会忧虑。长大了就不再单纯了,有些事情没有办法让自己不去想。

想家了,抓紧时间做事,争取早点回家。要接着开始好好练小提琴,暑假的目标是开始练习换把。

Jdbc 连接 Mysql 时的中文乱码问题

在用 jdbc 向 mysql 数据库插入中文时出现了乱码,严格来说是通过 Hibernate。记录下搜索和查文档以后找到的解决办法。

  • 首先要告诉数据库要插入的字符串使用的字符集,mysql 默认使用的字符集是 latin1。我要保存的字符串是 UTF-8 编码的(字符集是 Unicode),所以包含这个字段的表应该使用 UTF-8 编码。这里有几种解决办法。
    1. 在建立数据库的时候指定数据库的字符集编码,这样,这个数据库的所有表都会默认使用数据库的字符集编码。如 create database foo charset utf8;
    2. 在建表的时候指定字符集编码。如 create table foo (id char(20)) charset utf8;
    3. 指定某一列使用的字符集编码。如create table foo (id char(20) charset utf8);
    如果你有需要的话还可以指定字符排序的规则,也就是指定 collation,如 create database foo charset utf8 collate utf8_general_ci;,同样也可以指定单独的表、列使用的 collation 规则。
  • 然后在使用 jdbc 连接数据库的时候要告知 jdbc 使用什么字符集编码来跟服务器通信。很简单,只需要在 jdbc 指定数据库路径时做一点修改就可以了。比如,jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8。注意如果在 XML 文件里面的话 “&” 要改成 “&”。

如果你使用的是 gbk 编码的话把上面所有提到 utf8 的地方改成 gbk 应该就可以了,只要服务器和客户端使用的字符集编码统一就可以了。

mysql 命令行客户端默认使用的字符集也是 latin1,如果你通过这个来插入中文的话也会出现乱码的情况。解决的办法是执行语句 set names ‘utf8’ 来告诉服务器使用 UTF-8 编码来和客户端通信。你也可以使用 set charset ‘utf8’,它和 set names 区别只在于 collation 上。set names 和 set charset 都相当于执行了三条语句,具体的内容可以去看 mysql 文档 10.4 节。我想这个方法在使用 jdbc 的时候也是可以的,所以如果 jdbc 的指定数据库地址中没有告知使用的字符集编码的话可以通过执行上面的语句来达到相同的效果。

(如果对文章中字符集和字符集编码的使用感到困惑的话去看点 Unicode 方面的文章吧。)

分布式版本控制工具:git & Mercurial

说到版本控制工具很多人可能都会马上想到 CVS 和 Subversion,但自从开始使用 git 以后,我在自己的开发过程中都会优先选择 git 而非前者。

最早从今年初就已经开始用 git。刚开始的时候会的确会感到 git 比较复杂,一个原因是它不同于 Subversion 这样的集中式版本控制系统,在 Subversion 中只有一个仓库(repository),许多个工作目录(working copy),而像 git 这样的分部式版本控制系统中,每一个工作目录都包含一个完整仓库,仓库之间内容可能不相同,可以进行仓库之间的同步。另一个原因是 git 的命令非常之多,而它本身的概念也比较复杂(虽然 Linus 说git 是 stupid contenc tracker,但其实这个东西不适合傻瓜使用),还分 repository, index, working tree 等,直接使用也会比较麻烦些,所以我实际一直都是使用 cogito,只有必要的时候才直接使用 git。

为什么要使用分布式的版本控制系统?Subversion 有什么不好?

我最开始使用 Subversion 时一直觉得有一点很不爽,如果我想把某个已有的项目使用Subversion 来进行管理,首先要建立一个仓库,然后把文件 import 到仓库,最后再check out,然后在 check out 的工作目录中再进行修改。为什么要那么麻烦?我只是自己一个人进行开发而已,为什么非要有一个仓库?此其一,只是不爽而已。

第二点使我没有办法使用 Subversion,不得不寻找其他的工具。我要在几台电脑上同时进行开发,我希望在每一台电脑上都能使用版本控制工具。所以我需要有一个放在优盘上的仓库,这个时候使用 Subversion 就有问题了。一来当你提交时你必须得把优盘插上电脑,每次提交都得插上;二来仓库在优盘上的位置不能改变,否则路径改变的话使用 file 协议拷贝出来的工作目录就废了。(我查过 svn propset 的帮助,似乎可以改变仓库地址,但我不会,网上也没有搜到。)

这两个问题 git 都可以很好的解决(严格来说我使用的是 cogito)。要把已有的文件加入版本控制的话使用 cg init 一条命令即可。而分布式的版本控制系统解决第二个问题实在是再适合不过了。在优盘上建立一个仓库,不同机器上的仓库在开发时就尽管 commit 到本地的仓库好了,在要换机器之前先把修改 push 到优盘上的仓库,到其他机器上时 pull 出来然和 merge 一下就好了,cogito 可以直接使用 update 完成这两步操作。而优盘上仓库路径如果有改变的话可以使用 cg-branch-chg 很方便的修改远程仓库的地址。实际可以认为优盘上的仓库就是一个中央仓库,所以有许多个仓库其实并不是一件可怕的事情,也完全可以像使用集中式的版本控制系统那样自定一个为中央仓库即可。但分布式的版本控制系统不强制你这么做,给你更多的灵活性。

更加让我喜欢 git 的是它的 branch 概念。我在使用 Subversion 的时候从来没有用过,因为它的 branch 概念是通过 copy 来实现的(当然不是实际的拷贝),不够直观。目前我只用 branch 来进行实验性的开发。而 Linus使用 git 管理内核开发时通过 branch 来整合多人所做的修改,内核有那么多的 branch,Linus 通过 git 可以很轻松的 merge 这些不同的 branch 所做的修改,最后从他自己的仓库中发布新版本的 Linux 内核。

此外 git 对磁盘空间的利用也更高效(不过需要定期对仓库使用 git repack -d 命令以后),其他方面性能也都很出色。想想它要管理 Linux 内核那么大的项目就可以知道了。

Linus 在 Google Tech talk 上做过 git 的介绍,以及他是如何使用 git 来管理内核开发的。他的演讲里面对分布式版本控制系统的好处有更好的说明。不过 Linus 自己也承认自己是个 strong opinion person,它在演讲的时候多次说集中式的版本控制系统没有前途,因此 Subversion 的开发者想要开发一个更好的 CVS 其实是脑子出了毛病,实在是太 offensive 了,好在他是 Linus,大家都知道他的个性。(他还说过自己是 digust pig, and proud of it,汗啊……)

但是,但是……

git 很好,可是它不跨平台,至少在非 Linux 平台上运行的没有那么好,在非 Linux 文件系统上会有麻烦。我虽然不在 Windows 上做开发,但是最近要在 Solaris 上做开发,我不想花时间在 Solaris 上把 git 装起来,而且如果以后如果要和其他使用 Windows 的人合作的话我可不想再使用 Subversion。所以我需要一个替代 git 的工具。这篇文章介绍了 Mozilla“移向”新版本控制工具时是如何做出选择。(原文强调是 “move” 而不是 “pick”,因为最后的候选者都很好。)首先肯定要用分布式的,然后在 4 个分布式的版本控制工具中筛选,git 和 Monotone 因为支持平台问题而被排除,剩下 Bazaar 和 Mercurial。前者有 Canonicol 在支持。而后者已经是 OpenSolaris 等著名项目的版本控制工具,而且有着非常好的文档,可以很方便的使用 python 的 web server 发布项目。在 Mozilla 的版本控制工具选择中 Mercurial 最终因为性能而胜出。所以我也决定转到 Mercurial 去了,看了看文档,感觉和 cogito 很像,比 git 更简单,迁移过程应该会比较顺利。

另外提一下,分布式的版本控制工具还有 darcs, arch。前者是用 Haskell 写的,后者据说很复杂。而 git 则是受到 Monotone 的影响。