Python数据结构与算法分析(第2版)
上QQ阅读APP看书,第一时间看更新

1.3 何谓计算机科学

要定义计算机科学,通常十分困难,这也许是因为其中的“计算机”一词。你可能已经意识到,计算机科学并不仅是研究计算机本身。尽管计算机在这一学科中是非常重要的工具,但也仅仅只是工具而已。

计算机科学的研究对象是问题、解决问题的过程,以及通过该过程得到的解决方案。给定一个问题,计算机科学家的目标是开发一个能够逐步解决该问题的算法。算法是具有有限步骤的过程,依照这个过程便能解决问题。因此,算法就是解决方案。

可以认为计算机科学就是研究算法的学科。但是必须注意,某些问题并没有解决方案。尽管这一话题已经超出了本书讨论的范畴,但是对于学习计算机科学的人来说,认清这一事实非常重要。结合上述两类问题,可以将计算机科学更完善地定义为:研究问题及其解决方案,以及研究目前无解的问题的学科。

在描述问题及其解决方案时,经常用到“可计算”一词。若存在能够解决某个问题的算法,那么该问题便是可计算的。因此,计算机科学也可以被定义为:研究可计算以及不可计算的问题,即研究算法的存在性以及不存在性。在上述任意一种定义中,“计算机”一词都没有出现。解决方案本身是独立于计算机的。

在研究问题解决过程的同时,计算机科学也研究抽象。抽象思维使得我们能分别从逻辑视角和物理视角来看待问题及其解决方案。举一个常见的例子。

试想你每天开车去上学或上班。作为车的使用者,你在驾驶时会与它有一系列的交互:坐进车里,插入钥匙,启动发动机,换挡,刹车,加速以及操作方向盘。从抽象的角度来看,这是从逻辑视角来看待这辆车,你在使用由汽车设计者提供的功能来将自己从某个地方运送到另一个地方。这些功能有时候也被称作接口

另一方面,修车工看待车辆的角度与司机截然不同。他不仅需要知道如何驾驶,而且更需要知道实现汽车功能的所有细节:发动机如何工作,变速器如何换挡,如何控制温度,等等。这就是所谓的物理视角,即看到表面之下的实现细节。

使用计算机也是如此。大多数人用计算机来写文档、收发邮件、浏览网页、听音乐、存储图像以及打游戏,但他们并不需要了解这些功能的实现细节。大家都是从逻辑视角或者使用者的角度来看待计算机。计算机科学家、程序员、技术支持人员以及系统管理员则从另一个角度来看待计算机。他们必须知道操作系统的原理、网络协议的配置,以及如何编写各种脚本来控制计算机。他们必须能够控制用户不需要了解的底层细节。

上面两个例子的共同点在于,抽象的用户(或称客户)只需要知道接口是如何工作的,而并不需要知道实现细节。这些接口是用户用于与底层复杂的实现进行交互的方式。下面是抽象的另一个例子,来看看Python的math模块。一旦导入这一模块,便可以进行如下的计算。

        >>> import math
        >>> math.sqrt(16)
        4.0
        >>>

这是一个过程抽象的例子。我们并不需要知道平方根究竟是如何计算出来的,而只需要知道计算平方根的函数名是什么以及如何使用它。只要正确地导入模块,便可以认为这个函数会返回正确的结果。由于其他人已经实现了平方根问题的解决方案,因此我们只需要知道如何使用该函数即可。这有时候也被称为过程的“黑盒”视角。我们仅需要描述接口:函数名、所需参数,以及返回值。所有的计算细节都被隐藏了起来,如图1-1所示。

图1-1 过程抽象

1.3.1 何谓编程

编程是指通过编程语言将算法编码以使其能被计算机执行的过程。尽管有众多的编程语言和不同类型的计算机,但是首先得有一个解决问题的算法。如果没有算法,就不会有程序。

计算机科学的研究对象并不是编程。但是,编程是计算机科学家所做工作的一个重要组成部分。通常,编程就是为解决方案创造表达方式。因此,编程语言对算法的表达以及创造程序的过程是这一学科的基础。

通过定义表达问题实例所需的数据,以及得到预期结果所需的计算步骤,算法描述出了问题的解决方案。编程语言必须提供一种标记方式,用于表达过程和数据。为此,编程语言提供了众多的控制语句和数据类型。

控制语句使算法步骤能够以一种方便且明确的方式表达出来。算法至少需要能够进行顺序执行、决策分支、循环迭代的控制语句。只要一种编程语言能够提供这些基本的控制语句,它就能够被用于描述算法。

计算机中的所有数据实例均由二进制字符串来表达。为了赋予这些数据实际的意义,必须要有数据类型。数据类型能够帮助我们解读二进制数据的含义,从而使我们能从待解决问题的角度来看待数据。这些內建的底层数据类型(又称原生数据类型)提供了算法开发的基本单元。

举例来说,大部分编程语言都为整数提供了相应的数据类型。根据整数(如23、654以及-19)的常见定义,计算机内存中的二进制字符串可以被理解成整数。除此以外,数据类型也描述了该类数据能参与的所有运算。对于整数来说,就有加减乘除等常见运算。并且,对于数值类型的数据,以上运算均成立。

我们经常遇到的困难是,问题及其解决方案都过于复杂。尽管由编程语言提供的简单的控制语句和数据类型能够表达复杂的解决方案,但它们在解决问题的过程中仍然存在不足。因此,我们需要想办法控制复杂度以利于找到解决方案。

1.3.2 为何学习数据结构及抽象数据类型

为了控制问题及其求解过程的复杂度,计算机科学家利用抽象来帮助自己专注于全局,从而避免迷失在众多细节中。通过对问题进行建模,可以更高效地解决问题。模型可以帮助计算机科学家更一致地描述算法要用到的数据。

如前所述,过程抽象将功能的实现细节隐藏起来,从而使用户能从更高的视角来看待功能。数据抽象的基本思想与此类似。抽象数据类型(有时简称为ADT)从逻辑上描述了如何看待数据及其对应运算而无须考虑具体实现。这意味着我们仅需要关心数据代表了什么,而可以忽略它们的构建方式。通过这样的抽象,我们对数据进行了一层封装,其基本思想是封装具体的实现细节,使它们对用户不可见,这被称为信息隐藏

图1-2展示了抽象数据类型及其原理。用户通过利用抽象数据类型提供的操作来与接口交互。抽象数据类型是与用户交互的外壳。真正的实现则隐藏在内部。用户并不需要关心各种实现细节。

图1-2 抽象数据类型

抽象数据类型的实现常被称为数据结构,这需要我们通过编程语言的语法结构和原生数据类型从物理视角看待数据。正如之前讨论的,分成这两种不同的视角有助于为问题定义复杂的数据模型,而无须考虑模型的实现细节。这便提供了一个独立于实现的数据视角。由于实现抽象数据类型通常会有很多种方法,因此独立于实现的数据视角使程序员能够改变实现细节而不影响用户与数据的实际交互。用户能够始终专注于解决问题。

1.3.3 为何学习算法

计算机科学家通过经验来学习:观察他人如何解决问题,然后亲自解决问题。接触各种问题解决技巧并学习不同算法的设计方法,有助于解决新的问题。通过学习一系列不同的算法,可以举一反三,从而在遇到类似的问题时,能够快速加以解决。

各种算法之间往往差异巨大。回想前文提到的平方根的例子,完全可能有多种方法来实现计算平方根的函数。算法一可能使用了较少的资源,算法二返回结果所需的时间可能是算法一的10倍。我们需要某种方式来比较这两种算法。尽管这两种算法都能得到结果,但是其中一种可能比另一种“更好”——更高效、更快,或者使用的内存更少。随着对算法的进一步学习,你会掌握比较不同算法的分析技巧。这些技巧只依赖于算法本身的特性,而不依赖于程序或者实现算法的计算机的特性。

最坏的情况是遇到难以解决的问题,即没有算法能够在合理的时间内解决该问题。因此,至关重要的一点是,要能区分有解的问题、无解的问题,以及虽然有解但是需要过多的资源和时间来求解的问题。

在选择算法时,经常会有所权衡。除了有解决问题的能力之外,计算机科学家也需要知晓如何评估一个解决方案。总之,问题通常有很多解决方案,如何找到一个解决方案并且确定其为优秀的方案,是需要反复练习、熟能生巧的。