(上) 混沌篇
教材选用
2004年的春天, 我开始讲授数据结构这门课程. 虽然那会已经不是第一次上讲台, 但讲这门课时感觉完全是陌生的, 后来想想原因大概在于教材吧.
上学的时候用的是严蔚敏老师的《数据结构》(绿皮Pascal版), 这本书流传甚广而且影响巨大(后期改了C语言版同样热销). 由于在语言上的偏好, 我当时想用C++版来讲授这门课程, 所以选用了殷人昆老师的《数据结构(用面向对象方法与C++描述)》这本书. 事实上, 当时C++代表了“先进”的生产力, 人们对它趋之若鹜. 另外, 殷老师的这本书在当年还算比较新, 而且内容尝试很大胆, 不过里面有些疏漏给教学或多或少地带来了不便(后来的第2版改进了很多).
说到这里, 不能不提Horowitz和Sahni的《数据结构基础》这本书, 它给我的感觉就是讲了很多内容, 特别庞杂, 但似乎不能让读者明白真义所在. 在Horowitz和Sahni写作的年代, 数据结构的研究只能算是刚起步, 还没有系统性地整理并用于教学, 所以会有东一榔头西一棒的感觉. 其实, 核心的问题是: 我们这门课程究竟应该学哪些内容? 但是, 在那个年代没有办法交代地特别清楚.
有人会问, 为什么不选外版教材呢? 一方面当时的环境还不太普及, 例如《算法导论》的影印版2002年才正式引入国内, 而且引进的优秀外版教材不是特别多.[1] 另一方面就是学生还是喜欢中文教材, 这一点至今如是. 所以, 发展本版教材是非常重要的.
开发环境
2004年那会在学校用的比较多的还是Visual C++ 6.0, 里面的坑不少, 对C++98
的标准支持也不是很理想. 现在一晃而过, 出了很多新鲜的工具, 比如Clang. 而且现在连Visual Studio 2017都发布了.
不过, 说实话那个年代的STL确实不太成熟, 换gcc也好不到哪里去. 而且考虑到学生的实际情况, 还是微软系列的相对比较方便一些. 现在我会要求学生在笔记本上安装Visual Studio 2015的社区版, 如果电脑配置不行的话, 可以换一个比较低的版本例如Visual Studio 2013. 这样可以方便地支持C++11
, 不过回想当年真是困难重重.
学习笔记
值得欣慰的是, 当年的学生很有韧性, 非常认真地在学习这门课程. 据说某位认真听课的同学把我讲的话全部记录下来了, 整整写满了三个大笔记本. 其实我特别好奇这位同学都记了些什么, 现在要是能看看, 也许能回忆起不少过去的时光.
说到这里, 我想额外多说几句. 记笔记无论是手写、iPad/Surface Pro记录抑或是LaTeX录入(前提要有一定的手速, 而且要求对LaTeX比较熟练), 都是不错的. 因为在记笔记的过程中是一种头脑的读入, 这可比手机拍下来要强多了. 许多人觉得拍下来就是自己掌握了, 其实很可能永远躺在手机相册里再也不看了.
STL
非常幸运地是, 清华大学出版社当时影印并且翻译了Ford和Topp的那本《数据结构——C++语言描述:应用标准模板库(STL)》. 这本书所指引的思路非常正确, 就是一开始从STL入手学习数据结构, 并且充分利用已经有的结果来搭建一个比较的程序. 然后我们再去学习新的知识, 比如STL的内部构造. 这样的方案在STL不是很成熟的时候确实有很大的风险, 比如那时候的STL没有将散列纳入标准, 所以只能用Plauger实现的hash_map
. 事实上, 那个时代的C++也不是很成熟, 为了求证一个问题, 我专门给C++ Primer的作者Lippman发了邮件, 他坦承那是编译器尚不支持的功能.
Ford和Topp的这本书很厚, 直接用于教学不太方便. 为了让同学们能够充分吸取其中的思想, 我编写了一本名为《算法与数据结构补充教材》的小册子, 专门讲解STL中各种容器的基本使用方案. 这本小册子大概只有60多页, 但是足够大家使用了. 现在回过头来看, Ford和Topp的这本书里面有很多不甚专业的地方, 有些讲解也不够严谨, 但它确实是一本还算不错的书.
备课讲义
从备课的角度看, 第一次在笔记本上手写讲义比较合适. 人在思考的时候用手写笔记会比较适应, 也能够全身心投入问题的本质而不是去考虑录入公式之类的问题. 但讲完第一次之后我发现了这种模式的问题——后续的修改非常麻烦. 于是第二次上课的时候我就开始全面投向电子讲义, 不过用的是Word来记录, 这也是一个非常大的坑(主要是它太“智能”了).
电子讲义的好处就是可以无限提升和改进, 这点非常方便. 但是在修改的过程中逐渐发现Word的各种不便, 而且公式也不是很好看, 于是我开始投向LaTeX的怀抱. 说起来很多美国大学让学生入学的时候就使用LaTeX来提交笔记和作业, 这点我现在非常赞同. 因为对于学术写作来说, LaTeX是必须掌握的技能点. 由于条件所限, 我不能强制要求现在学生都使用它, 但希望大家特别是还在上学的同学们都去学习和掌握LaTeX. 虽然学习曲线有点陡峭, 但是你会发现漂亮的排版会激发你继续前行.
我现在上课会用一款Typora的软件, 完美支持MarkDown和LaTeX公式. 很多公式利用它能够快速展现在屏幕上, 而且可以保留下来放到下一次课程继续改进. 想像一下, 键入几个符号就能出现清晰的数学公式, 比在黑板上手写公式要方便多了(不过需要苦练LaTeX和手速). 如果有兴趣的朋友, 不妨试试这款软件, 效果无比惊艳......
故事开始
说了很多废话, 这篇却要结束了. 实际上, 正是《算法与数据结构补充教材》和这些不断改善的讲义, 才揭开了《面向算法设计的数据结构(C++语言版)》的漫漫征程......
(中) 顿悟篇
纲举目张
很多年前流传着一本叫《现代计算机常用数据结构和算法》的黑皮书, 译者是南京大学的潘金贵老师等人. 虽然由于时代所限, 导致这本书没有版权, 但起到了传播交流的巨大贡献. 后来大家知道了, 这就是传说中的《算法导论》啊! 从译名可以看出, 我们的认知停留在数据结构与算法这个层面. 很多人不明白, 这本《算法导论》里面为什么书名不体现数据结构但又讲很多数据结构呢? 这种组织似乎非常让人不解.
我个人认为, 《算法导论》中对数据结构中的态度非常明确, 作者的思路就是: 数据结构要服务于算法, 否则就是屠龙之术, 应该摒弃. 例如图算法它只给出常用的邻接矩阵和邻接表, 那些各种应该退出历史舞台的结构完全不讲. 这样一来, 整个思路就非常明晰了.
集合
那么如何将这些跨入到数据结构课程呢. 关键点在于抽象数据类型, 而最关键的一点就是集合这个抽象数据类型, 它包含如下操作:
- 查找. 在集合中查找元素k.
- 插入. 在集合中插入元素k.
- 删除. 在集合中插入元素k.
- 最小元. 返回集合的最小元素.
- 最大元. 返回集合的最大元素.
- 下一元素. 返回比k大的最小元素.
- 上一元素. 返回比k小的最大元素.
通过这些操作我们可以构造出许多完整的程序, 而不同的程序需要不同的侧重点, 例如侧重于查找的集合或侧重于最大元的集合. 《算法导论》对此表述地很明确, 专门放在数据结构部分的引言部分, 开宗明义地阐述了作者的思想.
种类
更具体的教学内容到底应该是哪些呢? Robert E. Tarjan在其名作《数据结构与网络算法》(Data structures and network algorithms)中给出了非常精辟的总结:
- 区间: 包括数组/向量, 其特点是以O(1)时间访问元素. 注意区间的下标可以双向延伸, 也即这里可能存在负数下标.
- 列表: 包含链、栈和队列. 这三种看似简单的抽象数据类型在图算法中的应用可谓是精彩纷呈.
- 集合: 不一定有序关系, 以查找为主, 可用散列实现. 《算法导论》上称此为字典.
- 映射: 定义于全序集, 有最小和最大元素, 可用查找树实现. 此外映射还可引出优先级队列.
另外安利一下, Tarjan这位算法大宗师的书虽然薄薄一本, 但实在是微言大义, 值得深入阅读.
书名之疑
另外有一本书也对我起到了比较深远的影响, 那就是Weiss的《数据结构与算法分析》(Data Structures and Algorithm Analysis), 可能有些人对这本书的名字感到困惑, 为什么数据结构要和算法分析挂钩呢?
实际上, 作者的用意就是数据结构必须以算法分析为导向. 我们选择一种数据结构, 最关键的是看能否满足整个程序的需求, 而其中的准则就是算法分析结果. 是好是坏, 都由算法分析说了算, 这点撑起了整本书的结构. 事实上, Weiss的这一系列书有多个语言版本, 一直都是热销的教材. 我想, 这和作者的整体思路有很大的关系.
想了很久, 最终给自己的书定名为《面向算法设计的数据结构》.
写作思路
有了上述几座灯塔的指引, 我也逐渐为自己的讲义定出了方向:
基于抽象数据类型的观点讲解数据结构, 以算法分析为导向, 以算法效率为准绳, 着墨于抽象数据类型的选择、使用和组合, 从而实现提升算法性能的终极目标.
落到实处就是将STL的使用作为首位, 尽量与
C++11
标准靠拢, 提倡拿来主义少造轮子, 力争快速构建可用程序.
在草稿逐渐积累的时候, 清华大学出版社的龙启铭先生和我商谈了这本书的出版事宜. 虽然非常迅速地完成了签约工作, 不过由于我的拖延症和一些事情, 迟迟未能交稿. 这时候, 启铭兄经常激励和鞭策我, 让我最终将这本书呈现在大家面前.
另外, 由于我的完美主义倾向, 总想把书修订地非常完善. 后来发现这是一个不可能完成的任务, 与其这样还不如在一定阶段推出一个版本, 今后再逐渐迭代更新. 事实证明, 这样做非常明智.
(下) 进化篇
《面向算法设计的数据结构(C++语言版)》自从2015年12月出版以来, 得到了很多非常有益的反馈, 这为下一版的改进提供了很好的思路. 特别是2016年我开了微博(现在更名为@算法时空)之后, 在与各界人士互相交流的过程中, 更是发现了这本书的种种不足.
libc++
目前标准库实现中逻辑性和可读性最好的当属LLVM的libc++
, 当然大家也都知道Clang, 具体它们的关系我也不细说了. 本书下一版将考虑讲解一些libc++
的实现, 让大家可以多接触到这种业界顶级的源代码.
更多容器
由于写作时间所限, 第1版里没有讲到一大类容器, 也就是unordered
系列(unordered_set
, unordered_map
, unordered_multiset
, unordered_multimap
). 这也是本书的一大遗憾, 我们在第2版将介绍这些容器. 实际上, 它们的平均表现很好, 但也有可能在最坏情况下极差. 读者一定不要只看它们的优点, 也要多甄别选用.
代码优化
下一版会引入更多优化实例, 比如在递归中会讲解qsort
函数, 它对于递归的优化相当好, 非常值得一读. 更重要的是, 我们将会在书中更强调STL的使用而不是自己编写函数, 尽可能像乐高积木一样用STL搭建出复杂的系统, 这样性能更好也更简洁.
极限性能
我们将会展示更多极限情况下的性能, 而不是一些非常简单的例子. 比如, 可以让读者尝试将自己的内存用量压榨到极致, 看看会有什么变化. 而这些鲜活的实例正是教学所需要的, 也是学习者所感兴趣的话题. 实际上, 这有点数据库压力测试的意思, 从这里我们也可以看到并且惊叹于STL的抗压能力.
算法应用
我们考虑展示更多数据结构在算法中的应用, 特别是引入高效数据结构之后算法性能发生显著变化的实例. 只有通过这些实实在在的例子, 才能让学习者感受到数据结构之妙. 事实上, 写作本书的目的也是为了配合后续进阶算法课程的需要, 希望能让这本书成为学习《算法导论》和《算法设计》的一个良好铺垫, 能让大家顺利学好更深入的算法设计的技术.
建议
欢迎读过本书的朋友们在本篇文章下写留言, 写出你们的意见和建议. 我会在第2版中加以改进和完善.
-
不过顺便八卦一下, 如果你手里有《算法导论》影印版的话, 可以看到封底有个“线性编程”一直没改为正确的“线性规划”. ↩