大道至简--简单与复杂数据结构之间的性能PK

2013-03-01

事情源起阿里巴巴的一道笔题.题目大概是要求统计<圣经>中的各个单词出现的次数,按单词首次出现的顺序输出结果.本来没什么技术含量的题,随便用个kv容器都很容易统计单词次数,然后用链表把单词按出现顺序链起来.

结果prince-W同学有点较真,回来还真的写代码.意外地发现了一些很惊讶的东西,他发现这个题用平衡二叉树(bst)实现,效率竟然没有二叉排序树(avl)高,便找我讨论,于是引出了本文.

在我机器上跑的结果:

bstavl
10800001280000

bst的代码在这里,还有avl的代码

他是用c++写的代码,递归版本的.我开始怀疑是不是这个案例中bst树没有退化,所以avl体现不出优势来.但是他看了一下树的高度,avl是18层,而bst大概有30多层.

这不科学啊!有点被惊讶到,于是做了些更有趣的研究:我让他去用c++自带的map容器实现一下,而我去写了一个非递归版本avl.因为我看stl源代码时记得c++的map容器是红黑树实现的,而且是非递归版本,我想把递归实现的影响,以及红黑树,都看一下.

我的代码在这里.得到的实验结果更令人惊讶了.我写的非递归的avl速度快了一点点,是1050000.非递归的avl居然只比递归的bst快那么一点点!

而最慢的就是c++的map实现的.c++库中那么精妙的红黑树在这里居然比所有的都慢!

中间我帮他优化了一下代码,他上面有一段大概是:

if(在map中) {
    map[key] ++;
} else {
    map[key] = 0;
}

没有保存iterator的查找结果,造成每次插入都有两次的查找过程.修改以后,这个实现的速度仍然是最慢的,耗时大概是我那边c语言非递归avl的3倍!

于是我得出了结论:简单的东西往往比复杂的来得有用.c++那一套精妙的库可能倒不如自己写出来的很脏的c代码.从这个事情,又让我想起了rob pike说的大道至简,以及unix的那些KISS哲学,真是让人不由得感叹.

把那段经典的语录复制过来吧:

  1. 你无法断定程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
  2. 估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。
  3. 花哨的算法在 n 很小时通常很慢,而 n 通常很小。花哨算法的常数复杂度很大。除非你确定 n 总是很大,否则不要用花哨算法(即使 n 很大,也优先考虑原则 2 )。比如,解决常见问题时,最简单的树——二叉树(binary tree),总是比那些复杂的树(AVL树,伸展树(splay tree)和红黑树、B-树(B-tree),多叉树(trie))来的高效。
  4. 花哨的算法比简单算法更容易出 bug 、更难实现。尽量使用简单的算法配合简单的数据结构。只要掌握了数据结构中的四大法宝,就可以包打天下,他们是:array 、linked list 、hash table、binary tree 。这四大法宝可不是各自为战的,灵活结合才能游刃有余。比如,一个用hash table组织的symbol table,其中是一个个由字符型array构成的linked list。
  5. 以数据为中心。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。
  6. 没有原则 6 。

本文发生时间大致在2012-09-13,找工作那段.写作时间为2013-03-01.快要离校了整理以前的东西时写了此文.