Python数据结构与算法(一)——数据结构与算法导论

x33g5p2x  于2021-11-13 转载在 Python  
字(4.5k)|赞(0)|评价(0)|浏览(567)

0. 学习目标

本节主要概述数据结构与算法研究的基本框架,了解数据结构与算法的重要性,并理解为什么说学习这些内容有助于更好的解决实际问题。
通过本节学习,应掌握以下内容:

  • 了解数据结构的基本概念
  • 理解数据的逻辑结构、存储结构和抽象数据类型的概念
  • 了解数据结构与算法学习的必要性

1. 数据结构概述

计算机技术的迅速发展,导致了计算机的应用范围迅速扩展,计算机的应用早已不再局限于科学计算,而更多地用于控制、管理及数据处理等非数值计算的处理工作。与此相应,计算机加工处理的数据也由纯粹的数值发展到字符、表格以及图像等各种具有一定结构的数据。如何合理地组织数据的内在联系、高效地处理数据,是“数据结构”主要研究问题。

1.1 什么是数据结构

为了能够更好的理解数据结构,首先需要定义一些基本概念和术语:

  • 数据 (data) 是能够输入并被计算机处理的符号集合。数据不仅包括实数、虚数等数值类型,同样也包含声音、图像等非数值类型。
  • 数据元素 (data element) 是数据的基本单位,在程序中通常作为一个整体进行考虑和处理。一个数据元素可以由若干数据项 (data item) 组成。例如在商品信息表中,一个商品的信息就是一个数据元素,而其每一项信息(如商品名、价格、编号等)就是一个数据项。
  • 数据对象 (data object) 是具有相同性质的若干个数据元素的集合。例如整数数据对象为集合 N={0, ±1, ±2, …},而商品信息表也可以视为一个数据对象。因此,只要集合内元素的性质均相同,都可称之为一个数据对象。
  • 数据结构 (data structure) 是相互之间存在一种或多种特定关系的数据元素的集合。在计算机中,数据元素并不是孤立、杂乱无序的,而是存在特定的关系,我们将这些关系称为结构 (structure)。数据结构包括以下三方面:逻辑结构、存储结构和数据的操作。

根据数据元素间的不同结构,可以分为四类基本结构:

  • 集合结构:集合中的各个数据元素是“平等”的,它们间的关系仅仅是“同属于一个集合”。
  • 线性结构:线性结构中的数据元素之间是一对一的关系。
  • 树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。
  • 图状结构:图状结构的数据元素是多对多的关系。

1.2 逻辑结构和物理结构

逻辑结构是指数据对象中数据元素之间的相互关系。它与数据的存储无关,是独立于计算机的。数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。因此,我们可以形式化的将数据结构定义为一个二元组:
D a t a S t r u c t u r e = ( D , S ) DataStructure=(D,S)DataStructure=(D,S)

其中,D DD 是数据元素的有限集,S SS 是 D DD 上关系的有限集。

物理结构(也称存储结构)是指数据的逻辑结构在计算机中的表示(也称映像,包括数据元素的表示和关系的表示)。我们知道,计算机中的所有数据实例均由二进制串来表达,如用 8 (bit) 二进制数表示一个字符,通常称这个二进制串为元素 (element) 或结点 (node)。当数据元素由若干数据项组成时,二进制串中对应于各个数据项的子二进制串称为数据域 (data field)。因此,结点是数据元素在计算机中的映像。物理结构主要有顺序存储、链式存储、索引存储和散列存储。

  • 顺序存储 (sequential storage structure):利用数据元素在存储器中的相对位置来表示数据元素之间的逻辑关系。优点是可以实现随机存取,每个元素占用最少的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的存储碎片。
  • 链式存储 (linked storage structure):每个结点是单独分配的,结点地址不一定连续的。为了表示元素之间的逻辑关系,给每个结点附加指针域,用于存放后继元素的存储地址。
  • 索引存储 (indexed storage structure):在存储元素信息的同时,还建立附加的索引表。存储数据元素信息的表称为主数据表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址),其中“关键字”唯一标识一个元素,“地址”对应该关键字的元素在主数据表中的存储地址。索引存储结构的优点是检索速度快;缺点是附加的索引表额外占用存储空间,且增加和删除数据时也要修改索引表,因而会花费较多的时间。
  • 散列存储 (hashed storage structure):根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash) 存储。其优点是检索、增加和删除结点的操作都很快;缺点是依赖良好的散列函数设计,否则可能出现元素存储单元的冲突,解决冲突会增加时间和空间开销。

数据的操作是指施加在数据上的操作,包括操作的定义和实现。操作的定义是针对逻辑结构的,其指定操作的功能;操作的实现是针对存储结构的,指出操作的具体操作步骤。常见的数据操作如下:

  • 读取:查看数据结构中某一位置上的数据。例如,查看索引 2 上是什么水果,就是一种读取
  • 查找:从数据结构中找出某个数据值的所在。例如,检查"桃子"是否存在于水果清单之中,给出其对应的索引,就是一种查找
  • 插入:给数据结构增加一个数据值。例如,往水果清单中多加一项"芒果",就是一种插入
  • 删除:从数据结构中移走一个数据值。例如,把水果清单中的"苹果"移除,就是一种删除

数据的逻辑结构、数据的存储结构及数据的操作是一个整体,需要从整体上进行理解,注意它们之间的联系。同一逻辑结构的不同存储结构可能产生不同的数据结构。在给定了数据的逻辑结构和存储结构之后,按定义的操作集合及操作性质不同,也可能导致完全不同的数据结构。数据结构的三个方面的相互关系,如下图所示:

1.3 抽象数据类型

我们知道,计算机中的所有数据实例均由二进制字符串来表达。为了赋予这些数据实际的意义,必须要有数据类型。数据类型 (data type) 是值的集合以及在这些值上定义的一组操作的总称。通常数据类型可以看作是程序设计语言中已实现的数据结构。数据类型根据“值“是否可分可以分为原子类型和结构类型两类,前者的值是不可分解的,例如整数、字符等,后者的值可以分为若干个成分,例如数组的值由若干个若干分量组成,每个分量可以是整数,也可以是数组等。
过程抽象将操作的实现细节隐藏起来,数据抽象的基本思想与此类似。抽象数据类型 (abstract data type, ADT) 从逻辑上描述了如何看待数据及其对应操作而无须考虑具体实现 (implementation)。这意味着我们仅需要关心数据代表了什么,而可以忽略它们的构建方式。通过这样的抽象,我们对数据进行了一层封装,其基本思想是封装具体的实现细节,使它们对用户不可见,这也称为信息隐藏。
我们可以将抽象数据类型定义为一个数学模型以及定义在该模型上的一组操作,那么和数据结构的形式定义相对应,抽象数据类型可用以下三元组表示:

A D T = ( D , S , P ) ADT=(D,S,P)ADT=(D,S,P)

其中,D DD 是数据对象,S SS 是 D DD 上的关系集,P PP 是对 D DD 的基本操作集。
下图展示了抽象数据类型及其原理。用户通过利用抽象数据类型提供的操作来与接口 (interface) 交互。 抽象数据类型是与用户交互的外壳。真正的实现则隐藏在内部。用户并不需要关心各种实现细节。

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

1.4 数据结构学习的必要性

数据结构不仅用于组织数据,它还极大地影响着代码的运行速度。由于数据结构不同,程序的运行速度可能相差多个数量级。当程序要处理大量的数据,或者需要数千人同时使用,那么采用何种数据结构,将决定它是否能够正常运行。
以数组和集合为例,但我们要为其添加一个数据值时,对于数组而言,我们可以直接在其末尾处插入,但是对于集合而言,由于其需要保证元素不重复,待添加元素需要从头开始逐个对比每个元素。
当我们对各种数据结构都有了深刻的理解,并明白它们对程序性能方面的影响,就能写出快速运行且优雅的代码,从而使软件运行得高效且流畅。当然,我们的编程能力也会向前迈出一大步。

2. 算法概述

如果要精确的定义计算机科学,可能非常困难,这是由于计算机科学早已不仅仅是研究计算机本身。计算机科学的研究对象可以总结为——问题、解决问题的过程,以及问题的解决方案。

2.1 什么是算法

对于一个给定问题,我们的目标是开发一个能够逐步解决该问题的算法 (algorithm)。算法是具有有限步骤的过程,依照这个过程便能解决问题。因此,我们也可以说算法就是解决方案。例如,“把大象关进冰箱里”的步骤就可以说是一个算法:

  • 第一步:把冰箱门打开
  • 第二步:把大象装进冰箱
  • 第三步:把冰箱门关上

因此,一些看似复杂的事情,只要设计好算法往往都像是三步把大象装进冰箱里一样简单。
更正式的讲,算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。算法具有以下重要特性:

  • 有穷性:一个算法必须在执行有穷步之后结束,且每一步都可在有穷时间内完成
  • 确定性:算法中每一条指令必须有确切的含义,不会产生二义性,并且对于相同的输入只能得出相同的输出
  • 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次实现
  • 输入:算法有零个或多个的输入,输入取自于特定的对象的集合
  • 输出:算法有一个或多个的输出,输出是同输入有特定关系的量

2.2 算法学习的必要性

我们经常需要通过经验来学习:观察他人如何解决问题,然后亲自实践解决问题。学习计算机技术也是同样的,接触各种问题解决技巧并学习不同算法的设计方法,有助于解决新的问题。通过学习一系列不同的算法,可以举一反三,从而在遇到类似的问题时,能够快速解决。
各种算法之间往往差异巨大。例如计算斐波那契数列,完全可能有多种方法来实现计算斐波那契数列的函数。某一算法可能使用了较少的资源,而另一算法返回结果所需的时间可能是第一种算法数十倍。尽管这两种算法都能得到结果,但是其中一种可能比另一种“更好”——更高效、更快,或者使用的内存更少。在之后的学习中,我们会学习比较不同算法的分析技巧。这些技巧只依赖于算法本身的特性,而不依赖于实现算法的计算机或编程语言的特性。
在选择算法时,经常需要进行权衡。除了有解决问题的能力之外,我们也需要知晓如何评估一个解决方案。总之,问题通常有很多解决方案,如何找到一个解决方案并且确定其为优秀的方案,是需要不断学习、熟能生巧的。

2.3 数据结构与算法间的关系

数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。为了能够编写出一个“好”的算法,必须分析待处理对象的特性及各处理对象之间存在的关系。

相关文章