从新视角来看待旧问题,则需要创造性的思维——爱因斯坦
数据结构的教与学经历
六七年前上数据结构课时,驾轻就熟地依然按照一贯的讲法上课。上了几次课后,收到班上一位同学的E-mail,信中说:“我自己是特别热爱写程序的。一方面我很熟悉电脑,软硬件都有涉猎,所以学起来就不缺基础的相关知识(像内存、寄存器、电子电路等等这些都很熟悉);另一方面我好像很能适应,也很喜欢这种思维方式……但好像班上不少的同学对数据结构的学习理解和接受起来还是比较困难的……”
教授数据结构的课程也有十余年了,一直以来,学生们总是认为数据结构不是一门容易学的课程,“在众多的专业课中,数据结构被很多学生认为是一门很难学习的课程。”[1]虽然我在学校读书时没有学过数据结构的课程,只是因为后来要教书,才自学的数据结构。在自学的过程中,也并没有觉着内容怎么难啊,这是怎么回事呢?
仔细回想一下自己学习与工作的经历过程,或许就是来信的这位同学说的,是因为在学习数据结构书本知识之前,已经有了较强的编程的技能、一些数据结构实际应用的先验知识吧。比如,在研究所工作时首次参加的软件开发项目中,就有多进程、链表、队列、散列等的实际应用,虽然在学校并没有学过这些概念,而是先接触到实际项目中要处理的问题,再看到其他程序员的具体算法处理和程序实现方法,从实际的问题切入,就比较容易理解相应的数据组织形式和对应的算法,等到后来再接触到书本的理论知识,就有一种一通百通,豁然开朗的感觉。还有一个原因是在软件开发过程中逐渐熟悉并掌握了程序调试方法,对例程通过跟踪的方法,很容易弄清执行的路径和结果,对算法的设计和实现的理解也起到了事半功倍的效果。
回头来看学生们学习的教科书,概念的介绍是传统意义上的叙述方式,抽象度很高,但从实际到抽象、从抽象到实际的过程介绍不足,即感性认识不足,抽象就难以理解接受。“现在有一个不好的倾向,就是教材或课堂过于重视抽象化的知识,忽视应用背景。数据结构的教材是这一倾向的代表。这对入门阶段的学生来讲是不适宜的,因为学生难以走进所涉及的抽象世界,最终表现为不知道在讲什么。”[2]
再看我的学生,既没有实际软件开发的经验也没有熟练的编程调试基础,对数据结构结构没有感性认识,先接触的是那些抽象的概念,感到理解和接受困难也就可以理解了。邹恒明在《数据结构:炫动的0、1之弦》一书中指出,对于很多人来说,数据结构的概念并不难,真正的难点是:
(1)如何实现从数据结构概念到程序实现的跨越(即如何实现一个数据结构);
(2)如何实现从实际应用到数据结构抽象的跨越(即如何利用数据结构解决实际问题)。
对于我来说,仅仅在学校学了一点点程序设计语言(记得所有上机时间加起来不超过20小时),没有任何数据结构的知识,刚出校门就参与了历时三年多后来获得国家科技进步三等奖的大型软件开发工作,以及后续多个电信用户单位的实际软件安装、应用调试和维护工作,亲历了实现了上述两个“跨越”的最实际生动的案例。虽然项目的开发过程非常艰苦,有在用户单位调试现场连续大半年的天天加班到深夜、第二天依然要正点到机房的超负荷工作,有通宵的跟踪调试,有24小时在线系统内存泄漏的巨大压力,有上线后双机备份系统同时崩溃争分夺秒找bug的惊心动魄,等等。应该说自己是很幸运的,虽然在学校仅仅学习了一点点编程的概念,但在工作中根据需要自学和向同事学习了很多新知识、经验和技巧,这样的实践磨练,对后来的程序设计类课程的理解和教授,是非常有益的。
学习数据结构困难的症结所在
回想与总结起来,之所以要有上述两个鸿沟要“跨越”,也是由于学校的传统教科书教法和实际的应用要求脱节造成的。
弗里德里希?威廉?尼采曾写道:“人们无法理解他没有经历过的事情。”[3]换句话来说,我们只接受与过去早已理解的事物相关的信息。这是一种比较学习的过程,在这个过程中,大脑要寻找每条信息之间的联系,借助以往经验来理解新事物[4]。
“欧拉认为,如果不能把解决数学问题背后的思维过程教给学生的话,数学教学就是没有意义的。现代计算机实质上的发明者莱布尼兹也说过:在我看来,没有什么能比探索发明的源头还要重要,它远比发明本身更重要。从小到大,我们看过的数学书几乎无一不是欧几里德式的:从定义到定理,再到推论。这样的书完全而彻底地扭曲了数学发现的真实过程。目前几乎所有的算法书的讲解方式也都是欧几里德式的,每一个推导步骤都是精准制导直接面向数据结构目标的,实际上,这完全把人类大脑创造发明的步骤给反过来了。对读者来说,这就等于直接告诉你答案和做法了,然后让你去验证这个答案和做法是可行或成立的,而关于答案和做法到底是怎么来的,从问题到答案之间经历了怎样的思维过程,却鲜有书能够很好地阐释。对于这类知识讲述(欧几里德方式)方式的批判,西方(尤其是在数学领域)早就有了。”[5]
传统数据结构教科书的一般模式都是给出问题,然后直接给出算法,而实际上要用计算机解决问题,必须要考虑的处理步骤有:如何分析问题中的已知信息,如何提炼数据及数据间的联系(数据的逻辑结构),如何选用合适的存储方式(数据的存储结构)将逻辑结构存到计算机中,然后在存储结构之上按照自顶向下逐步细化的方法给出算法,这才是真正解决实际问题的思维方法和步骤,也是软件开发中实际采用的方法。传统教科书的问题在于没有一个思维过程的引导与分析,致使概念论述、实现细节有余,设计实现过程描述不足,让学生看到的只是一个个问题具体的详解,而把握不住算法设计的总方法和原则。
本书尝试从学以致用的角度,注意给出问题或算法的知识背景或应用背景,增加一些在实际软件开发中的算法应用背景或实例;强调算法的分析方法、设计思路、给出重要算法的测试及调试分析,弥补上述传统教科书中的不足。教学生以“软件开发的方法”处理问题,使学生容易理解并掌握它,在实际的软件开发过程中能灵活地选择适当的逻辑结构、存储结构及相应的算法,设计性能优、效率高、可读性强、易维护的程序,达到数据结构课程最终的目的。
程序设计与数据结构的关系
我们在学习数据结构知识之前要有程序设计的基础,那么我们先来看看与编程相关的问题。
什么是编程?编程不仅仅是对语法的掌握,还涉及下面的诸多方面:
(1)程序的解题思路——算法是基本运算及规定的运算顺序所构成的完整的解题步骤,是程序的灵魂,算法的优劣直接影响程序的效率。本书的算法描述方法见稍后的说明。
(2)程序的运行速度——程序运行的速度受很多因素的影响。用户对程序的运行速度往往是有要求的,如实时响应系统。
(3)程序的运行空间——代码运行需要相应的内存空间及相关运行环境。在有些应用场合,对程序占用空间是有限制的,如嵌入式系统。
(4)代码规范——代码要按照一定的规范格式书写,以保证代码的一致性,便于交流和维护。
(5)程序的结构—— 一个功能复杂的程序由多个功能相对独立的模块组成。模块内高内聚,模块间低耦合,是判断程序结构是否合理的标准。
(6)模块接口——模块间的信息交流通过接口完成,模块间信息传递参数的设置应该合理有效。
(7)程序的测试与调试——要有精心设计的测试样例来测试程序是否正确。调试是高效率完成软件产品的有效方法。一个程序高手,也是调试专家,调试的经验方法多数都是实践中得到的。
我们在学习数据结构知识之前要有程序设计的基础,那么数据结构与程序设计间的关系是怎样的呢?应该说数据结构是编写规模庞大、逻辑复杂的更高级程序所需的基础。表0.1给出了程序设计与高级编程的特点。
表0.1 程序设计与高级编程
涉及课程 主要内容 课程目标
结构化程序
设计 程序设计基础类课程 语言语法形式、语句使用规则
模块设计思想 编写简单程序
解决简单问题
高级编程技术 数据结构 数据的抽象思维方法 编写规模庞大、
逻辑复杂的程序
算法的规范声明、算法的性能分析、算法的性能评价
软件工程 部件(模块)设计思想、软件工程思想
……
程序设计的首要问题是模块划分及相关问题,另一个重要方面,是把要解决问题的信息转换成计算机能认识并接收的数据,这一转换过程就是数据的抽象过程。要处理规模庞大的复杂问题,必须掌握数据的抽象思维方法,同时还要熟练掌握算法的规范声明、算法的性能分析、算法的性能评价等诸多技能。
数据结构与其他课程的关系
作为一门重要的专业核心必修课程,“数据结构与算法”课程既是对以往课程的深入和扩展,也为深入学习其他专业课程打下基础。课程中排序问题算法以及基本的树、图等数据结构,是计算机科学的基本功。B+树、散列(Hash)等高级数据结构,也是数据库、操作系统、编译原理、计算机网络等重要专业课程的基础。本课程在计算机学科中与其他课程的关系如图0.1所示。
图0.1 “数据结构与算法”课程在计算机学科中的重要地位
数据结构的重要性
商用程序员肖舸在他的博客中写道:“这么多年,我做过游戏、通信、工业控制、教育、VoIP、服务器集群等各个方向的项目,不可谓不宽。
但是我知道的是,其实我都是在用一种方法写程序。那就是从最底层的数据结构和算法开始做起,用最基本的C、C++语言开发。用来用去,还是那么几个数据结构,队列、堆栈,等等。
这好比武侠小说里面的内功,内力修好了,学招式,非常容易。但如果没有内力,练再好的招式,见到高手就软了。一力破十慧,就是这个道理。在绝对的实力面前,任何花招都是没有用的。”[6]
对清华大学计算机系历届毕业生和部分研究生追踪调查显示,几乎所有的学生都认为“数据结构”是他们在学校里学过的最有用的课程之一;数据结构是国内外许多软件开发机构要求考核的基本课程之一;IT业公司面试或笔试考核的绝大部分内容是数据结构或算法;数据结构也是计算机科学与技术、软件工程等专业研究生考试必考科目。