本节主要概述数据结构与算法研究的基本框架,了解数据结构与算法的重要性,并理解为什么说学习这些内容有助于更好的解决实际问题。
通过本节学习,应掌握以下内容:
计算机技术的迅速发展,导致了计算机的应用范围迅速扩展,计算机的应用早已不再局限于科学计算,而更多地用于控制、管理及数据处理等非数值计算的处理工作。与此相应,计算机加工处理的数据也由纯粹的数值发展到字符、表格以及图像等各种具有一定结构的数据。如何合理地组织数据的内在联系、高效地处理数据,是“数据结构”主要研究问题。
为了能够更好的理解数据结构,首先需要定义一些基本概念和术语:
根据数据元素间的不同结构,可以分为四类基本结构:
逻辑结构是指数据对象中数据元素之间的相互关系。它与数据的存储无关,是独立于计算机的。数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。因此,我们可以形式化的将数据结构定义为一个二元组:
D a t a S t r u c t u r e = ( D , S ) DataStructure=(D,S) DataStructure=(D,S)
其中, D D D 是数据元素的有限集, S S S 是 D D D 上关系的有限集。
物理结构(也称存储结构)是指数据的逻辑结构在计算机中的表示(也称映像,包括数据元素的表示和关系的表示)。我们知道,计算机中的所有数据实例均由二进制串来表达,如用 8 位 (bit) 二进制数表示一个字符,通常称这个二进制串为元素 (element) 或结点 (node)。当数据元素由若干数据项组成时,二进制串中对应于各个数据项的子二进制串称为数据域 (data field)。因此,结点是数据元素在计算机中的映像。物理结构主要有顺序存储、链式存储、索引存储和散列存储。
数据的操作是指施加在数据上的操作,包括操作的定义和实现。操作的定义是针对逻辑结构的,其指定操作的功能;操作的实现是针对存储结构的,指出操作的具体操作步骤。常见的数据操作如下:
数据的逻辑结构、数据的存储结构及数据的操作是一个整体,需要从整体上进行理解,注意它们之间的联系。同一逻辑结构的不同存储结构可能产生不同的数据结构。在给定了数据的逻辑结构和存储结构之后,按定义的操作集合及操作性质不同,也可能导致完全不同的数据结构。数据结构的三个方面的相互关系,如下图所示:
我们知道,计算机中的所有数据实例均由二进制字符串来表达。为了赋予这些数据实际的意义,必须要有数据类型。数据类型 (data type) 是值的集合以及在这些值上定义的一组操作的总称。通常数据类型可以看作是程序设计语言中已实现的数据结构。数据类型根据“值“是否可分可以分为原子类型和结构类型两类,前者的值是不可分解的,例如整数、字符等,后者的值可以分为若干个成分,例如数组的值由若干个若干分量组成,每个分量可以是整数,也可以是数组等。
过程抽象将操作的实现细节隐藏起来,数据抽象的基本思想与此类似。抽象数据类型 (abstract data type, ADT) 从逻辑上描述了如何看待数据及其对应操作而无须考虑具体实现 (implementation)。这意味着我们仅需要关心数据代表了什么,而可以忽略它们的构建方式。通过这样的抽象,我们对数据进行了一层封装,其基本思想是封装具体的实现细节,使它们对用户不可见,这也称为信息隐藏。
我们可以将抽象数据类型定义为一个数学模型以及定义在该模型上的一组操作,那么和数据结构的形式定义相对应,抽象数据类型可用以下三元组表示:
A D T = ( D , S , P ) ADT=(D,S,P) ADT=(D,S,P)
其中, D D D 是数据对象, S S S 是 D D D 上的关系集, P P P 是对 D D D 的基本操作集。
下图展示了抽象数据类型及其原理。用户通过利用抽象数据类型提供的操作来与接口 (interface) 交互。 抽象数据类型是与用户交互的外壳。真正的实现则隐藏在内部。用户并不需要关心各种实现细节。
抽象数据类型的实现常被称为数据结构,这需要我们通过编程语言的语法结构和原生数据类型从物理视角看待数据。分成逻辑和物理这两种不同的视角有助于为问题定义复杂的数据模型,而无须考虑模型的实现细节。由于实现抽象数据类型通常会有很多种方法,因此独立于实现的数据视角使程序员能够改变实现细节而不影响用户与数据的实际交互。
数据结构不仅用于组织数据,它还极大地影响着代码的运行速度。由于数据结构不同,程序的运行速度可能相差多个数量级。当程序要处理大量的数据,或者需要数千人同时使用,那么采用何种数据结构,将决定它是否能够正常运行。
以数组和集合为例,但我们要为其添加一个数据值时,对于数组而言,我们可以直接在其末尾处插入,但是对于集合而言,由于其需要保证元素不重复,待添加元素需要从头开始逐个对比每个元素。
当我们对各种数据结构都有了深刻的理解,并明白它们对程序性能方面的影响,就能写出快速运行且优雅的代码,从而使软件运行得高效且流畅。当然,我们的编程能力也会向前迈出一大步。
如果要精确的定义计算机科学,可能非常困难,这是由于计算机科学早已不仅仅是研究计算机本身。计算机科学的研究对象可以总结为——问题、解决问题的过程,以及问题的解决方案。
对于一个给定问题,我们的目标是开发一个能够逐步解决该问题的算法 (algorithm)。算法是具有有限步骤的过程,依照这个过程便能解决问题。因此,我们也可以说算法就是解决方案。例如,“把大象关进冰箱里”的步骤就可以说是一个算法:
因此,一些看似复杂的事情,只要设计好算法往往都像是三步把大象装进冰箱里一样简单。
更正式的讲,算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。算法具有以下重要特性:
我们经常需要通过经验来学习:观察他人如何解决问题,然后亲自实践解决问题。学习计算机技术也是同样的,接触各种问题解决技巧并学习不同算法的设计方法,有助于解决新的问题。通过学习一系列不同的算法,可以举一反三,从而在遇到类似的问题时,能够快速解决。
各种算法之间往往差异巨大。例如计算斐波那契数列,完全可能有多种方法来实现计算斐波那契数列的函数。某一算法可能使用了较少的资源,而另一算法返回结果所需的时间可能是第一种算法数十倍。尽管这两种算法都能得到结果,但是其中一种可能比另一种“更好”——更高效、更快,或者使用的内存更少。在之后的学习中,我们会学习比较不同算法的分析技巧。这些技巧只依赖于算法本身的特性,而不依赖于实现算法的计算机或编程语言的特性。
在选择算法时,经常需要进行权衡。除了有解决问题的能力之外,我们也需要知晓如何评估一个解决方案。总之,问题通常有很多解决方案,如何找到一个解决方案并且确定其为优秀的方案,是需要不断学习、熟能生巧的。
数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。为了能够编写出一个“好”的算法,必须分析待处理对象的特性及各处理对象之间存在的关系。