1、 数据结构在程序设计中的应用 长沙市一中 肖洲第 1 页 共 28 页数据结构的在程序设计中的应用长沙市一中 肖洲【关键字】 逻辑结构 存储结构 算法优化【摘要】 数据结构作为程序设计的基础,其对算法效率的影响必然是不可忽视的。本文就如何合理选择数据结构来优化算法这一问题,对选择数据结构的原则和方法进行了一些探讨。首先对数据逻辑结构的重要性进行了分析,提出了选择逻辑结构的两个基本原则;接着又比较了顺序和链式两种存储结构的优点和缺点,并讨论了选择数据存储结构的方法;最后本文从选择数据结构的的另一角度出发,进一步探讨了如何将多种数据结构进行结合的方法。在讨论方法的同时,本文还结合实际,选用了一些较
2、具有代表性的信息学竞赛试题举例进行了分析。【正文】一、引论“数据结构算法程序” ,这就说明程序设计的实质就是对确定的问题选择一种合适的数据结构,加上设计一种好的算法。由此可见,数据结构在程序设计中有着十分重要的地位。数据结构是相互之间存在一种或多种特定关系的数据元素的集合。因为这其中的“关系” ,指的是数据元素之间的逻辑关系,因此数据结构又称为数据的逻辑结构。而相对于逻辑结构这个比较抽象的概念,我们将数据结构在计算机中的表示又称为数据的存储结构。建立问题的数学模型,进而设计问题的算法,直至编出程序并进行调试通过,数据结构在程序设计中的应用 长沙市一中 肖洲第 2 页 共 28 页这就是我们解决
3、信息学问题的一般步骤。我们要建立问题的数学模型,必须首先找出问题中各对象之间的关系,也就是确定所使用的逻辑结构;同时,设计算法和程序实现的过程,必须确定如何实现对各个对象的操作,而操作的方法是决定于数据所采用的存储结构的。因此,数据逻辑结构和存储结构的好坏,将直接影响到程序的效率。二、选择合理的逻辑结构在程序设计中,逻辑结构的选用就是要分析题目中的数据元素之间的关系,并根据这些特定关系来选用合适的逻辑结构以实现对问题的数学描述,进一步解决问题。逻辑结构实际上是用数学的方法来描述问题中所涉及的操作对象及对象之间的关系,将操作对象抽象为数学元素,将对象之间的复杂关系用数学语言描述出来。根据数据元素
4、之间关系的不同特性,通常有以下四种基本逻辑结构:集合、线性结构、树形结构、图状(网状)结构。这四种结构中,除了集合中的数据元素之间只有“同属于一个集合”的关系外,其它三种结构数据元素之间分别为“一对一” 、 “一对多” 、 “多对多”的关系。因此,在选择逻辑结构之前,我们应首先把题目中的操作对象和对象之间的关系分析清楚,然后再根据这些关系的特点来合理的选用逻辑结构。尤其是在某些复杂的问题中,数据之间的关系相当复杂,且选用不同逻辑结构都可以解决这一问题,但选用不同逻辑结构实现的算法效率大不一样。对于这一类问题,我们应采用怎样的标准对逻辑结构进行选择呢?下文将探讨选择合理逻辑结构应充分考虑的两个因
5、素。一、充分利用“可直接使用”的信息。首先,我们这里所讲的“信息” ,指的是元素与元素之间的关系。对于待处理的信息,大致可分为“可直接使用”和“不可直接使用”两类。对于“可直接使用”的信息,我们使用时十分方便,只需直接拿来就可以了。而对于“不可直接使用”的这一类,我们也可以通过某些间接的方式,使之成为可以使用的信息,但其中转化的过程显然是比较浪费时间的。由此可见,我们所需要的是尽量多的“可直接使用”的信息。这样的信息越多,算法的效率就会越高。对于不同的逻辑结构,其包含的信息是不同的,算法对信息的利用也会出现不同的复杂程度。因此,要使算法能够充分利用“可直接使用”的信息,而避免数据结构在程序设计
6、中的应用 长沙市一中 肖洲第 3 页 共 28 页算法在信息由“不可直接使用”向“可直接使用”的转化过程中浪费过多的时间,我们必然需要采用一种合理的逻辑结构,使其包含更多“可直接使用”的信息。问题一 IOI99 的隐藏的码字 。问题描述问题中给出了一些码字和一个文本,要求编程找出文本中包含这些码字的所有项目,并将找出的项目组成一个最优的“答案” ,使得答案中各项目所包含的码字长度总和最大。每一个项目包括一个码字,以及该码字在文本中的一个覆盖序列(如 abcadc就是码字 abac的一个覆盖序列) ,并且覆盖序列的长度不超过1000。同时, “答案”要求其中每个项目的覆盖序列互相没有重叠。问题分
7、析对于此题,一种较容易得出的基本算法是:对覆盖序列在文本中的终止位置进行循环,再判断包含了哪些码字,找出所有项目,并最后使用动态规划的方法将项目组成最优的“答案” 。算法的其它方面我们暂且不做考虑,而先对问题所采用的逻辑结构进行选择。如果我们采用线性的逻辑结构(如循环队列) ,那么我们在判断是否包含某个码字 t 时,所用的方法为:初始时用指针 p 指向终止位置,接着通过 p 的不断前移,依次找出码字 t 从尾到头的各个字母。例如码字为 “ABDCAB”,而文本图 1-1,终止位置为最右边的箭头符号,每个箭头代表依次找到的码字的各个字母。由于题目规定码字的覆盖序列长度不超过 1000,所以进行这
8、样的一次是否包含的判断,其复杂度为 O(1000)。由于码字 t 中相邻两字母在文本中的位置,并非只有相邻 (如图 1-1 中的D和C)这一种关系,中间还可能间隔了许多的字母(如图 1-1 中C和A就间隔了2 个字母) ,而线性结构中拥有的信息,仅仅只存在于相邻的两元素之间。通过这样简单的信息来寻找码字的某一个字母,其效率显然不高。如果我们建立一个有向图,其中顶点 i(即文本的第 i 位)用 52 条弧分别连接a.z,A.Z这 52 个字母在 i 位以前最后出现的位置(如图 1-2 的连接方式) ,我们要寻找码字中某个字母的前一个字母,就可以直接利用已连接的边,而不需指针 p 的移动方向A B
9、 D C A BC D A C B D C A D C D B A D C C B A D图 1-1数据结构在程序设计中的应用 长沙市一中 肖洲第 4 页 共 28 页用枚举的方法。我们也可以把问题看为:从有向图的一个顶点出发,寻找一条长度为 length(t)-1 的路径,并且路径中经过的顶点,按照码字 t 中的字母有序。通过计算,用图进行记录在空间上完全可以承受(记录 1000 个点52 条弧4字节的长整型=200k 左右 )。在时间上,由于可以充分利用第 i 位和第 i+1 位弧的连接方式变化不大这一点(如图 1-2 所示,第 i 位和第 i+1 位只有一条弧的指向发生了变化,即第 i+
10、1 位将其中一条弧指向了第 i 位),所以要对图中的弧进行记录,只需对弧的指向进行整体赋值,并改变其中的某一条弧即可。因此,我们通过采用图的逻辑结构,使得寻找字母的效率大大提高,其判断的复杂度为 O(length(t),最坏为 O(100),比原来方法的判断效率提高了 10 倍。(附程序 codes.pas)对于这个例子,虽然用线性的数据结构也可以解决,但由于判断的特殊性,每次需要的信息并不能从相邻的元素中找到,而线性结构中只有相邻元素之间存在关系的这一点,就成为了一个很明显的缺点。因此,问题一线性结构中的信息,就属于“不可直接使用”的信息。相对而言,图的结构就正好满足了我们的需要,将所有可能
11、产生关系的点都用弧连接起来,使我们可以利用弧的关系,高效地进行判断寻找的过程。虽然图的结构更加复杂,但却将“不可直接使用”的信息,转化成为了“可直接使用”的信息,算法效率的提高,自然在情理之中。 。二、不记录“无用”信息。从问题一中我们看到,由于图结构的信息量大,所以其中的信息基本上都是“可用”的。但是,这并不表示我们就一定要使用图的结构。在某些情况下,图结构中的“可用”信息,是有些多余的。信息都“可用”自然是好事,但倘若其中“无用” (不需要)的信息太多,就只会增加我们思考分析和处理问题时的复杂程度,反而不利于我们解决问题了。问题二 湖南省 1997 年组队赛的乘船问题问题描述有 N 个人需
12、要乘船,而每船最多只能载两人,且必须同名或同姓。求最少需要多少条船。C D A C B D C A D C D B A D C C B A D图 1-2数据结构在程序设计中的应用 长沙市一中 肖洲第 5 页 共 28 页问题分析看到这道题,很多人都会想到图的数据结构:将 N 个人看作无向图的 N 个点,凡同名或同姓的人之间都连上边。要满足用船最少的条件,就是需要尽量多的两人共乘一条船,表现在图中就是要用最少的边完成对所有顶点的覆盖。这就正好对应了图论的典型问题:求最小边的覆盖。所用的算法为“求任意图最大匹配”的算法。使用“求任意图最大匹配”的算法比较复杂(要用到扩展交错树,对花的收缩等等) ,
13、效率也不是很高。因此,我们必须寻找一个更简单高效的方法。首先,由于图中任两个连通分量都是相对独立的,也就是说任一条匹配边的两顶点,都只属于同一个连通分量。因此,我们可以对每个连通分量分别进行处理,而不会影响最终的结果。同时,我们还可以对需要船只 s 的下限进行估计:对于一个包含 Pi 个顶点的连通分量,其最小覆盖边数显然为P i/2。若图中共有 L 个连通分量,则 s=P i/2(10)表示 lcp,i-1的左儿子,显然 lcp,i与 p 是同姓的。假设存在某个点 q,满足 qs 且 qT。由于 s 是有限集合,因而必然存在某个 lcp,k无左儿子。则我们可以令 lcp,k+1=q,所以 qT
14、,与假设 qT 相矛盾。所以假设不成立,原命题得证。由引理 2.1 的证明方法,我们同理可证引理 2.2。【引理 2.2】若二叉树 T 中包含了某个结点 p,那么连通分量中所有与 p 同名的点一定都在 T 中。有了上面的两个引理,我们就不难得出下面的定理了。【定理一】以连通分量中的任一点 p 作为根结点的二叉树,必然能够包含连通分量中的所有顶点。证明:由引理 2.1 和引理 2.2,所有与 p 同姓或同名的点都一定在二叉树中,即连通分量中所有与 p 有边相连的点都在二叉树中。由连通分量中任两点间都存在路径的特性,该连通分量中的所有点都在二叉树中。在证明二叉树中包含了连通分量的所有顶点后,我们接
15、着就需要证明我们的猜想,也就是下面的定理:【定理二】包含 m 个结点的二叉树 Tm,只需要船的数量为 boatm=m/2(mN)。图 2-1-2图 2-1-1438672 5123 1754 68数据结构在程序设计中的应用 长沙市一中 肖洲第 7 页 共 28 页证明:(i) 当 m=1,m=2,m=3 时命题显然成立。(ii)假设当 m3)时命题成立,那么当 m=k 时,我们首先从树中找到一个层次最深的结点,并假设这个结点的父亲为 p。那么,此时有且只有以下三种情况(结点中带有阴影的是 p 结点):(1) 如图 2-2-1,p 只有一个儿子。此时删去 p 和 p 唯一的儿子,T k 就成为了
16、 Tk-2,则 boatk=boatk-2+1=(k-2)/2+1=k/2。(2) 如图 2-2-2,p 有两个儿子,并且 p 是其父亲的左儿子。此时可删去 p和 p 的右儿子,并可将 p 的左儿子放到 p 的位置上。同样地,T k 成为了 Tk-2,boatk=boatk-2+1=k/2。(3) 如图 2-2-3,p 有两个儿子,并且 p 是其父亲的右儿子。此时可删去 p和 p 的左儿子,并可将 p 的右儿子放到 p 的位置上。情况与(2)十分相似,易得此时得 boatk=boatk-2+1=k/2。综合(1)、(2)、(3),当 m=k 时,boatk=k/2。最后,综合(i) 、(ii)
17、 ,对于一切 mN,boatm=m/2 。由上述证明,我们将问题中数据的图结构转化为树结构后,可以得出求一棵二叉树的乘船方案的算法:proc try(father:integer;var root:integer;var rest:byte);输出 root 为树根的子树的乘船方案,father=0 表示 root 是其父亲的左儿子,father=1 表示 root 是其父亲的右儿子, rest 表示输出子树的乘船方案后,是否还剩下一个根结点未乘船beginvisitroot:=true; 标记 root 已访问找到一个与 root 同姓且未访问的结点 j; if jn+1 then try(
18、1,k,rrest);if (lrest=1) xor (rrest=1) then begin 判断 root 是否只有一个儿子,情况一if lrest=1 then print(lrest,root) else print(rrest,root);图 2-2-1 图 2-2-2 图 2-2-3数据结构在程序设计中的应用 长沙市一中 肖洲第 8 页 共 28 页rest:=0;endelse if (lrest=1) and (rrest=1) then begin 判断 root 是否有两个儿子if father=0 then beginprint(rrest,root);root:=j;
19、 情况二endelse beginprint(lrest,root);root:=k; 情况三 end;rest:=1;endelse rest:=1;end;这只是输出一棵二叉树的乘船方案的算法,要输出所有人的乘船方案,我们还需再加一层循环,用于寻找各棵二叉树的根结点,但由于每个点都只会访问一次,寻找其左右儿子各需进行一次循环,所以算法的时间复杂度为 O(n2)。 (附程序 boat.pas)最后,我们对两种结构得出不同时间复杂度算法的原因进行分析。其中最关键的一点就是因为二叉树虽然结构相对较简单,但已经包含了几乎全部都“有用”的信息。由我们寻找乘船方案的算法可知,二叉树中的所有边不仅都发挥
20、了作用,而且没有重复的使用,可见信息的利用率也是相当之高的。既然采用树结构已经足够,图结构中的一些信息就显然就成为了“无用”的信息。这些多余的“无用”信息,使我们在分析问题时难于发现规律,也很难找到高效的算法进行解决。这正如迷宫中的墙一样,越多越难走。 “无用”的信息,只会干扰问题的规律性,使我们更难找出解决问题的方法。小结我们对数据的逻辑结构进行选择,是构造数学模型一大关键,而算法又是用来解决数学模型的。要使算法效率高,首先必须选好数据的逻辑结构。上面已经提出了选择逻辑结构的两个条件(思考方向) ,总之目的是提高信息的利用效果。利用“可直接使用”的信息,由于中间不需其它操作,利用的效率自然很
21、高;不不记录“无用”的信息,就会使我们更加专心地研究分析“有用”的信息,对信息的使用也必然会更加优化。总之,在解决问题的过程中,选择合理的逻辑结构是相当重要的环节。数据结构在程序设计中的应用 长沙市一中 肖洲第 9 页 共 28 页三、 选择合理的存储结构数据的存储结构,分为顺序存储结构和链式存储结构。顺序存储结构的特点是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系;链式存储结构则是借助指示元素存储地址的指针表示数据元素之间的逻辑关系。因为两种存储结构的不同,导致这两种存储结构在具体使用时也分别存在着优点和缺点。这里有一个较简单的例子:我们需要记录一个 nn 的矩阵,矩阵中包含的非
22、0 元素为 m 个。此时,我们若采用顺序存储结构,就会使用一个 nn 的二维数组,将所有数据元素全部记录下来;若采用链式存储结构,则需要使用一个包含 m 个结点的链表,记录所有非 0 的 m 个数据元素。由这样两种不同的记录方式,我们可以通过对数据的不同操作来分析它们的优点和缺点。1随机访问矩阵中任意元素。由于顺序结构在物理位置上是相邻的,所以可以很容易地获得任意元素的存储地址,其复杂度为 O(1);对于链式结构,由于不具备物理位置相邻的特点,所以首先必须对整个链表进行一次遍历,寻找需进行访问的元素的存储地址,其复杂度为 O(m)。此时使用顺序结构显然效率更高。2对所有数据进行遍历。两种存储结
23、构对于这种操作的复杂度是显而易见的,顺序结构的复杂度为 O(n2),链式结构为 O(m)。由于在一般情况下m 要远小于 n2,所以此时链式结构的效率要高上许多。除上述两种操作外,对于其它的操作,这两种结构都不存在很明显的优点和缺点,如对链表进行删除或插入操作,在顺序结构中可表示为改变相应位置的数据元素。既然两种存储结构对于不同的操作,其效率存在较大的差异,那么我们在确定存储结构时,必须仔细分析算法中操作的需要,合理地选择一种能够“扬长避短”的存储结构。一、合理采用顺序存储结构。我们在平常做题时,大多都是使用顺序存储结构对数据进行存储。究其原因,一方面是出于顺序结构操作方便的考虑,另一方面是在程
24、序实现的过程中,使用顺序结构相对于链式结构更便于对程序进行调试和查找错误。因此,大多数人习惯上认为,能够使用顺序结构进行存储的问题,最“好”采用顺序存储结构。其实,这个所谓的“好”只是一个相对的标准,是建立在以下两个前提条件之下的:1链式结构存储的结点与顺序结构存储的结点数目相差不大。这种情况下,数据结构在程序设计中的应用 长沙市一中 肖洲第 10 页 共 28 页由于存储的结点数目比较接近,使用链式结构完全不能体现出记录结点少的优点,并且可能会由于指针操作较慢而降低算法的效率。更有甚者,由于指针自身占用的空间较大,且结点数目较多,因而算法对空间的要求可能根本无法得到满足。2并非算法效率的瓶颈
25、所在。由于不是算法最费时间的地方,这里是否进行改进,显然是不会对整个算法构成太大影响的,若使用链式结构反而会显得操作过于繁琐。二、必要时采用链式存储结构。上面我对使用顺序存储结构的条件进行了分析,最后就只剩下何时应该采用链式存储结构的问题了。由于链式结构中指针操作确实较繁琐,并且速度也较慢,调试也不方便,因而大家一般都不太愿意用链式的存储结构。但是,这只是一般的观点,当链式结构确实对算法有很大改进时,我们还是不得不进行考虑的。问题三 IOI99 的地下城市 。问题描述已知一个城市的地图,但未给出你的初始位置。你需要通过一系列的移动和探索,以确定初始时所在的位置。题目的限制是:1不能移动到有墙的
26、方格。2只能探索当前所在位置四个方向上的相邻方格。在这两个限制条件下,要求我们的探索次数(不包括移动)尽可能的少。问题分析由于存储结构要由算法的需要确定,因此我们首先来确定问题的算法。经过对问题的分析,我们得出解题的基本思想:先假设所有无墙的方格都可能是初始位置,再通过探索一步步地缩小初始位置的范围,最终得到真正的初始位置。同时,为提高算法效率,我们还用到了分治的思想,使我们每一次探索都尽量多的缩小初始位置的范围(使程序尽量减少对运气的依赖)。接着,我们来确定此题的存储结构。由于这道题的地图是一个二维的矩阵,所以一般来讲,采用顺序存储结构理所当然。但是,顺序存储结构在这道题中暴露了很大的缺点。我们所进行的最多的操作,一是对初始位置的范围进行筛选,二是判断要选择哪个位置进行探索。而这两种操作,所需要用到的数据,只是庞大地图中很少的一部分。如果采用顺序存储结构(如图 3-1 中阴影部分表示已标记),无论你需要用到多少数据,始终都要完全的遍历整个地图。