
数据结构严蔚敏.ppt
814页1、算法与数据结构算法与数据结构教材教材:数据结构数据结构(C语言版语言版)。严蔚敏,吴伟民。严蔚敏,吴伟民 编编 著。清华大学出版社。著。清华大学出版社。参考文献参考文献: 1 数据结构数据结构 。张选平,雷咏梅。张选平,雷咏梅 编,编, 严蔚敏严蔚敏 审。审。 机械工业出版社。机械工业出版社。 2 数据结构与算法分析数据结构与算法分析。Clifford A. Shaffer著,著, 张张 铭,刘晓丹铭,刘晓丹 译。电子工业出版社。译。电子工业出版社。 3 数据结构习题与解析数据结构习题与解析(C语实言版语实言版)。李春葆。李春葆。 清华大学出版社。清华大学出版社。 4 数据结构与算法数据结构与算法。夏克俭。夏克俭 编著。国防工业出编著。国防工业出版社。版社。第1章 绪 论 目前,计算机已深入到社会生活的各个领域,其应目前,计算机已深入到社会生活的各个领域,其应用已不再仅仅局限于科学计算,而更多的是用于控制,用已不再仅仅局限于科学计算,而更多的是用于控制,管理及数据处理等非数值计算领域。计算机是一门研究管理及数据处理等非数值计算领域。计算机是一门研究用计算机进行信息表示和处理的科学。这里
2、面涉及到两用计算机进行信息表示和处理的科学。这里面涉及到两个问题:信息的个问题:信息的表示表示,信息的,信息的处理处理。 信息的表示和组织又直接关系到处理信息的程序的信息的表示和组织又直接关系到处理信息的程序的效率。随着应用问题的不断复杂,导致信息量剧增与信效率。随着应用问题的不断复杂,导致信息量剧增与信息范围的拓宽,使许多系统程序和应用程序的规模很大,息范围的拓宽,使许多系统程序和应用程序的规模很大,结构又相当复杂。因此,必须分析待处理问题中的对象结构又相当复杂。因此,必须分析待处理问题中的对象的特征及各对象之间存在的关系,这就是数据结构这门的特征及各对象之间存在的关系,这就是数据结构这门课所要研究的问题。课所要研究的问题。编写解决实际问题的程序的一般过程编写解决实际问题的程序的一般过程: 如何用数据形式描述问题如何用数据形式描述问题?即由问题抽象出一个即由问题抽象出一个适当的数学模型适当的数学模型; 问题所涉及的数据量大小及数据之间的关系问题所涉及的数据量大小及数据之间的关系; 如何在计算机中存储数据及体现数据之间的关系如何在计算机中存储数据及体现数据之间的关系? 处理问题时需要对
3、数据作何种运算处理问题时需要对数据作何种运算? 所编写的程序的性能是否良好所编写的程序的性能是否良好?上面所列举的问题基本上由数据结构这门课程来回答。上面所列举的问题基本上由数据结构这门课程来回答。计算机求解问题的一般步骤计算机求解问题的一般步骤1.1 数据结构及其概念数据结构及其概念 算法与数据结构算法与数据结构是计算机科学中的一门综合性专是计算机科学中的一门综合性专业基础课业基础课。是。是介于数学、计算机硬件、计算机软件三者介于数学、计算机硬件、计算机软件三者之间的一门核心课程,不仅是一般程序设计的基础,而之间的一门核心课程,不仅是一般程序设计的基础,而且是设计和实现编译程序、操作系统、数据库系统及其且是设计和实现编译程序、操作系统、数据库系统及其他系统程序和大型应用程序的重要基础。他系统程序和大型应用程序的重要基础。1.1.1 数据结构的例子数据结构的例子姓名姓名电话号码电话号码陈海陈海13612345588李四锋李四锋13056112345。例例1:电话号码查询系统:电话号码查询系统 设有一个电话号码薄,它记录了设有一个电话号码薄,它记录了N个人的名字和其个人的名字和其相应的电
4、话号码,假定按如下形式安排:相应的电话号码,假定按如下形式安排:(a1, b1),(a2, b2),(an, bn),其中其中ai, bi(i=1,2n) 分别表示某人的分别表示某人的名字和电话号码。名字和电话号码。 本问题是一种典型的表格问题本问题是一种典型的表格问题。如表如表1-1,数据与数据成简单的一对一的,数据与数据成简单的一对一的线性关系线性关系。表表1-1 线性表结构线性表结构例例2:磁盘目录文件系统:磁盘目录文件系统 磁盘根目录下有很多子目录磁盘根目录下有很多子目录及文件,每个子目录里又可以包及文件,每个子目录里又可以包含多个子目录及文件,但每个子含多个子目录及文件,但每个子目录只有一个父目录,依此类推目录只有一个父目录,依此类推: 本问题是一种典型的树型结本问题是一种典型的树型结构问题,如图构问题,如图1-1 ,数据与数据,数据与数据成一对多的关系,是一种典型的成一对多的关系,是一种典型的非线性关系结构非线性关系结构树形结构树形结构。图图图图1-11-1 树形树形结构结构结构结构例例3:交通网络图:交通网络图 从一个地方到另外一个地方可以有多条路径从一个地方到另外一个地
5、方可以有多条路径。本问本问题是一种典型的题是一种典型的网状结构网状结构问题,数据与数据成多对多的问题,数据与数据成多对多的关系,是一种非线性关系结构关系,是一种非线性关系结构。佛山惠州广州中山东莞深圳珠海图图1-2 网状结构网状结构 数据数据(Data) :是客观事物的符号表示。在计算机科:是客观事物的符号表示。在计算机科学中指的是所有能输入到计算机中并被计算机程序处理学中指的是所有能输入到计算机中并被计算机程序处理的符号的总称。的符号的总称。 数据元素数据元素(Data Element) :是数据的基本单位,在程:是数据的基本单位,在程序中通常序中通常作为一个整体作为一个整体来进行考虑和处理。来进行考虑和处理。 一个数据元素可由若干个一个数据元素可由若干个数据项数据项(Data Item)组成。组成。数据项是数据的不可分割的最小单位。数据项是对客观数据项是数据的不可分割的最小单位。数据项是对客观事物某一方面特性的数据描述。事物某一方面特性的数据描述。 数据对象数据对象(Data Object):是性质相同的数据元素的集:是性质相同的数据元素的集合,是数据的一个子集。如字符集合合,是数
6、据的一个子集。如字符集合C=A,B,C, 。1.1.2 基本概念和术语基本概念和术语 数据结构数据结构(Data Structure):是指相互之间具有:是指相互之间具有(存在存在)一定联系一定联系(关系关系)的数据元素的集合。元素之间的相互联的数据元素的集合。元素之间的相互联系系(关系关系)称为称为逻辑结构逻辑结构。数据元素之间的逻辑结构有四。数据元素之间的逻辑结构有四种基本类型,如图种基本类型,如图1-3所示。所示。 集合集合:结构中的数据元素除了:结构中的数据元素除了“同属于一个集合同属于一个集合”外,没有其它关系。外,没有其它关系。 线性结构线性结构:结构中的数据元素之间存在一对一的:结构中的数据元素之间存在一对一的关系。关系。 树型结构树型结构:结构中的数据元素之间存在一对多的:结构中的数据元素之间存在一对多的关系。关系。 图状结构或网状结构图状结构或网状结构:结构中的数据元素之间存:结构中的数据元素之间存在多对多的关系。在多对多的关系。 数据结构的形式定义是一个二元组:数据结构的形式定义是一个二元组: Data-Structure=(D,S)其中:其中:D是数据元素的有限集
7、,是数据元素的有限集,S是是D上关系的有限集。上关系的有限集。例例2:设数据逻辑结构:设数据逻辑结构B=(K,R) K=k1, k2, , k9 R= , 画出这逻辑结构的图示,并确定那些是起点,那些是终点画出这逻辑结构的图示,并确定那些是起点,那些是终点1.1.3 数据结构的形式定义数据结构的形式定义图图1-3 四类基本四类基本结构图结构图结构图结构图1.1.4 数据结构的存储方式数据结构的存储方式 数据元素之间的关系可以是元素之间代表某种含义数据元素之间的关系可以是元素之间代表某种含义的自然关系,也可以是为处理问题方便而人为定义的关的自然关系,也可以是为处理问题方便而人为定义的关系,这种系,这种自然或人为定义的自然或人为定义的 “关系关系”称为数据元素之称为数据元素之间的间的逻辑关系逻辑关系,相应的,相应的结构结构称为称为逻辑结构逻辑结构。 数据结构在计算机内存中的存储包括数据结构在计算机内存中的存储包括数据元素的数据元素的存储存储和和元素之间的关系的表示元素之间的关系的表示。 元素之间的关系在计算机中有两种不同的表示方法:元素之间的关系在计算机中有两种不同的表示方法:顺序表示和非
8、顺序表示顺序表示和非顺序表示。由此得出两种不同的存储结构:由此得出两种不同的存储结构:顺序存储结构顺序存储结构和和链式存储结构链式存储结构。 顺序存储结构顺序存储结构:用数据元素在存储器中的相对位置用数据元素在存储器中的相对位置来表示数据元素之间的逻辑结构来表示数据元素之间的逻辑结构(关系关系)。 链式存储结构链式存储结构:在每一个数据元素中增加一个存放在每一个数据元素中增加一个存放另一个元素地址的指针另一个元素地址的指针(pointer ),用该指针来表示,用该指针来表示数据元素之间的逻辑结构数据元素之间的逻辑结构(关系关系)。例例:设有数据集合设有数据集合A=3.0,2.3,5.0,-8.5,11.0 ,两种不同,两种不同的存储结构。的存储结构。 顺序结构:数据元素存放的顺序结构:数据元素存放的地址是连续的地址是连续的; 链式结构:数据元素存放的链式结构:数据元素存放的地址是否连续没有要地址是否连续没有要求求。 数据的逻辑结构和物理结构是密不可分的两个方面,数据的逻辑结构和物理结构是密不可分的两个方面,一个一个算法的设计取决于算法的设计取决于所选定的所选定的逻辑结构逻辑结构,而,而
9、算法的实算法的实现依赖于现依赖于所采用的所采用的存储结构存储结构。 在在C语言中,用语言中,用一维数组一维数组表示顺序存储结构表示顺序存储结构;用用结结构体类型构体类型表示链式存储结构。表示链式存储结构。数据结构的三个组成部分:数据结构的三个组成部分:逻辑结构逻辑结构: 数据元素之间逻辑关系的描述数据元素之间逻辑关系的描述 D_S=(D,S)存储结构存储结构: 数据元素在计算机中的存储及其逻辑数据元素在计算机中的存储及其逻辑关系的表现称为数据的存储结构或物理结构关系的表现称为数据的存储结构或物理结构。数据操作数据操作: 对数据要进行的运算对数据要进行的运算。 本课程中将要讨论的三种逻辑结构及其采用的存储本课程中将要讨论的三种逻辑结构及其采用的存储结构如图结构如图1-4所示。所示。数据的逻辑结构数据的逻辑结构非线性结构非线性结构集合图状结构有向图无向图树形结构一般树二叉树线性结构线性结构一般线性表线性表推广广义表数组串受限线性表栈和队列图1-5 数据逻辑结构层次关系图数据逻辑结构层次关系图图图1-4 逻辑结构与所采用的存储结构逻辑结构与所采用的存储结构线性表线性表树树图图顺序存储结构顺序
10、存储结构链式存储结构链式存储结构复合存储结构复合存储结构逻辑结构逻辑结构物理结构物理结构 数据类型数据类型(Data Type):指的是:指的是一个值的集合一个值的集合和定义和定义在在该值集上的一组操作该值集上的一组操作的总称。的总称。 数据类型是和数据结构密切相关的一个概念。数据类型是和数据结构密切相关的一个概念。 在在C语言中数据类型有:基本类型和构造类型。语言中数据类型有:基本类型和构造类型。 数据结构不同于数据类型,也不同于数据对象,它数据结构不同于数据类型,也不同于数据对象,它不仅要描述数据类型的数据对象,而且要描述数据对象不仅要描述数据类型的数据对象,而且要描述数据对象各元素之间的相互关系。各元素之间的相互关系。1.1.5 数据类型数据类型 数据结构的主要运算包括:数据结构的主要运算包括: 建立建立(Create)一个数据结构;一个数据结构; 消除消除(Destroy)一个数据结构;一个数据结构; 从一个数据结构中删除从一个数据结构中删除(Delete)一个数据元素;一个数据元素; 把一个数据元素插入把一个数据元素插入(Insert)到一个数据结构中;到一个数据结构中; 对
11、一个数据结构进行访问对一个数据结构进行访问(Access); 对一个数据结构对一个数据结构(中的数据元素中的数据元素)进行修改进行修改(Modify); 对一个数据结构进行排序对一个数据结构进行排序(Sort); 对一个数据结构进行查找对一个数据结构进行查找(Search)。1.1.6 数据结构的运算数据结构的运算 抽象数据类型抽象数据类型(Abstract Data Type ,简称,简称ADT):是:是指一个数学模型以及定义在该模型上的一组操作。指一个数学模型以及定义在该模型上的一组操作。 ADT的定义仅是一组逻辑特性描述,的定义仅是一组逻辑特性描述, 与其在计算与其在计算机内的表示和实现无关。因此,不论机内的表示和实现无关。因此,不论ADT的内部结构如的内部结构如何变化,只要其数学特性不变,都不影响其外部使用。何变化,只要其数学特性不变,都不影响其外部使用。 ADT的形式化定义是三元组:的形式化定义是三元组:ADT=(D,S,P)其中:其中:D是是数据对象数据对象,S是是D上的上的关系集关系集,P是对是对D的的基本基本操作集操作集。1.2 抽象数据类型抽象数据类型ADT的一般定义
12、形式是:的一般定义形式是:ADT 数据对象:数据对象: 数据关系:数据关系: 基本操作:基本操作: ADT 其中数据对象和数据关系的定义用伪码描述。其中数据对象和数据关系的定义用伪码描述。 基本操作的定义是:基本操作的定义是:()初始条件:初始条件: 操作结果:操作结果: 初始条件:描述操作执行之前数据结构和参数应初始条件:描述操作执行之前数据结构和参数应满足的条件满足的条件;若不满足,则操作失败,返回相应的出若不满足,则操作失败,返回相应的出错信息。错信息。 操作结果:描述操作正常完成之后,数据结构的操作结果:描述操作正常完成之后,数据结构的变化状况和变化状况和 应返回的结果。应返回的结果。1.3.1 算法算法算法算法(Algorithm):是对特定问题求解方法:是对特定问题求解方法(步骤步骤)的一种的一种描述,是指令的有限序列,其中每一条指令表示一个或描述,是指令的有限序列,其中每一条指令表示一个或多个操作。多个操作。算法具有以下五个特性算法具有以下五个特性 有穷性有穷性: 一个算法必须总是在执行有穷步之后结一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。束,且每
13、一步都在有穷时间内完成。 确定性确定性:算法中每一条指令必须有确切的含义。:算法中每一条指令必须有确切的含义。不存在二义性。且算法只有一个入口和一个出口。不存在二义性。且算法只有一个入口和一个出口。 可行性可行性: 一个算法是能行的。即算法描述的操作一个算法是能行的。即算法描述的操作都可以通过已经实现的基本运算执行有限次来实现。都可以通过已经实现的基本运算执行有限次来实现。1.3 算法分析初步算法分析初步 输入输入: 一个算法有零个或多个输入,这些输入取一个算法有零个或多个输入,这些输入取自于某个特定的对象集合。自于某个特定的对象集合。 输出输出: 一个算法有一个或多个输出,这些输出是一个算法有一个或多个输出,这些输出是同输入有着某些特定关系的量。同输入有着某些特定关系的量。 一个算法可以用多种方法描述,主要有:使用自然一个算法可以用多种方法描述,主要有:使用自然语言描述;使用形式语言描述;使用计算机程序设计语语言描述;使用形式语言描述;使用计算机程序设计语言描述。言描述。 算法和程序是两个不同的概念算法和程序是两个不同的概念。一个计算机程序是。一个计算机程序是对一个算法使用某种程序设
14、计语言的具体实现。算法必对一个算法使用某种程序设计语言的具体实现。算法必须可终止意味着不是所有的计算机程序都是算法。须可终止意味着不是所有的计算机程序都是算法。 在本门课程的学习、作业练习、上机实践等环节,在本门课程的学习、作业练习、上机实践等环节,算法都用算法都用C语言来描述。在上机实践时,为了检查算法语言来描述。在上机实践时,为了检查算法是否正确,应编写成完整的是否正确,应编写成完整的C语言程序。语言程序。评价一个好的算法有以下几个标准评价一个好的算法有以下几个标准 正确性正确性(Correctness ): 算法应满足具体问题的算法应满足具体问题的需求。需求。 可读性可读性(Readability): 算法应容易供人阅读和交算法应容易供人阅读和交流。可读性好的算法有助于对算法的理解和修改。流。可读性好的算法有助于对算法的理解和修改。 健壮性健壮性(Robustness): 算法应具有容错处理。当算法应具有容错处理。当输入非法或错误数据时,算法应能适当地作出反应输入非法或错误数据时,算法应能适当地作出反应或进行处理,而不会产生莫名其妙的输出结果。或进行处理,而不会产生莫名其妙的输出
15、结果。 通用性通用性(Generality): 算法应具有一般性算法应具有一般性 ,即算,即算法的处理结果对于一般的数据集合都成立。法的处理结果对于一般的数据集合都成立。1.3.2 算法设计的要求算法设计的要求 算法执行时间需通过依据该算法编制的程序在计算算法执行时间需通过依据该算法编制的程序在计算机上运行所消耗的时间来度量。其方法通常有两种:机上运行所消耗的时间来度量。其方法通常有两种:事后统计事后统计:计算机内部进行执行时间和实际占用空间的:计算机内部进行执行时间和实际占用空间的统计。统计。 问题:必须先运行依据算法编制的程序;依赖软硬问题:必须先运行依据算法编制的程序;依赖软硬件环境,容易掩盖算法本身的优劣;没有实际价值。件环境,容易掩盖算法本身的优劣;没有实际价值。事前分析事前分析:求出该算法的一个时间界限函数。:求出该算法的一个时间界限函数。1.3.3 算法效率的度量算法效率的度量 效率与存储量需求效率与存储量需求: 效率指的是算法执行的时间;效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空存储量需求指算法执行过程中所需要的最大存储空间。一般地,这两者与
16、问题的规模有关。间。一般地,这两者与问题的规模有关。与此相关的因素有:与此相关的因素有: 依据算法选用何种策略;依据算法选用何种策略; 问题的规模;问题的规模; 程序设计的语言;程序设计的语言; 编译程序所产生的机器代码的质量;编译程序所产生的机器代码的质量; 机器执行指令的速度;机器执行指令的速度; 撇开软硬件等有关部门因素,可以认为一个特定算撇开软硬件等有关部门因素,可以认为一个特定算法法“运行工作量运行工作量”的大小,只依赖于问题的规模(通常的大小,只依赖于问题的规模(通常用用n表示),或者说,它表示),或者说,它是问题规模的函数是问题规模的函数。算法分析应用举例算法分析应用举例 算法中算法中基本操作重复执行的次数基本操作重复执行的次数是问题规模是问题规模n的某的某个函数,其时间量度记作个函数,其时间量度记作 T(n)=O(f(n),称作算法的渐,称作算法的渐近时间复杂度近时间复杂度(Asymptotic Time complexity),简称,简称时间时间复杂度复杂度。 一般地,常用一般地,常用最深层循环内最深层循环内的语句中的原操作的的语句中的原操作的执执行频度行频度(重复执
17、行的次数重复执行的次数)来表示。来表示。 “O”的定义:的定义: 若若f(n)是正整数是正整数n的一个函数,则的一个函数,则 O(f(n)表示表示 M0 ,使得当,使得当n n0时,时,| f(n) | M | f(n0) | 。表示表示时间复杂度时间复杂度的阶有:的阶有: O(1) :常量时间阶:常量时间阶 O (n):线性时间阶:线性时间阶 O(n) :对数时间阶:对数时间阶 O(nn) :线性对数时间阶:线性对数时间阶 O (nk): k2 ,k次方时间阶次方时间阶例例 两个两个n阶方阵的乘法阶方阵的乘法 for(i=1,i=n; +i) for(j=1; j=n; +j) cij=0 ; for(k=1; k=n; +k) cij+=aik*bkj ; 由于是一个三重循环,每个循环从由于是一个三重循环,每个循环从1到到n,则总次数为:,则总次数为: nnn=n3时间复杂度为时间复杂度为T(n)=O(n3)例例 +x; s=0 ; 将将x自增看成是基本操作,则语句频度为,即时自增看成是基本操作,则语句频度为,即时间复杂度为间复杂度为(1) 。如果将如果将s=0也看成是基本操作,则
18、语句频度为,其时也看成是基本操作,则语句频度为,其时间复杂度仍为间复杂度仍为(1),即常量阶。,即常量阶。例例 for(i=1; i=n; +i) +x; s+=x ; 语句频度为:语句频度为:2n,其时间复杂度为:,其时间复杂度为:O(n) ,即为线性,即为线性阶。阶。例例 for(i=1; i=n; +i)for(j=1; j=n; +j) +x; s+=x ; 语句频度为:语句频度为:2n2 ,其时间复杂度为:,其时间复杂度为:O(n2) ,即为平,即为平方阶。方阶。 定理定理:若若A(n)=a m n m +a m-1 n m-1 +a1n+a0是一个是一个m次多项式,则次多项式,则A(n)=O(n m)例例 for(i=2;i=n;+i) for(j=2;j=i-1;+j) +x; ai,j=x; 语句频度为:语句频度为: 1+2+3+n-2=(1+n-2) (n-2)/2 =(n-1)(n-2)/2 =n2-3n+2 时间复杂度为时间复杂度为O(n2),即此算法的时间复杂度为平方,即此算法的时间复杂度为平方阶。阶。 一个算法时间为一个算法时间为O(1)的算法,它的基本运算执
19、行的算法,它的基本运算执行的次数是固定的。因此,总的时间由一个常数(即的次数是固定的。因此,总的时间由一个常数(即零次多项式)来限界。而一个时间为零次多项式)来限界。而一个时间为O(n2)的算法则的算法则由一个二次多项式来限界。由一个二次多项式来限界。 以下六种计算算法时间的多项式是最常用的。其关以下六种计算算法时间的多项式是最常用的。其关系为:系为: O(1)O(n)O(n)O(nn)O(n2)O(n3) 指数时间的关系为:指数时间的关系为: O(2n)O(n!)O(nn) 当当n取得很大时,指数时间算法和多项式时间算法取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊。因此,只要有人能将现有指数在所需时间上非常悬殊。因此,只要有人能将现有指数时间算法中的任何一个算法化简为多项式时间算法,那时间算法中的任何一个算法化简为多项式时间算法,那就取得了一个伟大的成就。就取得了一个伟大的成就。 有的情况下,算法中基本操作重复执行的次数还有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同。随问题的输入数据集不同而不同。例例1:素数的判断算法。素数的判断算法。Voi
20、d prime( int n)/* n是一个正整数是一个正整数 */ int i=2 ; while ( (n% i)!=0 & i*1.0sqrt(n) )printf(“&d 是一个素数是一个素数n” , n) ;elseprintf(“&d 不是一个素数不是一个素数n” , n) ; 嵌套的最深层语句是嵌套的最深层语句是i+;其频度由条件;其频度由条件( (n% i)!=0 & i*1.0 sqrt(n) ) 决定,显然决定,显然i*1.01 & change; -i)for (j=0; jaj+1) aj aj+1 ; change=TURE ; 最好情况:最好情况:0次次 最坏情况:最坏情况:1+2+3+ +n-1=n(n-1)/2 平均时间复杂度为:平均时间复杂度为: O(n2) 1.3.4 算法的空间分析算法的空间分析 空间复杂度空间复杂度(Space complexity) :是指算法编写成:是指算法编写成程序后,在计算机中运行时所需存储空间大小的度量。程序后,在计算机中运行时所需存储空间大小的度量。记作:记作: S(n)=O(f(n) 其中:其中: n为问题的规模为问
21、题的规模(或大小或大小)该存储空间一般包括三个方面:该存储空间一般包括三个方面: 指令常数变量所占用的存储空间指令常数变量所占用的存储空间; 输入数据所占用的存储空间输入数据所占用的存储空间; 辅助辅助(存储存储)空间。空间。 一般地,算法的一般地,算法的空间复杂度空间复杂度指的是指的是辅助空间辅助空间。 一维数组一维数组an: 空间复杂度空间复杂度 O(n) 二维数组二维数组anm: 空间复杂度空间复杂度 O(n*m)习习 题题 一一1 简要回答术语:数据,数据元素,数据结构,数据简要回答术语:数据,数据元素,数据结构,数据类型。类型。2 数据的逻辑结构?数据的物理结构?逻辑结构与物数据的逻辑结构?数据的物理结构?逻辑结构与物理结构的区别和联系是什么?理结构的区别和联系是什么?3 数据结构的主要运算包括哪些?数据结构的主要运算包括哪些?4 算法分析的目的是什么?算法分析的主要方面是什算法分析的目的是什么?算法分析的主要方面是什么?么?5 分析以下程序段的时间复杂度,请说明分析的理由分析以下程序段的时间复杂度,请说明分析的理由或原因。或原因。 Sum1( int n ) int p=1
22、, sum=0, m ;for (m=1; m=n; m+) p*=m ; sum+=p ; return (sum) ;Sum2( int n ) int sum=0, m, t ;for (m=1; m=n; m+) p=1 ; for (t=1; t=m; t+) p*=t ;sum+=p ; return (sum) ; 递归函数递归函数fact( int n ) if (n0时,将非空的线性表记作:时,将非空的线性表记作: (a1,a2,an) a1称为线性表的称为线性表的第一个第一个( (首首) )结点,结点,an称为线性表的称为线性表的最后最后一个一个( (尾尾) )结点。结点。2.1.1 线性表的定义线性表的定义a1,a2,ai-1都是都是ai(2in)的的前驱前驱,其中,其中ai-1是是ai的的直接直接前驱前驱;ai+1,ai+2,an都是都是ai(1i n-1)的的后继后继,其中,其中ai+1是是ai的的直接后继直接后继。2.1.2 线性表的逻辑结构线性表的逻辑结构 线性表中的数据元素线性表中的数据元素ai所代表的具体含义随具体应所代表的具体含义随具体应用的不同而不
23、同,在线性表的定义中,只不过是一个抽用的不同而不同,在线性表的定义中,只不过是一个抽象的表示符号。象的表示符号。 线性表中的线性表中的结点结点可以是可以是单值元素单值元素(每个元素只有一每个元素只有一个数据项个数据项) 。例例1: 26个英文字母组成的字母表:个英文字母组成的字母表: (A,B,C、Z)例例2 : 某校从某校从1978年到年到1983年各种型号的计算机拥有量年各种型号的计算机拥有量的变化情况:的变化情况:(6,17,28,50,92,188)例例3 : 一副扑克的点数一副扑克的点数 (2,3,4,J,Q,K,A) 线性表中的线性表中的结点结点可以是可以是记录型记录型元素,每个元素含元素,每个元素含有多个数据项有多个数据项 ,每个项称为结点的一个域,每个项称为结点的一个域 。每个元。每个元素有一个可以唯一标识每个结点的素有一个可以唯一标识每个结点的数据项组数据项组,称为,称为关键字关键字。例例4 : 某校某校2001级同学的基本情况:级同学的基本情况:(2001414101,张里户张里户,男男,06/24/1983), (2001414102,张化司张化司,男男,08/1
24、2/1984) , (2001414102,李利辣李利辣,女女,08/12/1984) 若线性表中的结点是若线性表中的结点是按值按值(或按关键字值或按关键字值)由小到由小到大大(或由大到小或由大到小)排列排列的,称线性表是有序的。的,称线性表是有序的。2.1.3 线性表的抽象数据类型定义线性表的抽象数据类型定义ADT List数据对象:数据对象:D = ai | aiElemSet, i=1,2,n, n0 数据关系:数据关系:R = | ai-1, aiD, i=2,3,n 基本操作:基本操作:InitList( &L )操作结果:构造一个空的线性表操作结果:构造一个空的线性表L; 线性表是一种相当灵活的数据结构,其长度可根线性表是一种相当灵活的数据结构,其长度可根据需要增长或缩短。据需要增长或缩短。 对线性表的数据元素可以访问、插入和删除。对线性表的数据元素可以访问、插入和删除。ListLength( L )初始条件:线性表初始条件:线性表L已存在;已存在;操作结果:若操作结果:若L为空表,则返回为空表,则返回TRUE,否则返回,否则返回FALSE;.GetElem( L, i,
25、&e )初始条件:线性表初始条件:线性表L已存在,已存在,1iListLength(L);操作结果:用操作结果:用e返回返回L中第中第i个数据元素的值;个数据元素的值;ListInsert ( L, i, &e )初始条件:线性表初始条件:线性表L已存在,已存在,1iListLength(L) ;操作结果:在线性表操作结果:在线性表L中的第中的第i个位置插入元素个位置插入元素e; ADT List2.2 线性表的顺序存储线性表的顺序存储 顺序存储顺序存储 :把线性表的结点:把线性表的结点按逻辑顺序按逻辑顺序依次存放在依次存放在一组地址连续的存储单元一组地址连续的存储单元里。用这种方法存储的线性表里。用这种方法存储的线性表简称顺序表。简称顺序表。顺序存储顺序存储的线性表的的线性表的特点特点: 线性表的逻辑顺序与物理顺序一致线性表的逻辑顺序与物理顺序一致; 数据元素之间的关系是以元素在计算机内数据元素之间的关系是以元素在计算机内“物理物理位置相邻位置相邻”来体现。来体现。 设有非空的线性表:设有非空的线性表:(a1,a2,an) 。顺序存储如图。顺序存储如图2-1所示。所示。2.2.1 线
26、性表的顺序存储结构线性表的顺序存储结构 在具体的机器环境下在具体的机器环境下:设线性表的每个元素需占用:设线性表的每个元素需占用l个存储单元,以所占的第一个单元的存储地址作为数据个存储单元,以所占的第一个单元的存储地址作为数据元素的存储位置。则线性表中第元素的存储位置。则线性表中第i+1个数据元素的存储位个数据元素的存储位置置LOC(ai+1)和第和第i个数据元素的存储位置个数据元素的存储位置LOC(ai)之间满之间满足下列关系:足下列关系: LOC(ai+1)=LOC(ai)+l 线性表的第线性表的第i个数据元素个数据元素ai的存储位置为:的存储位置为: LOC(ai)=LOC(a1)+(i-1)*l a1 a2 ai an Loc(a1) Loc(ai)+(i-1)* l 图图2-1 线性表的顺序存储表示线性表的顺序存储表示 在高级语言在高级语言( (如如C C语言语言) )环境下环境下:数组具有随机存取:数组具有随机存取的特性的特性,因此,借助数组来描述顺序表。除了用数组来,因此,借助数组来描述顺序表。除了用数组来存储线性表的元素之外,顺序表还应该有表示线性表的存储线性表的元素之
27、外,顺序表还应该有表示线性表的长度属性,所以用结构类型来定义顺序表类型。长度属性,所以用结构类型来定义顺序表类型。#define OK 1#define ERROR -1#define MAX_SIZE 100typedef int Status ;typedef int ElemType ; typedef struct sqlist ElemType Elem_arrayMAX_SIZE ;int length ; SqList ;2.2.2 顺序表的基本操作顺序表的基本操作 顺序存储结构中,很容易实现线性表的一些操作:顺序存储结构中,很容易实现线性表的一些操作:初始化、赋值、查找、修改、插入、删除、求长度等初始化、赋值、查找、修改、插入、删除、求长度等。以下将对几种主要的操作进行讨论以下将对几种主要的操作进行讨论。1 顺序线性表初始化顺序线性表初始化 Status Init_SqList( SqList *L ) L-elem_array=( ElemType * )malloc(MAX_SIZE*sizeof( ElemType ) ) ;if ( !L - elem_arra
28、y ) return ERROR ; else L-length= 0 ; return OK ; 2 顺序顺序线性表的插入线性表的插入 在线性表在线性表 L= (a1,a i-1,ai, ai+1,an) 中中的的第第i(1in)个位置上插入一个新结点个位置上插入一个新结点e,使其成为线性,使其成为线性表表: L=(a1,a i-1,e,ai,ai+1,an) 实现步骤实现步骤(1)(1) 将线性表将线性表L中的中的第第i个至第个至第n个结点后移一个位置。个结点后移一个位置。(2) (2) 将结点将结点e插入到结点插入到结点ai-1之后之后。 (3) (3) 线性表长度加线性表长度加1。算法描述算法描述Status Insert_SqList(Sqlist *L,int i ,ElemType e) int j ;if ( iL-length-1) return ERROR ;if (L-length=MAX_SIZE) printf(“线性表溢出线性表溢出!n”); return ERROR ; for ( j=L-length1; j=i-1; -j )L-Elem_arrayj
29、+1=L-Elem_arrayj;/* i-1位置以后的所有结点后移位置以后的所有结点后移 */L-Elem_arrayi-1=e; /* 在在i-1位置插入结点位置插入结点 */L-length+ ;return OK ; 时间复杂度分析时间复杂度分析 在线性表在线性表L中的中的第第i个个元素之前插入新结点,其时间元素之前插入新结点,其时间主要耗费在表中结点的移动操作上,因此,可用结点的主要耗费在表中结点的移动操作上,因此,可用结点的移动来估计算法的时间复杂度。移动来估计算法的时间复杂度。 设设在线性表在线性表L中的中的第第i个个元素之前插入结点的概率元素之前插入结点的概率为为Pi,不失一般性,设各个位置插入是等概率,则,不失一般性,设各个位置插入是等概率,则Pi=1/(n+1),而插入时移动结点的次数为,而插入时移动结点的次数为n-i+1。总的平均移动次数:总的平均移动次数: Einsert=pi*(n-i+1) (1in) Einsert=n/2 。 即即在顺序表上做插入运算,平均要移动表上一半结在顺序表上做插入运算,平均要移动表上一半结点。当表长点。当表长n较大时,算法的效率相
30、当低。因此算法的较大时,算法的效率相当低。因此算法的平均时间复杂度为平均时间复杂度为O(n)。3 顺序线性表的删除顺序线性表的删除 在线性表在线性表 L=(a1,a i-1,ai, ai+1,an) 中删除中删除结点结点ai(1in),使其成为线性表,使其成为线性表: L= (a1,ai-1,ai+1,an) 实现步骤实现步骤(1)(1) 将线性表将线性表L中的中的第第i+1个至第个至第n个结点依此向前移个结点依此向前移动一个位置。动一个位置。(2) (2) 线性表长度减线性表长度减1。算法描述算法描述ElemType Delete_SqList(Sqlist *L,int i) int k ; ElemType x ;if (L-length=0) printf(“线性表线性表L为空为空!n”); return ERROR; else if ( iL-length ) printf(“要删除的数据元素不存在要删除的数据元素不存在!n”) ; return ERROR ; else x=L-Elem_arrayi-1 ; /*保存结点的值保存结点的值*/for ( k=i ; klen
31、gth ; k+) L-Elem_arrayk-1=L-Elem_arrayk; /* i位置以后的所有结点前移位置以后的所有结点前移 */L-length-; return (x); 时间复杂度分析时间复杂度分析 删除线性表删除线性表L中的中的第第i个个元素,其时间主要耗费在表元素,其时间主要耗费在表中结点的移动操作上,因此,可用结点的移动来估计算中结点的移动操作上,因此,可用结点的移动来估计算法的时间复杂度。法的时间复杂度。 设设在线性表在线性表L中中删除第删除第i个个元素的概率为元素的概率为Pi,不失,不失一般性,设删除各个位置是等概率,则一般性,设删除各个位置是等概率,则Pi=1/n,而删除,而删除时移动结点的次数为时移动结点的次数为n-i。则总的平均移动次数:则总的平均移动次数: Edelete=pi*(n-i) (1in) Edelete=(n-1)/2 。 即即在顺序表上做删除运算,平均要移动表上一半结在顺序表上做删除运算,平均要移动表上一半结点。当表长点。当表长n较大时,算法的效率相当低。因此算法的较大时,算法的效率相当低。因此算法的平均时间复杂度为平均时间复杂度为O(
32、n)。4 顺序线性表的查找定位删除顺序线性表的查找定位删除 在线性表在线性表 L= (a1,a2,an) 中删除值为中删除值为x的第一的第一个结点个结点。实现步骤实现步骤(1)(1) 在线性表在线性表L查找值为查找值为x的第的第一个数据元素。一个数据元素。(2) (2) 将从找到的位置至最后一将从找到的位置至最后一个结点依次向前移动一个结点依次向前移动一个位置。个位置。 (3) (3) 线性表长度减线性表长度减1。算法描述算法描述Status Locate_Delete_SqList(Sqlist *L,ElemType x) /* 删除线性表删除线性表L中值为中值为x的的第第一个一个结点结点 */ int i=0 , k ; while (ilength) /*查找值为查找值为x的的第第一个一个结点结点*/ if (L-Elem_arrayi!=x ) i+ ; else for ( k=i+1; klength; k+) L-Elem_arrayk-1=L-Elem_arrayk; L-length-; break ; if (iL-length) printf(“要删除的数据元素
33、不存在要删除的数据元素不存在!n”) ; return ERROR ; return OK; 时间复杂度分析时间复杂度分析 时间主要耗费在数据元素的比较和移动操作上。时间主要耗费在数据元素的比较和移动操作上。首先,首先,在线性表在线性表L中查找值为中查找值为x的的结点是否存在结点是否存在;其次,若其次,若值为值为x的的结点存在,且在结点存在,且在线性表线性表L中的位置为中的位置为i ,则在,则在线性表线性表L中删除中删除第第i个个元素。元素。 设设在线性表在线性表L删除删除数据元素概率为数据元素概率为Pi,不失一般性,不失一般性,设各个位置是等概率,则设各个位置是等概率,则Pi=1/n。 比较的平均次数比较的平均次数: Ecompare=pi*i (1in) Ecompare=(n+1)/2 。 删除时平均移动次数删除时平均移动次数:Edelete=pi*(n-i) (1in) Edelete=(n-1)/2 。 平均时间复杂度:平均时间复杂度:Ecompare+Edelete=n ,即为即为O(n)2.3 线性表的链式存储线性表的链式存储 2.3.1 线性表的链式存储结构线性表的链式
34、存储结构 链式存储链式存储 :用用一组任意的存储单元存储一组任意的存储单元存储线性表中线性表中的数据元素。用这种方法存储的线性表简称的数据元素。用这种方法存储的线性表简称线性链表线性链表。 存储链表中结点的一组任意的存储单元可以是连续存储链表中结点的一组任意的存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。意位置上的。 链表中结点的逻辑顺序和物理顺序不一定相同。链表中结点的逻辑顺序和物理顺序不一定相同。 为了正确表示结点间的逻辑关系,在存储每个结点为了正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其直接后继结点的地址值的同时,还必须存储指示其直接后继结点的地址( (或或位置位置) ),称为指针,称为指针(pointer)或链或链(link),这两部分组成了,这两部分组成了链表中的结点结构,如图链表中的结点结构,如图2-2所示。所示。 链表是通过每个结点的指针域将线性表的链表是通过每个结点的指针域将线性表的n个结点个结点按其逻辑次序链接在一起的。按其逻辑次序链接在一起的。 每一个结只包含一个指
35、针域的链表,称为单链表。每一个结只包含一个指针域的链表,称为单链表。 为操作方便,总是在链表的第一个结点之前附设一为操作方便,总是在链表的第一个结点之前附设一个头结点个头结点( (头指针头指针) )head指向第一个结点。头结点的数据指向第一个结点。头结点的数据域可以不存储任何信息域可以不存储任何信息( (或链表长度等信息或链表长度等信息) )。data next图图2-2 链表结点结构链表结点结构data :数据域,存放结点的值。:数据域,存放结点的值。next :指针:指针域,存放结点的直接后继的地址。域,存放结点的直接后继的地址。 3695headfat1100bat1300cat1305eat3700hatNULL1100370013001305bat cat eat fat hat head 图图2-3 带头结点的单链表的逻辑状态、物理存储方式带头结点的单链表的逻辑状态、物理存储方式 单链表是由表头唯一确定,因此单单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。链表可以用头指针的名字来命名。例例1 1、线性表、线性表L=(bat,cat,eat,fat,hat)其
36、其带头结点的单带头结点的单链表的逻辑状态和物理链表的逻辑状态和物理存储方式如图存储方式如图2-3所示。所示。1 结点的描述与实现结点的描述与实现 C语言中用语言中用带指针的结构体类型带指针的结构体类型来描述来描述typedef struct Lnode ElemType data; /*数据域,保存结点的值数据域,保存结点的值 */struct Lnode *next; /*指针域指针域*/LNode; /*结点的类型结点的类型 */2 结点的实现结点的实现 结点是通过动态分配和释放来的实现结点是通过动态分配和释放来的实现,即需要时分,即需要时分配,不需要时释放。实现时是分别使用配,不需要时释放。实现时是分别使用C语言提供的标语言提供的标准函数:准函数:malloc() ,realloc(),sizeof() ,free() 。动态分配动态分配 p=(LNode*)malloc(sizeof(LNode);函数函数malloc分配了一个类型为分配了一个类型为LNode的结点变量的空间,的结点变量的空间,并将其首地址放入指针变量并将其首地址放入指针变量p中。中。动态释放动态释放 free
37、(p) ;系统回收由指针变量系统回收由指针变量p所指向的内存区。所指向的内存区。P必须是最近一必须是最近一次调用次调用malloc函数时的返回值。函数时的返回值。3 最常用的基本操作及其示意图最常用的基本操作及其示意图 结点的赋值结点的赋值 LNode *p;p=(LNode*)malloc(sizeof(LNode); p-data=20; p-next=NULL ;p20NULL 常见的指针操作常见的指针操作 q=p ;pa操作前paq操作后 q=p-next ;bpa操作前操作后qbpa p=p-next ;bpa操作前操作后pba q-next=p ;cpbqa操作前操作后qbacp(a) q-next=p-next ;(a)xypbqa操作前操作后qbaxyp操作前ypxbqa操作后ypxbqa(b)操作前ypxbqa操作后ypxbqa(b)2.3.2 单线性链式的基本操作单线性链式的基本操作1 建立单链表建立单链表 假设线性表中结点的数据类型是整型,以假设线性表中结点的数据类型是整型,以32767作作为结束标志。动态地建立单链表的常用方法有如下两种:为结束标志。动态地建立单
38、链表的常用方法有如下两种:头插入法头插入法,尾插入法尾插入法。 头插入法建表头插入法建表 从一个空表开始,重复读入数据,生成新结点,从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。即入到当前链表的表头上,直到读入结束标志为止。即每每次插入的结点都作为链表的第一个结点次插入的结点都作为链表的第一个结点。算法描述算法描述LNode *create_LinkList(void) /* 头插入法创建单链表头插入法创建单链表,链表的头结点链表的头结点head作为返回值作为返回值 */ int data ;LNode *head, *p;head= (LNode *) malloc( sizeof(LNode);head-next=NULL; /* 创建链表的表头结点创建链表的表头结点head */ while (1) scanf(“%d”, &data) ;if (data=32767) break ;p= (LNode *)malloc(sizeof(LNode
39、);pdata=data; /* 数据域赋值数据域赋值 */pnext=headnext ; headnext=p ; /* 钩链钩链,新创建,新创建的结点总是作为第一个结点的结点总是作为第一个结点 */return (head);(2) 尾插入法建表尾插入法建表 头插入法建立链表虽然算法简单,但生成的链表中头插入法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将可采用尾插法建表。该方法是将新结点插入到当前链表新结点插入到当前链表的表尾,使其成为当前链表的尾结点的表尾,使其成为当前链表的尾结点。算法描述算法描述LNode *create_LinkList(void) /* 尾插入法创建单链表尾插入法创建单链表,链表的头结点链表的头结点head作为返回值作为返回值 */ int data ;LNode *head, *p, *q;head=p=(LNode *)malloc(sizeof(LNode); p-next=NULL; /* 创建单链表的表头结点创建单链表的表头结点h
40、ead */while (1) scanf(“%d”,& data);if (data=32767) break ;q= (LNode *)malloc(sizeof(LNode); qdata=data; /* 数据域赋值数据域赋值 */qnext=pnext; pnext=q; p=q ; /*钩链钩链,新创建,新创建的结点总是作为最后一个结点的结点总是作为最后一个结点*/return (head); 无论是哪种插入方法,如果要插入建立的单线性无论是哪种插入方法,如果要插入建立的单线性链表的结点是链表的结点是n个,算法的时间复杂度均为个,算法的时间复杂度均为O(n)。 对于单链表,无论是哪种操作,只要涉及到钩链对于单链表,无论是哪种操作,只要涉及到钩链( (或重新钩链或重新钩链) ),如果,如果没有明确给出直接后继没有明确给出直接后继,钩链,钩链( (或或重新钩链重新钩链) )的次序必须是的次序必须是“先右后左先右后左”。2 单链表的查找单链表的查找(1) 按序号查找按序号查找 取单链表中的第取单链表中的第i个元素。个元素。 对于单链表,不能象顺序表中那样直接按序号对于单链表,不能
41、象顺序表中那样直接按序号i访访问结点,而只能从链表的头结点出发,沿链域问结点,而只能从链表的头结点出发,沿链域next逐个逐个结点往下搜索,直到搜索到第结点往下搜索,直到搜索到第i个结点为止。因此,个结点为止。因此,链链表不是随机存取结构表不是随机存取结构。 设单链表的长度为设单链表的长度为n,要查找表中第,要查找表中第i个结点,仅个结点,仅当当1in时,时,i的值是合法的。的值是合法的。算法描述算法描述ElemType Get_Elem(LNode *L , int i) int j ; LNode *p;p=L-next; j=1; /* 使使p指向第一个结点指向第一个结点 */while (p!=NULL & jnext; j+; /* 移动指针移动指针p , j计数计数 */if (j!=i) return(-32768) ;else return(p-data); /* p为为NULL 表示表示i太大太大; ji表示表示i为为0 */移动指针移动指针p的频度:的频度:in:n次。次。时间复杂度时间复杂度: O(n)。(2) 按值查找按值查找 按值查找是在链表中,查找是否有结点
42、值等于给定按值查找是在链表中,查找是否有结点值等于给定值值key的结点的结点? ? 若有,则返回首次找到的值为若有,则返回首次找到的值为key的结点的结点的存储位置;否则返回的存储位置;否则返回NULL。查找时从开始结点出发,。查找时从开始结点出发,沿链表逐个将结点的值和给定值沿链表逐个将结点的值和给定值key作比较。作比较。算法描述算法描述LNode *Locate_Node(LNode *L,int key)/* 在以在以L为头结点的单链表中查找值为为头结点的单链表中查找值为key的第一个结点的第一个结点 */ LNode *p=Lnext;while ( p!=NULL& pdata!=key) p=pnext;if (pdata=key) return p;else printf(“所要查找的结点不存在所要查找的结点不存在!n”); retutn(NULL); 算法的执行与形参算法的执行与形参key有关,平均时间复杂度为有关,平均时间复杂度为O(n)。3 单链表的插入单链表的插入 插入运算是将值为插入运算是将值为e的新结点插入到表的第的新结点插入到表的第i个结个结点的位置上,即
43、插入到点的位置上,即插入到ai-1与与ai之间。因此,必须首先找之间。因此,必须首先找到到ai-1所在所在的结点的结点p,然后生成一个数据域为,然后生成一个数据域为e的新结点的新结点q,q结点作为结点作为p的直接后继结点。的直接后继结点。算法描述算法描述void Insert_LNode(LNode *L,int i,ElemType e) /* 在以在以L为头结点的单链表的第为头结点的单链表的第i个位置插入值为个位置插入值为e的结点的结点 */ int j=0; LNode *p,*q;p=Lnext ;while ( p!=NULL& jnext; j+; if (j!=i-1) printf(“i太大或太大或i为为0!n ”); else q=(LNode *)malloc(sizeof(LNode);qdata=e; qnext=pnext;pnext=q; 设链表的长度为设链表的长度为n,合法的插入位置是,合法的插入位置是1in。算。算法的时间主要耗费法的时间主要耗费移动指针移动指针p上,故时间复杂度亦为上,故时间复杂度亦为O(n)。4 单链表的删除单链表的删除 按序号删除按
44、序号删除 删除单链表中的第删除单链表中的第i个结点。个结点。 为了删除第为了删除第i个结点个结点ai,必须找到结点的存储地址。,必须找到结点的存储地址。该存储地址是在其直接前趋结点该存储地址是在其直接前趋结点ai-1的的next域中,因此,域中,因此,必须首先找到必须首先找到ai-1的存储位置的存储位置p,然后令,然后令pnext指向指向ai的的直接后继结点,即把直接后继结点,即把ai从链上摘下。最后释放结点从链上摘下。最后释放结点ai的的空间,将其归还给空间,将其归还给“存储池存储池”。 设单链表长度为设单链表长度为n,则删去第,则删去第i个结点仅当个结点仅当1in时时是合法的。则当是合法的。则当i=n+1时,虽然被删结点不存在,但其时,虽然被删结点不存在,但其前趋结点却存在,是终端结点。故判断条件之一是前趋结点却存在,是终端结点。故判断条件之一是pnext!=NULL。显然此算法的时间复杂度也是。显然此算法的时间复杂度也是O(n)(n)。 算法描述算法描述void Delete_LinkList(LNode *L, int i) /* 删除以删除以L为头结点的单链表中的第为头结点的
45、单链表中的第i i个结点个结点 */ int j=1; LNode *p,*q;p=L; q=L-next;while ( p-next!=NULL& jnext; j+; if (j!=i) printf(“i太大或太大或i为为0!n ”); else pnext=qnext; free(q); 按值删除按值删除 删除单链表中值为删除单链表中值为key的第一个结点。的第一个结点。 与按值查找相类似,首先要查找值为与按值查找相类似,首先要查找值为keykey的结点是的结点是否存在否存在? ? 若存在,则删除;否则返回若存在,则删除;否则返回NULL。算法描述算法描述void Delete_LinkList(LNode *L,int key)/* 删除以删除以L为头结点的单链表中值为为头结点的单链表中值为key的第一个结点的第一个结点 */ LNode *p=L, *q=Lnext;while ( q!=NULL& qdata!=key) p=q; q=qnext; if (qdata=key) p-next=q-next; free(q); else printf(“所要删除的结点不存
46、在所要删除的结点不存在!n”); 算法的执行与形参算法的执行与形参k key有关,平均时间复杂度为有关,平均时间复杂度为O(n)。 从上面的讨论可以看出,链表上实现插入和删除运从上面的讨论可以看出,链表上实现插入和删除运算,无需移动结点,仅需修改指针。解决了顺序表的插算,无需移动结点,仅需修改指针。解决了顺序表的插入或删除操作需要移动大量元素的问题。入或删除操作需要移动大量元素的问题。变形之一:变形之一: 删除单链表中值为删除单链表中值为key的所有结点。的所有结点。 与按值查找相类似,但比前面的算法更简单。与按值查找相类似,但比前面的算法更简单。基本思想基本思想:从单链表的第一个结点开始,对每个结点从单链表的第一个结点开始,对每个结点进行检查,若结点的值为进行检查,若结点的值为keykey,则删除之,然后检查下一,则删除之,然后检查下一个结点,直到所有的结点都检查。个结点,直到所有的结点都检查。 算法描述算法描述void Delete_LinkList_Node(LNode *L,int key)/* 删除以删除以L为头结点的单链表中值为为头结点的单链表中值为key的第一个结点的第一
47、个结点 */ LNode *p=L, *q=Lnext;while ( q!=NULL) if (qdata=key) p-next=q-next; free(q); q=p-next; else p=q; q=qnext; 变形之二:变形之二: 删除单链表中所有值重复的结点,使得所有结点的删除单链表中所有值重复的结点,使得所有结点的值都不相同。值都不相同。 与按值查找相类似,但比前面的算法更复杂。与按值查找相类似,但比前面的算法更复杂。基本思想基本思想:从单链表的第一个结点开始,对每个结点从单链表的第一个结点开始,对每个结点进行检查:检查链表中该结点的所有后继结点,只要有进行检查:检查链表中该结点的所有后继结点,只要有值和该结点的值相同,则删除之;然后检查下一个结点,值和该结点的值相同,则删除之;然后检查下一个结点,直到所有的结点都检查。直到所有的结点都检查。 算法描述算法描述void Delete_Node_value(LNode *L)/* 删除以删除以L为头结点的单链表中所有值相同的结点为头结点的单链表中所有值相同的结点 */ LNode *p=L-next, *q, *ptr
48、; while ( p!=NULL) /* 检查链表中所有结点检查链表中所有结点 */ *q=p, *ptr=pnext;/* 检查结点检查结点p的所有后继结点的所有后继结点ptr */while (ptr!=NULL) if (ptrdata=p-data) q-next=ptr-next; free(ptr); ptr=q-next; else q=ptr; ptr=ptrnext; p=p-next ; 5 单链表的合并单链表的合并 设有两个有序的单链表,它们的头指针分别是设有两个有序的单链表,它们的头指针分别是La 、 Lb,将它们合并为以,将它们合并为以Lc为为头指针的有序链表。合并头指针的有序链表。合并前的示意图如图前的示意图如图2-4所示。所示。15 图图2-4 两个有序的单链表两个有序的单链表La ,Lb的初始状态的初始状态-2 4 9 Lb pb-7 3 12 23 La Lcpapc合并了值为合并了值为-7,-2的结点后示意图如图的结点后示意图如图2-5所示。所示。图图2-5 合并了值为合并了值为-7 ,-2的结点后的状态的结点后的状态-2 4 9 15 Lb pc
49、pbLc-7 3 12 23 La pa算法说明算法说明 算法中算法中pa ,pb分别是待考察的两个链表的当前结分别是待考察的两个链表的当前结点,点,pc是合并过程中合并的链表的最后一个结点。是合并过程中合并的链表的最后一个结点。算法描述算法描述LNode *Merge_LinkList(LNode *La, LNode *Lb) /* 合并以合并以La, Lb为头结点的两个有序单链表为头结点的两个有序单链表 */ LNode *Lc, *pa , *pb , *pc, *ptr ;Lc=La ; pc=La ; pa=La-next ; pb=Lb-next ; while (pa!=NULL & pb!=NULL) if (pa-datadata) pc-next=pa ; pc=pa ; pa=pa-next ; /* 将将pa所指的结点合并,所指的结点合并,pa指向下一个结点指向下一个结点 */if (pa-datapb-data) pc-next=pb ; pc=pb ; pb=pb-next ; /* 将将pa所指的结点合并,所指的结点合并,pa指向下一个结点指向下一个结点
50、 */if (pa-data=pb-data) pc-next=pa ; pc=pa ; pa=pa-next ; ptr=pb ; pb=pb-next ; free(ptr) ; /* 将将pa所指的结点合并,所指的结点合并,pb所指结点删除所指结点删除 */ if (pa!=NULL) pc-next=pa ;else pc-next=pb ; /*将剩余的结点链上将剩余的结点链上*/free(Lb) ;return(Lc) ;算法分析算法分析 若若La ,Lb两个链表的长度分别是两个链表的长度分别是m,n,则链表合,则链表合并的时间复杂度为并的时间复杂度为O(m+n) 。2.3.3 循环链表循环链表 循环链表循环链表(Circular Linked List):是一种头尾相接是一种头尾相接的链表。其特点是最后一个结点的指针域指向链表的头的链表。其特点是最后一个结点的指针域指向链表的头结点,整个结点,整个链表的指针域链接成一个环链表的指针域链接成一个环。 从循环链表的任意一个结点出发都可以找到链表中从循环链表的任意一个结点出发都可以找到链表中的其它结点,使得表处理更加方便灵活。的
51、其它结点,使得表处理更加方便灵活。 图图2-6是带头结点的单循环链表的示意图。是带头结点的单循环链表的示意图。空表空表图图2-6 单循环链表示意图单循环链表示意图非空表非空表a1 a2 anhead head 循环链表的操作循环链表的操作 对于单循环链表,除链表的合并外,其它的操作和对于单循环链表,除链表的合并外,其它的操作和单线性链表基本上一致,仅仅需要在单线性链表操作算单线性链表基本上一致,仅仅需要在单线性链表操作算法基础上作以下简单修改:法基础上作以下简单修改: 判断是否是空链表:判断是否是空链表:head-next=head ; ; 判断是否是表尾结点:判断是否是表尾结点:p-next=head ; ;2.4 双向链表双向链表 双向链表双向链表(Double Linked List) : :指的是构成链表的指的是构成链表的每个结点中设立两个指针域:一个指向其直接前趋的指每个结点中设立两个指针域:一个指向其直接前趋的指针域针域prior,一个指向其直接后继的指针域,一个指向其直接后继的指针域next。这样形。这样形成的链表中有两个方向不同的链,故称为成的链表中有两个方向不同的链,
52、故称为双向链表双向链表。 和单链表类似,双向链表一般增加头指针也能使双和单链表类似,双向链表一般增加头指针也能使双链表上的某些运算变得方便。链表上的某些运算变得方便。 将头结点和尾结点链接起来也能构成循环链表,并将头结点和尾结点链接起来也能构成循环链表,并称之为双向循环链表。称之为双向循环链表。 双向链表是为了克服单链表的单向性的缺陷而引入双向链表是为了克服单链表的单向性的缺陷而引入的。的。1 双向链表的结点及其类型定义双向链表的结点及其类型定义 双向链表的结点的类型定义如下。其结点形式如双向链表的结点的类型定义如下。其结点形式如图图2-7所示所示,带头结点的双向链表的形式如图,带头结点的双向链表的形式如图2-8所示所示。typedef struct Dulnode ElemType data ;struct Dulnode *prior , *next ;DulNode ;data nextprior图图2-7 双向链表结点形式双向链表结点形式非空双向链表非空双向链表heada2a1an空双向链表空双向链表head图图2-8 带头结点的双向链表形式带头结点的双向链表形式 双向链表结构
53、具有对称性,设双向链表结构具有对称性,设p指向双向链表中的某指向双向链表中的某一结点,则其对称性可用下式描述:一结点,则其对称性可用下式描述:(p-prior)-next=p=(p-next)-prior ; 结点结点p的存储位置存放在其的存储位置存放在其直接前趋结点直接前趋结点p-prior的的直接后继指针域直接后继指针域中,同时也存放在中,同时也存放在其直接后继结点其直接后继结点p-next的的直接前趋指针域直接前趋指针域中。中。2 双向链表的基本操作双向链表的基本操作(1) 双向链表的插入双向链表的插入 将值为将值为e的结点插入双向链表中。的结点插入双向链表中。插入前后链表的变化如图插入前后链表的变化如图2-9所示。所示。Sepaiai+1Sepaiai+1图图2-9 双向链表的插入双向链表的插入 插入时仅仅指出直接前驱结点插入时仅仅指出直接前驱结点,钩链时必须注意,钩链时必须注意先后次序是先后次序是: “先右后左先右后左” 。部分语句组如下:。部分语句组如下:S=(DulNode *)malloc(sizeof(DulNode); S-data=e;S-next=p-next;
54、 p-next-prior=S;p-next=S; S-prior=p; /* 钩链次序非常重要钩链次序非常重要 */ 插入时同时指出直接前驱结点插入时同时指出直接前驱结点p和直接后继结点和直接后继结点q,钩链时无须注意先后次序。部分语句组如下:,钩链时无须注意先后次序。部分语句组如下:S=(DulNode *)malloc(sizeof(DulNode);S-data=e;p-next=S; S-next=q;S-prior=p; q-prior=S; (2) 双向链表的结点删除双向链表的结点删除 设要删除设要删除的结点为的结点为p ,删除时可以不引入新的辅助,删除时可以不引入新的辅助指针变量,可以直接先断链,再释放结点。部分语句组指针变量,可以直接先断链,再释放结点。部分语句组如下:如下:p-prior-next=p-next;p-next-prior=p-prior;free(p);注意:注意: 与单链表的插入和删除操作不同的是,在双向链表与单链表的插入和删除操作不同的是,在双向链表中中插入插入和和删除删除必须同时必须同时修改两个方向上的指针域的指向修改两个方向上的指针域的指向。
55、2.5 一元多项式的表示和相加一元多项式的表示和相加1 一元多项式的表示一元多项式的表示 一元多项式一元多项式 p(x)=p0+p1x+p2x2+ +pnxn ,由,由n+1个个系数唯一确定系数唯一确定。则在计算机中可。则在计算机中可用线性表用线性表(p0 ,p1 ,p2 , ,pn )表示表示。既然是。既然是线性表线性表,就可以用顺序表和链表,就可以用顺序表和链表来实现。两种不同实现方式的元素类型定义如下:来实现。两种不同实现方式的元素类型定义如下:(1) 顺序存储表示的类型顺序存储表示的类型typedef struct float coef; /*系数部分系数部分*/int expn; /*指数部分指数部分*/ ElemType ;(2) 链式存储表示的类型链式存储表示的类型typedef struct ploy float coef ; /*系数部分系数部分*/int expn ; /*指数部分指数部分*/struct ploy *next ; Ploy ;2 一元多项式的相加一元多项式的相加 不失一般性,设有两个一元多项式不失一般性,设有两个一元多项式:P(x)=p0+p1x+
56、p2x2+ +pnxn ,Q(x)=q0+q1x+q2x2+ +qmxm (mnext ; pb=Lb-next ;while (pa!=NULL&pb!=NULL) if (pa-expnexpn) pc-next=pa ; pc=pa ; pa=pa-next ; /* 将将pa所指的结点合并,所指的结点合并,pa指向下一个结点指向下一个结点 */if (pa-expnpb-expn) pc-next=pb ; pc=pb ; pb=pb-next ; /* 将将pb所指的结点合并,所指的结点合并,pb指向下一个结点指向下一个结点 */else x=pa-coef+pb-coef ; if (abs(x)next ; free(ptr) ; ptr=pb ; pb=pb-next ; free(ptr) ; else /* 如果系数和不为如果系数和不为0,修改其中一个结,修改其中一个结点的系数域,删除另一个结点点的系数域,删除另一个结点 */ pc-next=pa ; pa-coef=x ; pc=pa ; pa=pa-next ; ptr=pb ; pb=pb-next ; fr
57、ee(pb) ; /* end of while */ if (pa=NULL) pc-next=pb ;else pc-next=pa ;return (Lc) ; 算法之二:算法之二: 对两个多项式链表进行相加,生成一个新的相加后对两个多项式链表进行相加,生成一个新的相加后的结果多项式链表,原来两个多项式链表依然存在,的结果多项式链表,原来两个多项式链表依然存在,不发生任何改变,如果要再对原来两个多项式进行其不发生任何改变,如果要再对原来两个多项式进行其它操作也不影响。它操作也不影响。算法描述算法描述Ploy *add_ploy(ploy *La, ploy *Lb) /* 将以将以La ,Lb为头指针表示的一元多项式相加,生成一个新为头指针表示的一元多项式相加,生成一个新的结果多项式的结果多项式 */ ploy *Lc , *pc , *pa , *pb , *p ; float x ;Lc=pc=(ploy *)malloc(sizeof(ploy) ; pa=La-next ; pb=Lb-next ;while (pa!=NULL&pb!=NULL) if (pa-expn
58、expn) p=(ploy *)malloc(sizeof(ploy) ; p-coef=pa-coef ; p-expn=pa-expn ; p-next=NULL ; /* 生成一个新的结果结点并赋值生成一个新的结果结点并赋值 */ pc-next=p ; pc=p ; pa=pa-next ; /* 生成的结点插入到结果链表的最后,生成的结点插入到结果链表的最后,pa指向下指向下一个结点一个结点 */if (pa-expnpb-expn) p=(ploy *)malloc(sizeof(ploy) ; p-coef=pb-coef ; p-expn=pb-expn ; p-next=NULL ; /* 生成一个新的结果结点并赋值生成一个新的结果结点并赋值 */ pc-next=p ; pc=p ; pb=pb-next ; /* 生成的结点插入到结果链表的最后,生成的结点插入到结果链表的最后,pb指指向下一个结点向下一个结点 */if (pa-expn=pb-expn) x=pa-coef+pb-coef ; if (abs(x)next ; pb=pb-next ; else
59、/* 若系数和不为若系数和不为0,生成的结点插入到结,生成的结点插入到结果链表的最后,果链表的最后, pa, pb分别直接后继结点分别直接后继结点 */ p=(ploy *)malloc(sizeof(ploy) ; p-coef=x ; p-expn=pb-expn ; p-next=NULL ; /* 生成一个新的结果结点并赋值生成一个新的结果结点并赋值 */ pc-next=p ; pc=p ; pa=pa-next ; pb=pb-next ; /* end of while */ if (pb!=NULL) while(pb!=NULL) p=(ploy *)malloc(sizeof(ploy) ; p-coef=pb-coef ; p-expn=pb-expn ; p-next=NULL ; /* 生成一个新的结果结点并赋值生成一个新的结果结点并赋值 */ pc-next=p ; pc=p ; pb=pb-next ; if (pa!=NULL) while(pa!=NULL) p=(ploy *)malloc(sizeof(ploy) ; p-coef=pb-coef
60、; p-expn=pa-expn ; p-next=NULL ; /* 生成一个新的结果结点并赋值生成一个新的结果结点并赋值 */ pc-next=p ; pc=p ; pa=pa-next ; return (Lc) ; 习习 题题 二二1 简述下列术语:线性表,顺序表,链表。简述下列术语:线性表,顺序表,链表。2 何时选用顺序表,何时选用链表作为线性表的存储何时选用顺序表,何时选用链表作为线性表的存储结构合适结构合适?各自的主要优缺点是什么各自的主要优缺点是什么?3 在顺序表中插入和删除一个结点平均需要移动多少在顺序表中插入和删除一个结点平均需要移动多少个结点个结点?具体的移动次数取决于哪两个因素具体的移动次数取决于哪两个因素?4 链表所表示的元素是否有序链表所表示的元素是否有序?如有序,则有序性体如有序,则有序性体现于何处现于何处?链表所表示的元素是否一定要在物理上是相链表所表示的元素是否一定要在物理上是相邻的邻的?有序表的有序性又如何理解有序表的有序性又如何理解?5 设顺序表设顺序表L是递增有序表,试写一算法,将是递增有序表,试写一算法,将x插入插入到到L中并使中并使L仍是递增
61、有序表。仍是递增有序表。6 写一求单链表的结点数目写一求单链表的结点数目ListLength(L)的算法。的算法。7 写一算法将单链表中值重复的结点删除写一算法将单链表中值重复的结点删除,使所得的,使所得的结果链表中所有结点的值均不相同。结果链表中所有结点的值均不相同。 8 写一算法从一给定的向量写一算法从一给定的向量A删除值在删除值在x到到y(xy)之间之间的所有元素的所有元素(注意:注意:x和和y是给定的参数,可以和表中的是给定的参数,可以和表中的元素相同,也可以不同元素相同,也可以不同)。 9 设设A和和B是两个按元素值递增有序的单链表是两个按元素值递增有序的单链表,写一写一算法将算法将A和和B归并为按按元素值递减有序的单链表归并为按按元素值递减有序的单链表C,试,试分析算法的时间复杂度。分析算法的时间复杂度。第第3章章 栈和队列栈和队列 栈和队列是两种应用非常广泛的数据结构栈和队列是两种应用非常广泛的数据结构,它们都,它们都来自线性表数据结构,来自线性表数据结构,都是都是“操作受限操作受限”的线性表的线性表。栈在计算机的实现有多种方式:栈在计算机的实现有多种方式: 硬堆栈硬堆栈
62、:利用:利用CPU中的某些寄存器组或类似的硬中的某些寄存器组或类似的硬件或使用内存的特殊区域来实现。这类堆栈容量有限,件或使用内存的特殊区域来实现。这类堆栈容量有限,但速度很快;但速度很快; 软堆栈软堆栈:这类堆栈主要在内存中实现。堆栈容量:这类堆栈主要在内存中实现。堆栈容量可以达到很大。在实现方式上,又有可以达到很大。在实现方式上,又有动态方式动态方式和和静态静态方式方式两种。两种。 本章将讨论栈和队列的基本概念本章将讨论栈和队列的基本概念、存储结构存储结构、基本基本操作以及这些操作的具体实现操作以及这些操作的具体实现。3.1 栈栈3.1.1 栈的基本概念栈的基本概念1 栈的概念栈的概念 栈栈(Stack):是限制在表的一端进行插入和删除操是限制在表的一端进行插入和删除操作的线性表。又称为后进先出作的线性表。又称为后进先出LIFO (Last In First Out)或先进后出或先进后出FILO (First In Last Out)线性线性表。表。 栈顶栈顶(Top):允许进行插入、删除操作的一端,又允许进行插入、删除操作的一端,又称为表尾。用栈顶指针称为表尾。用栈顶指针(top
63、)来指示栈顶元素来指示栈顶元素。 栈底栈底(Bottom):是固定端,又称为表头。是固定端,又称为表头。 空栈空栈:当表中没有元素时称为空栈。当表中没有元素时称为空栈。 设栈设栈S=(a1,a2,an),则,则a1称称为栈底元素,为栈底元素,an为栈顶元素,如图为栈顶元素,如图3-1所示。所示。 栈中元素按栈中元素按a1,a2,an的次序的次序进栈,退栈的第一个元素应为栈顶元进栈,退栈的第一个元素应为栈顶元素。即栈的修改是按后进先出的原则素。即栈的修改是按后进先出的原则进行的。进行的。图图3-1 顺序顺序栈示意图栈示意图a1a2aianbottomtop进栈(进栈(push)出栈出栈(pop)2 栈的抽象数据类型定义栈的抽象数据类型定义ADT Stack数据对象:数据对象:D = ai|aiElemSet, i=1,2,n,n0 数据关系:数据关系:R =|ai-1,aiD, i=2,3,n 基本操作:初始化、进栈、出栈、取栈顶元素等基本操作:初始化、进栈、出栈、取栈顶元素等 ADT Stack 栈的顺序存储结构简称为顺序栈,和线性表相类似,栈的顺序存储结构简称为顺序栈,和线性表相类似
64、,用用一维数组一维数组来来存储栈存储栈。根据数组是否可以根据需要增大,。根据数组是否可以根据需要增大,又可分为又可分为静态顺序栈静态顺序栈和和动态顺序栈动态顺序栈。 静态顺序栈静态顺序栈实现简单,但不能根据需要增大栈的实现简单,但不能根据需要增大栈的存储空间;存储空间; 动态顺序栈动态顺序栈可以根据需要增大栈的存储空间,但可以根据需要增大栈的存储空间,但实现稍为复杂。实现稍为复杂。3.1.2 栈的顺序存储表示栈的顺序存储表示 采用采用动态动态一维数组一维数组来来存储栈存储栈。所谓动态,指的是栈。所谓动态,指的是栈的大小可以根据需要增加。的大小可以根据需要增加。 用用bottom表示栈底指针,栈底固定不变的;栈顶表示栈底指针,栈底固定不变的;栈顶则随着进栈和退栈操作而变化。用则随着进栈和退栈操作而变化。用top( (称为栈顶指针称为栈顶指针) )指示当前栈顶位置。指示当前栈顶位置。 用用top=bottom作为栈空的标记作为栈空的标记,每次,每次top指向栈顶指向栈顶数组中的下一个存储位置数组中的下一个存储位置。 结点进栈结点进栈:首先将数据元素保存到栈顶首先将数据元素保存到栈顶(top
65、所指所指的当前位置的当前位置),然后执行,然后执行top加加1,使,使top指向栈顶的下指向栈顶的下一个存储位置一个存储位置;3.1.2.1 栈的动态顺序存储表示栈的动态顺序存储表示 结点出栈结点出栈:首先执行首先执行top减减1,使,使top指向栈顶元指向栈顶元素的存储位置,然后将栈顶元素取出素的存储位置,然后将栈顶元素取出。 图图3-2是一个动态栈的变化示意图是一个动态栈的变化示意图。图图3-2 (动态动态)堆栈变化示意图堆栈变化示意图空栈空栈bottomtop元素元素a进栈进栈bottomtopa元素元素b,c进栈进栈bottomtopabc元素元素c退栈退栈bottomtopabbottomtopabdef元素元素d,e,f进栈进栈基本操作的实现基本操作的实现1 栈的类型定义栈的类型定义#define STACK_SIZE 100 /* 栈初始向量大小栈初始向量大小 */#define STACKINCREMENT 10 /* 存储空间分配增量存储空间分配增量 */#typedef int ElemType ;typedef struct sqstack ElemType *b
66、ottom; /* 栈不存在时值为栈不存在时值为NULL */ElemType *top; /* 栈顶指针栈顶指针 */int stacksize ; /* 当前已分配空间,以元素为单位当前已分配空间,以元素为单位 */SqStack ;2 栈的初始化栈的初始化Status Init_Stack(void) SqStack S ;S.bottom=(ElemType *)malloc(STACK_SIZE *sizeof(ElemType);if (! S.bottom) return ERROR;S.top=S.bottom ; /* 栈空时栈顶和栈底指针相同栈空时栈顶和栈底指针相同 */S. stacksize=STACK_SIZE; return OK ;3 压栈压栈(元素进栈元素进栈)Status push(SqStack S , ElemType e) if (S.top-S.bottom=S. stacksize-1) S.bottom=(ElemType *)realloc(S. STACKINCREMENT+STACK_SIZE) *sizeof(ElemType);
67、/* 栈满,追加存储空间栈满,追加存储空间 */if (! S.bottom) return ERROR; S.top=S.bottom+S. stacksize ;S. stacksize+=STACKINCREMENT ; *S.top=e; S.top+ ; /* /* 栈顶指针加栈顶指针加1,e成为新的栈顶成为新的栈顶 * */ /return OK;4 弹栈弹栈( (元素元素出栈出栈) )Status pop( SqStack S, ElemType *e ) /*弹出栈顶元素弹出栈顶元素*/ if ( S.top= S.bottom ) return ERROR ; /* 栈空,返回失败标志栈空,返回失败标志 */S.top- ; e=*S. top ; return OK ; 采用采用静态静态一维数组一维数组来来存储栈存储栈。 栈底固定不变的,而栈顶则随着进栈和退栈操作变栈底固定不变的,而栈顶则随着进栈和退栈操作变化的,化的, 栈底固定不变的;栈顶则随着进栈和退栈操作而栈底固定不变的;栈顶则随着进栈和退栈操作而变化,用一个整型变量变化,用一个整型变量top( (称为栈顶指
68、针称为栈顶指针) )来指示当前来指示当前栈顶位置。栈顶位置。 用用top=0表示栈空的初始状态表示栈空的初始状态,每次,每次top指向栈顶指向栈顶在数组中的存储位置在数组中的存储位置。 结点进栈结点进栈:首先执行首先执行top加加1,使,使top指向新的栈指向新的栈顶位置顶位置,然后将数据元素保存到栈顶然后将数据元素保存到栈顶(top所指的当前所指的当前位置位置)。3.1.2.2 栈的静态顺序存储表示栈的静态顺序存储表示 结点出栈结点出栈:首先首先把把top指向的栈顶元素取出指向的栈顶元素取出,然然后执行后执行top减减1,使,使top指向新的栈顶位置指向新的栈顶位置。 若栈的数组有若栈的数组有Maxsize个元素个元素,则,则top=Maxsize-1时时栈满栈满。图。图3-3是一个大小为是一个大小为5的栈的变化示意图的栈的变化示意图。图图3-3 静态静态堆栈变化示意图堆栈变化示意图空栈空栈bottomtopTop=11个元素进栈个元素进栈bottomtopaTop=33个元素进栈个元素进栈bottomtopabcTop=4栈满栈满bottomtopabedTop=2元素元素c进栈进
69、栈bottomtopab基本操作的实现基本操作的实现1 栈的类型定义栈的类型定义# define MAX_STACK_SIZE 100 /* 栈向量大小栈向量大小 */# typedef int ElemType ;typedef struct sqstack ElemType stack_arrayMAX_STACK_SIZE ;int top;SqStack ;2 栈的初始化栈的初始化SqStack Init_Stack(void) SqStack S ;S.bottom=S.top=0 ; return(S) ;3 压栈压栈(元素进栈元素进栈)Status push(SqStack S , ElemType e) /* /* 使数据元素使数据元素e进栈成为新的栈顶进栈成为新的栈顶 * */ / if (S.top=MAX_STACK_SIZE-1) return ERROR; /* 栈满,返回错误标志栈满,返回错误标志 */S.top+ ; /* /* 栈顶指针加栈顶指针加1 */*/S.stack_arrayS.top=e ; /* /* e成为新的栈顶成为新的栈顶 * */
70、/return OK; /* 压栈成功压栈成功 */4 弹栈弹栈( (元素元素出栈出栈) )Status pop( SqStack S, ElemType *e ) /*弹出栈顶元素弹出栈顶元素*/ if ( S.top=0 )return ERROR ; /* 栈空,返回错误标志栈空,返回错误标志 */*e=S.stack_arrayS.top ; S.top- ; return OK ; 当栈满时做进栈运算必定产生空间溢出,简称当栈满时做进栈运算必定产生空间溢出,简称“上上溢溢”。上溢是一种出错状态,应设法避免。上溢是一种出错状态,应设法避免。 当栈空时做退栈运算也将产生溢出,简称当栈空时做退栈运算也将产生溢出,简称“下溢下溢”。下溢则可能是正常现象,因为栈在使用时,其初态或终下溢则可能是正常现象,因为栈在使用时,其初态或终态都是空栈,所以下溢常用来作为控制转移的条件。态都是空栈,所以下溢常用来作为控制转移的条件。1 栈的链式表示栈的链式表示 栈的栈的链式存储结构链式存储结构称为链栈,是运算受限的单链表。称为链栈,是运算受限的单链表。其插入和删除操作只能在表头位置上进行。因此,链栈
71、其插入和删除操作只能在表头位置上进行。因此,链栈没有必要像单链表那样附加头结点,栈顶指针没有必要像单链表那样附加头结点,栈顶指针top就是就是链表的头指针。图链表的头指针。图3-4是栈的链式存储表示形式是栈的链式存储表示形式。3.1.3 栈的链式存储表示栈的链式存储表示空栈空栈top 非空栈非空栈top a4 a3 a1 a2图图图图3-4 3-4 链链链链栈存储形式栈存储形式链栈的链栈的结点类型说明如下:结点类型说明如下:typedef struct Stack_Node ElemType data ;struct Stack_Node *next ; Stack_Node ;2 链栈基本操作的实现链栈基本操作的实现(1) 栈的初始化栈的初始化Stack_Node *Init_Link_Stack(void) Stack_Node *top ;top=(Stack_Node *)malloc(sizeof(Stack_Node ) ;top-next=NULL ; return(top) ;(2) 压栈压栈(元素进栈元素进栈)Status push(Stack_Node *top ,
72、 ElemType e) Stack_Node *p ;p=(Stack_Node *)malloc(sizeof(Stack_Node) ; if (!p) return ERROR; /* 申请新结点失败,返回错误标志申请新结点失败,返回错误标志 */p-data=e ; p-next=top-next ; top-next=p ; /* 钩链钩链 */return OK;(3) 弹栈弹栈( (元素元素出栈出栈) )Status pop(Stack_Node *top , ElemType *e)/* /* 将栈顶元素将栈顶元素出出栈栈 * */ / Stack_Node *p ;ElemType e ;if (top-next=NULL )return ERROR ; /* 栈空,返回错误标志栈空,返回错误标志 */p=top-next ; e=p-data ; /* /* 取栈顶元素取栈顶元素 * */ /top-next=p-next ; /* /* 修改栈顶指针修改栈顶指针 * */ /free(p) ; return OK ;3.2 栈的应用栈的应用 由于栈具有的由于栈具
73、有的“后进先出后进先出”的固有特性,因此,栈的固有特性,因此,栈成为程序设计中常用的工具和数据结构。以下是几个栈成为程序设计中常用的工具和数据结构。以下是几个栈应用的例子。应用的例子。3.2.1 数制转换数制转换 十进制整数十进制整数N向其它进制数向其它进制数d(二二、八八、十六十六) )的转换的转换是计算机实现计算的基本问题。是计算机实现计算的基本问题。转换法则转换法则:该转换法则对应于一个简单算法原理该转换法则对应于一个简单算法原理: :n=(n div d)*d+n mod d 其中:其中:div为整除运算为整除运算, ,mod为求余运算为求余运算例如例如 (1348)10= (2504)8,其运算过程如下:其运算过程如下: n n div 8 n mod 8 1348 168 4 168 21 0 21 2 5 2 0 2 采用静态顺序栈方式实现采用静态顺序栈方式实现 void conversion(int n , int d) /*将将十进制整数十进制整数N转换为转换为d(2或或8)进制数进制数*/ SqStack S ; int k, *e ;S=Init_Stack();
74、while (n0) k=n%d ; push(S , k) ; n=n/d ; /* 求出所有的余求出所有的余数,数,进栈进栈 */while (S.top!=0) /* 栈不空时出栈,输出栈不空时出栈,输出 */ pop(S, e) ; printf(“%1d” , *e) ; 3.2.2 括号匹配问题括号匹配问题 在文字处理软件或编译程序设计时,常常需要检查在文字处理软件或编译程序设计时,常常需要检查一个字符串或一个表达式中的括号是否相匹配一个字符串或一个表达式中的括号是否相匹配?匹配思想匹配思想:从左至右扫描一个字符串从左至右扫描一个字符串(或表达式或表达式),则,则每个右括号将与最近遇到的那个左括号相匹配每个右括号将与最近遇到的那个左括号相匹配。则可以。则可以在从左至右扫描过程中把所遇到的左括号存放到堆栈中。在从左至右扫描过程中把所遇到的左括号存放到堆栈中。每当遇到一个右括号时,就将它与栈顶的左括号每当遇到一个右括号时,就将它与栈顶的左括号(如果如果存在存在)相匹配,同时从栈顶删除该左括号。相匹配,同时从栈顶删除该左括号。算法思想算法思想:设置一个栈,当读到左括号时,左括号进
75、设置一个栈,当读到左括号时,左括号进栈。当读到右括号时,则从栈中弹出一个元素,与读到栈。当读到右括号时,则从栈中弹出一个元素,与读到的左括号进行匹配,若匹配成功,继续读入;否则匹配的左括号进行匹配,若匹配成功,继续读入;否则匹配失败,返回失败,返回FLASE。算法描述算法描述#define TRUE 0#define FLASE -1SqStack S ; S=Init_Stack() ; /*堆栈初始化堆栈初始化*/int Match_Brackets( ) char ch , x ;scanf(“%c” , &ch) ; while (asc(ch)!=13) if (ch=()|(ch=) push(S , ch) ; else if (ch=) x=pop(S) ; if (x!=) printf(“括号不匹配括号不匹配”) ; return FLASE ; else if (ch=) x=pop(S) ; if (x!=() printf(“(括号不匹配括号不匹配”) ; return FLASE ; if (S.top!=0) printf(“括号数量不匹配!括号数量不匹配
76、!”) ; return FLASE ;else return TRUE ; 3.2.2 栈与递归调用的实现栈与递归调用的实现 栈的另一个重要应用是在程序设计语言中实现递归栈的另一个重要应用是在程序设计语言中实现递归调用。调用。 递归调用递归调用:一个函数一个函数( (或过程或过程) )直接或间接地调用直接或间接地调用自己本身,简称自己本身,简称递归递归(Recursive)。 递归递归是程序设计中的一个强有力的工具。因为递归是程序设计中的一个强有力的工具。因为递归函数结构清晰,程序易读,正确性很容易得到证明。函数结构清晰,程序易读,正确性很容易得到证明。 为了使递归调用不至于无终止地进行下去,实际上为了使递归调用不至于无终止地进行下去,实际上有效的递归调用函数有效的递归调用函数( (或过程或过程) )应包括两部分:应包括两部分:递推规则递推规则( (方法方法) ),终止条件终止条件。例如:求例如:求n!Fact(n)=1 当当n=0时时 终止条件终止条件n*fact(n-1) 当当n0时时 递推规则递推规则 为保证递归调用正确执行,系统设立一个为保证递归调用正确执行,系统设立一个“递
77、归工递归工作栈作栈”,作为整个递归调用过程期间使用的数据存储区。,作为整个递归调用过程期间使用的数据存储区。 每一层递归包含的信息如:每一层递归包含的信息如:参数参数、局部变量局部变量、上一上一层的返回地址构成层的返回地址构成一个一个“工作记录工作记录” 。每进入一层递归,。每进入一层递归,就产生一个新的工作记录压入栈顶;每退出一层递归,就产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录。就从栈顶弹出一个工作记录。 从被调函数返回调用函数的一般步骤:从被调函数返回调用函数的一般步骤:(1) 若栈为空,则执行正常返回。若栈为空,则执行正常返回。 从栈顶弹出一个工作记录。从栈顶弹出一个工作记录。 将将“工作记录工作记录”中的参数值、局部变量值赋给相中的参数值、局部变量值赋给相应的变量;读取返回地址。应的变量;读取返回地址。 将函数值赋给相应的变量。将函数值赋给相应的变量。(5) 转移到返回地址。转移到返回地址。1 队列的基本概念队列的基本概念 队列队列(Queue):也是运算受限的线性表。是一种:也是运算受限的线性表。是一种先先进先出进先出(First In Firs
78、t Out ,简称,简称FIFO)的线性表。只允的线性表。只允许在表的一端进行插入,而在另一端进行删除。许在表的一端进行插入,而在另一端进行删除。 队首队首(front) :允许进行删除的一端称为队首。:允许进行删除的一端称为队首。 队尾队尾(rear) :允许进行插入的一端称为队尾。:允许进行插入的一端称为队尾。例如:排队购物。操作系统中的作业排队。先进入例如:排队购物。操作系统中的作业排队。先进入队列的成员总是先离开队列。队列的成员总是先离开队列。3.3 队队 列列3.3.1 队列及其基本概念队列及其基本概念 队列中没有元素时称为空队列。在空队列中依次队列中没有元素时称为空队列。在空队列中依次加入元素加入元素a1, a2, , an之后,之后,a1是队首元素,是队首元素,an是队尾元是队尾元素。显然退出队列的次序也只能是素。显然退出队列的次序也只能是a1, a2, , an ,即队,即队列的修改是依列的修改是依先进先出先进先出的原则进行的,如图的原则进行的,如图3-5所示。所示。a1 , a2 , , an出队出队入队入队队尾队尾队首队首图图3-5 队列示意图队列示意图2 队列的抽
79、象数据类型定义队列的抽象数据类型定义ADT Queue数据对象:数据对象:D = ai|aiElemSet, i=1, 2, , n, n = 0 数据关系:数据关系:R = | ai-1, aiD, i=2,3,n 约定约定a1端为队首,端为队首,an端为队尾。端为队尾。基本操作:基本操作:Create():创建一个空队列:创建一个空队列;EmptyQue():若队列为空:若队列为空,则返回则返回true ,否则返否则返回回flase ;InsertQue(x) :向队尾插入元素:向队尾插入元素x;DeleteQue(x) :删除队首元素:删除队首元素x; ADT Queue3.3.2 队列的顺序表示和实现队列的顺序表示和实现 利用利用一组连续的存储单元一组连续的存储单元(一维数组一维数组) 依次存放从队依次存放从队首到队尾的各个元素,称为首到队尾的各个元素,称为顺序队列顺序队列。 对于队列,和顺序栈相类似,也有动态和静态之分。对于队列,和顺序栈相类似,也有动态和静态之分。本部分介绍的是本部分介绍的是静态顺序队列静态顺序队列,其类型定义如下:,其类型定义如下:#define MAX_
80、QUEUE_SIZE 100typedef struct queue ElemType Queue_arrayMAX_QUEUE_SIZE ;int front ;int rear ;SqQueue;3.3.2.1 队列的顺序队列的顺序存储结构存储结构 设立一个设立一个队首指针队首指针front ,一个,一个队尾指针队尾指针rear ,分,分别指向队首和队尾元素别指向队首和队尾元素。 初始化初始化:front=rear=0。 入队入队:将新元素插入将新元素插入rear所指的位置,然后所指的位置,然后rear加加1。 出队出队:删去删去front所指的元素,然后加所指的元素,然后加1并返回被删并返回被删元素。元素。 队列为空队列为空:front=rear。 队满队满:rear=MAX_QUEUE_SIZE-1或或front=rear。 在非空队列里,队首指针始终指向队头元素,而队在非空队列里,队首指针始终指向队头元素,而队尾指针始终指向队尾元素的下一位置。尾指针始终指向队尾元素的下一位置。 顺序队列中存在顺序队列中存在“假溢出假溢出”现象。因为在入队和出现象。因为在入队和出队操作中,头、
81、尾指针只增加不减小,致使被删除元素队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为向量空间的上界而不能做入队操作。该现象称为假溢出假溢出。如图如图3-6所示是数组大小为所示是数组大小为5的顺序队列中队首的顺序队列中队首、队尾指队尾指针和队列中元素的变化情况。针和队列中元素的变化情况。(a) 空队列空队列Q.frontQ.rear(b)入队入队3个元素个元素a3a2a1Q.frontQ.rear(c) 出队出队3个元素个元素Q.frontQ.rear(d) 入队入队2个元素个元素a5a4Q.frontQ.rear图图3-6 队列示意图队列示意图3.3.2.2 循环队列循环队列 为充分利用向量空间,克服上述为充分利用向量空间,克服上述“假溢出假溢出”现象的现象的方法是:将为队列分配的方法是:将为队列分配的向量空间看成为一个首尾相接向量空间看成为一个首尾相接的圆
82、环的圆环,并称这种队列为,并称这种队列为循环队列循环队列(Circular Queue)。 在循环队列中进行出队、入队操作时,队首、队尾在循环队列中进行出队、入队操作时,队首、队尾指针仍要加指针仍要加1,朝前移动。只不过当队首、队尾指针指,朝前移动。只不过当队首、队尾指针指向向量上界向向量上界(MAX_QUEUE_SIZE-1)时,其加时,其加1操作的结操作的结果是指向向量的下界果是指向向量的下界0。这种循环意义下的加这种循环意义下的加1操作可以描述为:操作可以描述为:if (i+1=MAX_QUEUE_SIZE) i=0;else i+ ;其中:其中: i代表队首指针代表队首指针(front)或队尾指针或队尾指针(rear)用模运算可简化为:用模运算可简化为:i=(i+1)%MAX_QUEUE_SIZE ; 显然,为循环队列所分配的空间可以被充分利用,显然,为循环队列所分配的空间可以被充分利用,除非向量空间真的被队列元素全部占用,否则不会上溢。除非向量空间真的被队列元素全部占用,否则不会上溢。因此,真正实用的顺序队列是循环队列。因此,真正实用的顺序队列是循环队列。 例:设例:设有循环
83、队列有循环队列QU0,5,其初始状态是,其初始状态是front=rear=0,各种操作后队列的头、尾指针的状态变,各种操作后队列的头、尾指针的状态变化情况如下图化情况如下图3-7所示。所示。 123450(a) 空队列空队列frontrear123450(b) d, e, b, g入入队队frontdebgrear123450(c) d, e出队出队bgfrontrear 入队时尾指针向前追赶头指针,出队时头指针向前入队时尾指针向前追赶头指针,出队时头指针向前追赶尾指针,故队空和队满时头尾指针均相等。因此,追赶尾指针,故队空和队满时头尾指针均相等。因此,无法通过无法通过front=rear来判断队列来判断队列“空空”还是还是“满满”。解。解决此问题的方法是:约定入队前,测试尾指针在循环意决此问题的方法是:约定入队前,测试尾指针在循环意义下加义下加1后是否等于头指针,若相等则认为队满后是否等于头指针,若相等则认为队满。即。即: rear所指的单元始终为空。所指的单元始终为空。123450(d) i, j, k入入队队bgfrontijkrear123450(e) b, g出队出队ijkr
84、earfront123450(f) r, p, s, t入队入队ijkfrontrprear图图3-7 循环队列操作及指针变化情况循环队列操作及指针变化情况 循环队列为空循环队列为空:front=rear 。 循环队列满循环队列满:(rear+1)%MAX_QUEUE_SIZE =front。循环队列的基本操作循环队列的基本操作1 循环队列的初始化循环队列的初始化SqQueue Init_CirQueue(void) SqQueue Q ;Q.front=Q.rear=0; return(Q) ;2 入队操作入队操作Status Insert_CirQueue(SqQueue Q , ElemType e) /* /* 将数据元素将数据元素e插入到循环队列插入到循环队列Q的队尾的队尾 * */ / if (Q.rear+1)%MAX_QUEUE_SIZE= Q.front)return ERROR; /* 队满,返回错误标志队满,返回错误标志 */Q.Queue_arrayQ.rear=e ; /* /* 元素元素e入队入队 * */ /Q.rear=(Q.rear+1)% MAX_Q
85、UEUE_SIZE ; /* 队尾指针向前移动队尾指针向前移动 */return OK; /* 入队成功入队成功 */3 出队操作出队操作Status Delete_CirQueue(SqQueue Q, ElemType *x ) /* /* 将循环队列将循环队列Q的队首元素出队的队首元素出队 * */ / if (Q.front+1= Q.rear)return ERROR ; /* 队空,返回错误标志队空,返回错误标志 */*x=Q.Queue_arrayQ.front ; /* /* 取队首元素取队首元素 * */ /Q.front=(Q.front+1)% MAX_QUEUE_SIZE ; /* 队首指针向前移动队首指针向前移动 */return OK ;1 队列的链式存储表示队列的链式存储表示 队列的链式存储结构简称为链队列,它是限制仅队列的链式存储结构简称为链队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表。在表头进行删除操作和表尾进行插入操作的单链表。 需要两类不同的结点需要两类不同的结点:数据元素数据元素结点结点,队列的队列的队首队首指针和队尾指针指针和队
86、尾指针的结点的结点,如图,如图3-8所示。所示。3.3.3 队列的链式表示和实现队列的链式表示和实现数据元素结点类型定义:数据元素结点类型定义:typedef struct Qnode ElemType data ;struct Qnode *next ;QNode ;数据元素结点数据元素结点data指针结点指针结点front rear图图3-8 链队列结点示意图链队列结点示意图指针结点类型定义:指针结点类型定义:typedef struct link_queue QNode *front , *rear ;Link_Queue ;2 链队运算及指针变化链队运算及指针变化 链队的操作实际上是单链表的操作,只不过是删链队的操作实际上是单链表的操作,只不过是删除在表头进行,插入在表尾进行。插入、删除时分别修除在表头进行,插入在表尾进行。插入、删除时分别修改不同的指针。链队运算及指针变化如图改不同的指针。链队运算及指针变化如图3-9所示。所示。图图3-9 队列操作及指针变化队列操作及指针变化(a) 空队列空队列front rear(b) x入队入队 x front rear(c) y再入队再
87、入队 y front rear x(d) x出队出队 y xfront rear3 链队列的基本操作链队列的基本操作 链队列的初始化链队列的初始化LinkQueue *Init_LinkQueue(void) LinkQueue *Q ; QNode *p ;p=(QNode *)malloc(sizeof(QNode) ; /* 开辟头结点开辟头结点 */p-next=NULL ;Q=(LinkQueue *)malloc(sizeof(LinkQueue) ; /* 开辟链队的指针结点开辟链队的指针结点 */Q.front=Q.rear=p ; return(Q) ; 链队列的链队列的入队操作入队操作 在已知队列的队尾插入一个元素在已知队列的队尾插入一个元素e ,即,即修改队尾修改队尾指针指针(Q.rear)。Status Insert_CirQueue(LinkQueue *Q , ElemType e) /* /* 将数据元素将数据元素e插入到链队列插入到链队列Q的队尾的队尾 * */ / p=(QNode *)malloc(sizeof(QNode) ;if (!p) ret
88、urn ERROR;/* 申请新结点失败,返回错误标志申请新结点失败,返回错误标志 */p-data=e ; p-next=NULL ; /* 形成新结点形成新结点 */Q.rear-next=p ; Q.rear=p ; /* 新结点插入到队尾新结点插入到队尾 */return OK; 链队列的出队操作链队列的出队操作Status Delete_LinkQueue(LinkQueue *Q, ElemType *x) QNode *p ;if (Q.front=Q.rear) return ERROR ; /* 队空队空 */p=Q.front-next ; /* /* 取队首结点取队首结点 * */ /*x=p-data ; Q.front-next=p-next ; /* 修改队首指针修改队首指针 */if (p=Q.rear) Q.rear=Q.front ; /* 当队列只有一个结点时应防止丢失队尾指针当队列只有一个结点时应防止丢失队尾指针 */ free(p) ; return OK ; 链队列的撤消链队列的撤消void Destroy_LinkQueue(LinkQueu
89、e *Q ) /* 将链队列将链队列Q的队首元素出队的队首元素出队 */ while (Q.front!=NULL) Q.rear=Q.front-next; /* 令尾指针指向队列的第一个结点令尾指针指向队列的第一个结点 */free(Q.front); /* 每次释放一个结点每次释放一个结点 */ /* 第一次是头结点,以后是元素结点第一次是头结点,以后是元素结点 */Q.ront=Q.rear;习习 题题 三三1 设有一个栈,元素进栈的次序为设有一个栈,元素进栈的次序为a, b, c。问经过栈。问经过栈操作后可以得到哪些输出序列?操作后可以得到哪些输出序列?2 循环队列的优点是什么循环队列的优点是什么? ?如何判断它的空和满如何判断它的空和满? ?3 设有一个静态顺序队列,向量大小为设有一个静态顺序队列,向量大小为MAX,判断,判断队列为空的条件是什么?队列满的条件是什么?队列为空的条件是什么?队列满的条件是什么?4 设有一个静态循环队列,向量大小为设有一个静态循环队列,向量大小为MAX,判断,判断队列为空的条件是什么?队列满的条件是什么?队列为空的条件是什么?队列满的条件是什么
90、?5 利用栈的基本操作,利用栈的基本操作,写一个返回栈写一个返回栈S中结点个数的中结点个数的算法算法int StackSize(SeqStack S) ,并说明,并说明S S为何不作为指为何不作为指针参数针参数的算法的算法? ?6 一个双向栈一个双向栈S是在同一向量空间内实现的两个栈是在同一向量空间内实现的两个栈,它们的栈底分别设在向量空间的两端。试为此双向栈设它们的栈底分别设在向量空间的两端。试为此双向栈设计初始化计初始化InitStack(S) ,入栈,入栈Push(S,i,x),出栈,出栈Pop(S,i,x)算法算法,其中,其中i为为0或或1 ,用以表示栈号。,用以表示栈号。7 设设Q0,6是一个静态顺序队列是一个静态顺序队列,初始状态为,初始状态为front=rear=0,请画出做完下列操作后队列的头尾指针,请画出做完下列操作后队列的头尾指针的状态变化情况,若不能入对,请指出其元素,并说明的状态变化情况,若不能入对,请指出其元素,并说明理由。理由。a, b, c, d入队入队a, b, c出队出队i , j , k , l , m入队入队d, i出队出队n, o, p, q,
91、r入队入队8 假设假设Q0,5是一个循环队列是一个循环队列,初始状态为,初始状态为front=rear=0,请画出做完下列操作后队列的头尾指针,请画出做完下列操作后队列的头尾指针的状态变化情况,若不能入对,请指出其元素,并说明的状态变化情况,若不能入对,请指出其元素,并说明理由。理由。d, e, b, g, h入队入队d, e出队出队i , j , k , l , m入队入队b出队出队n, o, p, q, r入队入队 第第4章章 串串 在非数值处理、事务处理等问题常涉及到一系列在非数值处理、事务处理等问题常涉及到一系列的字符操作。计算机的硬件结构主要是反映数值计算的的字符操作。计算机的硬件结构主要是反映数值计算的要求,因此,字符串的处理比具体数值处理复杂。本章要求,因此,字符串的处理比具体数值处理复杂。本章讨论串的存储结构及几种基本的处理。讨论串的存储结构及几种基本的处理。4.1 串类型的定义串类型的定义4.1.1 串的基本概念串的基本概念 串串( (字符串字符串) ):是零个或多个字符组成的有限序列。:是零个或多个字符组成的有限序列。记作:记作: S=“a1a2a3”,其中,其中S
92、是串名,是串名,ai(1in)是单是单个,个,可以是字母、数字或其它字符。可以是字母、数字或其它字符。 串值串值:双引号括起来的字符序列是串值。:双引号括起来的字符序列是串值。 串长串长:串中所包含的字符个数称为该串的长度。:串中所包含的字符个数称为该串的长度。 空串空串( (空的字符串空的字符串) ):长度为零的串称为空串,它不:长度为零的串称为空串,它不包含任何字符。包含任何字符。 空格串空格串( (空白串空白串) ):构成串的所有字符都是空格的串:构成串的所有字符都是空格的串称为空白串。称为空白串。注意注意:空串和空白串的不同,例如:空串和空白串的不同,例如“ ”“ ”和和“”“”分别表分别表示长度为示长度为1 1的空白串和长度为的空白串和长度为0的空串。的空串。 子串子串( (substring) ):串中任意个连续字符组成的子:串中任意个连续字符组成的子序列称为该串的子串,包含子串的串相应地称为主串。序列称为该串的子串,包含子串的串相应地称为主串。 子串的序号子串的序号:将子串在主串中首次出现时的该子串:将子串在主串中首次出现时的该子串的首字符对应在主串中的序号,称为子串在主
93、串中的序的首字符对应在主串中的序号,称为子串在主串中的序号(或位置)。号(或位置)。例如,例如,设有串设有串A和和B分别是:分别是: A=“这是字符串这是字符串”,B=“是是”则则B是是A的子串,的子串,A为主串。为主串。B在在A中出现了两次,其中中出现了两次,其中首次出现所对应的主串位置是首次出现所对应的主串位置是3。因此,称。因此,称B在在A中的序中的序号为号为3 。 特别地,空串是任意串的子串,任意串是其自身的特别地,空串是任意串的子串,任意串是其自身的子串。子串。 串相等串相等:如果两个串的串值相等:如果两个串的串值相等( (相同相同) ),称这两个,称这两个串相等。换言之,只有当两个串的长度相等,且各个对串相等。换言之,只有当两个串的长度相等,且各个对应位置的字符都相同时才相等。应位置的字符都相同时才相等。 通常在程序中使用的串可分为两种:串变量和串常通常在程序中使用的串可分为两种:串变量和串常量。量。 串常量和整常数、实常数一样,在程序中只能被引串常量和整常数、实常数一样,在程序中只能被引用但不能不能改变其值,即只能读不能写。通常串常量用但不能不能改变其值,即只能读不能写。
94、通常串常量是由直接量来表示的,例如语句错误是由直接量来表示的,例如语句错误(“(“溢出溢出”) )中中“溢溢出出”是直接量。是直接量。 串变量和其它类型的变量一样,其值是可以改变。串变量和其它类型的变量一样,其值是可以改变。4.1.2 串的抽象数据类型定义串的抽象数据类型定义 ADT String数据对象:数据对象:D = ai|aiCharacterSet, i=1,2,n, n 0 数据关系:数据关系:R = | ai-1, aiD, i=2,3,n 基本操作:基本操作:StrAssign(t , chars)初始条件:初始条件: chars是一个字符串常量。是一个字符串常量。操作结果:生成一个值为操作结果:生成一个值为chars的串的串t 。StrConcat(s, t)初始条件:串初始条件:串s, t 已存在。已存在。操作结果:将串操作结果:将串t联结到串联结到串s后形成新串存放到后形成新串存放到s中。中。StrLength(t)初始条件:字符串初始条件:字符串t已存在。已存在。操作结果:返回串操作结果:返回串t中的元素个数,称为串长。中的元素个数,称为串长。SubString
95、 (s, pos, len, sub)初始条件:串初始条件:串s, 已存在已存在, 1posStrLength(s)且且 0lenStrLength(s) pos+1。操作结果:用操作结果:用sub返回串返回串s的第的第pos个字符起长度为个字符起长度为len的子串。的子串。 ADT String4.2 串的存储表示和实现串的存储表示和实现 串是一种特殊的线性表,其存储表示和线性表类似,串是一种特殊的线性表,其存储表示和线性表类似,但又不完全相同。串的存储方式取决于将要对串所进行但又不完全相同。串的存储方式取决于将要对串所进行的操作。串在计算机中有的操作。串在计算机中有3种表示方式:种表示方式: 定长顺序存储表示定长顺序存储表示:将串定义成字符数组,利用:将串定义成字符数组,利用串名可以直接访问串值。用这种表示方式,串的存串名可以直接访问串值。用这种表示方式,串的存储空间在编译时确定,其大小不能改变。储空间在编译时确定,其大小不能改变。 堆分配存储方式堆分配存储方式:仍然用一组地址连续的存储单:仍然用一组地址连续的存储单元来依次存储串中的字符序列,但串的存储空间是元来依次存储串中的字符
96、序列,但串的存储空间是在程序运行时根据串的实际长度动态分配的。在程序运行时根据串的实际长度动态分配的。 块链存储方式块链存储方式:是一种链式存储结构表示。:是一种链式存储结构表示。4.2.1 串的定长顺序存储表示串的定长顺序存储表示 这种存储结构又称为串的顺序存储结构。是用一这种存储结构又称为串的顺序存储结构。是用一组连续的存储单元来存放串中的字符序列。所谓定长顺组连续的存储单元来存放串中的字符序列。所谓定长顺序存储结构,是直接使用定长的字符数组来定义,数组序存储结构,是直接使用定长的字符数组来定义,数组的上界预先确定。的上界预先确定。定长顺序存储结构定义为:定长顺序存储结构定义为:#define MAX_STRLEN 256typedef struct char strMAX_STRLEN ;int length; StringType ; 1 串的联结操作串的联结操作Status StrConcat ( StringType s, StringType t)/* 将串将串t联结到串联结到串s之后,结果仍然保存在之后,结果仍然保存在s中中 */ int i, j ;if (s.len
97、gth+t.length)MAX_STRLEN)Return ERROR ; /* 联结后长度超出范围联结后长度超出范围 */ for (i=0 ; it.length ; i+)s.strs.length+i=t.stri ; /* 串串t联结到串联结到串s之后之后 */s.length=s.length+t.length ; /* 修改联结后的串长度修改联结后的串长度 */return OK ;2 求子串操作求子串操作Status SubString (StringType s, int pos, int len, StringType *sub) int k, j ;if (poss.length|len(s.length-pos+1)return ERROR ; /* 参数非法参数非法 */sub-length=len-pos+1 ; /* 求得子串长度求得子串长度 */for (j=0, k=pos ; kstrj=s.stri ; /* 逐个字符复制求得子串逐个字符复制求得子串 */return OK ;4.2.2 串的堆分配存储表示串的堆分配存储表示 实现方法:系统提供一
98、个空间足够大且地址连续的实现方法:系统提供一个空间足够大且地址连续的存储空间存储空间( (称为称为“堆堆”) )供串使用。可使用供串使用。可使用C语言的动态语言的动态存储分配函数存储分配函数malloc()和和free()来来管理。管理。 特点是:仍然以一组地址连续的存储空间来存储字特点是:仍然以一组地址连续的存储空间来存储字符串值,但其所需的存储空间是在程序执行过程中动态符串值,但其所需的存储空间是在程序执行过程中动态分配,故是动态的,变长的。分配,故是动态的,变长的。串的堆式存储结构的类型定义串的堆式存储结构的类型定义typedef struct char *ch; /* 若非空,按长度分配,否则为若非空,按长度分配,否则为NULL */int length; /* 串的长度串的长度 */ HString ;1 串的联结操作串的联结操作Status Hstring *StrConcat(HString *T, HString *s1, HString *s2)/* 用用T返回由返回由s1和和s2联结而成的串联结而成的串 */ int k, j , t_len ; if (T.ch)
99、 free(T); /* 释放旧空间释放旧空间 */t_len=s1-length+s2-length ;if (p=(char *)malloc(sizeof(char)*t_len)=NULL) printf(“系统空间不够,申请空间失败系统空间不够,申请空间失败 !n”) ; return ERROR ; for (j=0 ; jlength; j+) T-chj=s1-chj ; /* 将串将串s复制到串复制到串T中中 */for (k=s1-length, j=0 ; jlength; k+, j+) T-chj=s1-chj ; /* 将串将串s2复制到串复制到串T中中 */free(s1-ch) ; free(s2-ch) ; return OK ; 4.2.3 串的链式存储表示串的链式存储表示 串的链式存储结构和线性表的串的链式存储结构类串的链式存储结构和线性表的串的链式存储结构类似,采用单链表来存储串,似,采用单链表来存储串,结点的构成是:结点的构成是: data域:存放字符,域:存放字符,data域可存放的字符个数称为域可存放的字符个数称为结点的大小;结点的大小;
100、next域:存放指向下一结点的指针。域:存放指向下一结点的指针。 若每个结点仅存放一个字符,则结点的指针域就非若每个结点仅存放一个字符,则结点的指针域就非常多,造成系统空间浪费,为节省存储空间,考虑串结常多,造成系统空间浪费,为节省存储空间,考虑串结构的特殊性,使每个结点存放若干个字符,这种结构称构的特殊性,使每个结点存放若干个字符,这种结构称为块链结构。如为块链结构。如图图4-1是块大小为是块大小为3的串的块链式存储结的串的块链式存储结构示意图。构示意图。串的块链式存储的类型定义包括:串的块链式存储的类型定义包括: 块结点的类型定义块结点的类型定义#define BLOCK_SIZE 4typedef struct Blstrtype char dataBLOCK_SIZE ;struct Blstrtype *next;BNODE ;a b c e p c g head图图4-1 串的块链式存储结构示意图串的块链式存储结构示意图(2) 块链串的类型定义块链串的类型定义typedef struct BNODE head; /* 头指针头指针 */ int Strlen ; /* 当前
101、长度当前长度 */ Blstring ; 在这种存储结构下,结点的分配总是完整的结点为在这种存储结构下,结点的分配总是完整的结点为单位,因此,为使一个串能存放在整数个结点中,在串单位,因此,为使一个串能存放在整数个结点中,在串的末尾填上不属于串值的特殊字符,以表示串的终结。的末尾填上不属于串值的特殊字符,以表示串的终结。 当一个块当一个块(结点结点)内存放多个字符时,往往会使操作内存放多个字符时,往往会使操作过程变得较为复杂,如在串中插入或删除字符操作时通过程变得较为复杂,如在串中插入或删除字符操作时通常需要在块间移动字符。常需要在块间移动字符。4.3 串的模式匹配算法串的模式匹配算法模式匹配模式匹配( (模范匹配模范匹配) ):子串在主串中的定位称为模子串在主串中的定位称为模式匹配或串匹配式匹配或串匹配( (字符串匹配字符串匹配) ) 。模式匹配成功是指在。模式匹配成功是指在主串主串S中能够找到模式串中能够找到模式串T,否则,称模式串,否则,称模式串T在主串在主串S中不存在。中不存在。 模式匹配的应用在非常广泛。例如,在文本编辑程模式匹配的应用在非常广泛。例如,在文本编辑程序中,我们
102、经常要查找某一特定单词在文本中出现的位序中,我们经常要查找某一特定单词在文本中出现的位置。显然,解此问题的有效算法能极大地提高文本编辑置。显然,解此问题的有效算法能极大地提高文本编辑程序的响应性能。程序的响应性能。 模式匹配是一个较为复杂的串操作过程。迄今为止,模式匹配是一个较为复杂的串操作过程。迄今为止,人们对串的模式匹配提出了许多思想和效率各不相同的人们对串的模式匹配提出了许多思想和效率各不相同的计算机算法。介绍两种主要的模式匹配算法。计算机算法。介绍两种主要的模式匹配算法。4.3.1 Brute-Force模式匹配算法模式匹配算法 设设S为目标串,为目标串,T为模式串,且不妨设:为模式串,且不妨设:S=“s0s1s2sn-1” , T=“t0t1t2 tm-1” 串的匹配实际上是对合法的位置串的匹配实际上是对合法的位置0in-m依次将目依次将目标串中的子串标串中的子串sii+m-1和模式串和模式串t0m-1进行比较:进行比较: 若若sii+m-1=t0m-1:则称从位置:则称从位置i开始的匹开始的匹配成功,亦称模式配成功,亦称模式t在目标在目标s中出现;中出现; 若若sii+m-
103、1t0m-1:从:从i开始的匹配失败。开始的匹配失败。位置位置i称为位移,当称为位移,当sii+m-1=t0m-1时,时,i称为称为有效位移;当有效位移;当sii+m-1 t0m-1时,时,i称为无效称为无效位移。位移。 这样,串匹配这样,串匹配问题可简化为找出某给定模式问题可简化为找出某给定模式T在给在给定目标串定目标串S中首次出现中首次出现的有效位移。的有效位移。算法实现算法实现int IndexString(StringType s , StringType t , int pos ) /* 采用顺序存储方式存储主串采用顺序存储方式存储主串s和模式和模式t, */ /* 若模式若模式t在主串在主串s中从第中从第pos位置开始有匹配的子串,位置开始有匹配的子串,*/ /* 返回位置,否则返回返回位置,否则返回-1 */ char *p , *q ;int k , j ;k=pos-1 ; j=0 ; p=s.str+pos-1 ; q=t.str ; /* 初始匹配位置设置初始匹配位置设置 */ /* 顺序存放时第顺序存放时第pos位置的下标值为位置的下标值为pos-1 */whi
104、le (ks.length)&(jt.length) if (*p=*q) p+ ; q+ ; k+ ; j+ ; else k=k-j+1 ; j=0 ; q=t.str ; p=s.str+k ; /* 重新设置匹配位置重新设置匹配位置 */if (j=t.length) return(k-t.length) ; / * 匹配,返回位置匹配,返回位置 */else return(-1) ; /* 不匹配,返回不匹配,返回-1 */ 该算法简单,易于理解。在一些场合的应用里,如该算法简单,易于理解。在一些场合的应用里,如文字处理中的文本编辑,其效率较高。文字处理中的文本编辑,其效率较高。 该算法的时间复杂度为该算法的时间复杂度为O(n*m) ,其中,其中n 、m分别是分别是主串和模式串的长度。通常情况下,实际运行过程中,主串和模式串的长度。通常情况下,实际运行过程中,该算法的执行时间近似于该算法的执行时间近似于O(n+m) 。理解该算法的关键点理解该算法的关键点 当第一次当第一次sktj时:主串要退回到时:主串要退回到k-j+1的位置,而模的位置,而模式串也要退回到第一个字符(即式串
105、也要退回到第一个字符(即j=0的位置)。的位置)。比较出现比较出现sktj时:则应该有时:则应该有sk-1=tj-1,sk-j+1=t1, sk-j=t0 。4.3.2 模式匹配的一种改进算法模式匹配的一种改进算法 该改进算法是由该改进算法是由D.E.Knuth ,J.H.Morris和和 V.R.Pratt提出来的,简称为提出来的,简称为KMP算法。其算法。其改进在于:改进在于: 每当一趟匹配过程出现字符不相等时,主串指示器每当一趟匹配过程出现字符不相等时,主串指示器不用回溯,而是利用已经得到的不用回溯,而是利用已经得到的“部分匹配部分匹配”结果,将结果,将模式串的指示器向右模式串的指示器向右“滑动滑动”尽可能远的一段距离后,尽可能远的一段距离后,继续进行比较。继续进行比较。例:例:设有串设有串s=“abacabab” ,t=“abab” 。则第一次匹配。则第一次匹配过程如图过程如图4-2所示。所示。图图4-2 模式匹配示例模式匹配示例 s=“a b cbb” i=3 | | 匹配失败匹配失败 t=“a b b” j=3 在在i=3和和j=3时,匹配失败。但重新开始第二次匹配时,匹配
106、失败。但重新开始第二次匹配时,不必从时,不必从i=1 ,j=0开始。因为开始。因为s1=t1,t0t1,必有,必有s1t0,又因为,又因为t0=t2,s2=t2,所以必有,所以必有s2=t0。由此可知,第二。由此可知,第二次匹配可以直接从次匹配可以直接从i=3 、j=1开始。开始。 总之,在主串总之,在主串s与模式串与模式串t的匹配过程中,一旦出现的匹配过程中,一旦出现sitj ,主串,主串s的指针不必回溯,而是直接与模式串的的指针不必回溯,而是直接与模式串的tk(0kj进行比较,而进行比较,而k的取值与主串的取值与主串s无关,只与模式无关,只与模式串串t本身的构成有关,即从模式串本身的构成有关,即从模式串t可求得可求得k值。值。) 不失一般性,设不失一般性,设主串主串s=“s1s2sn” ,模式串模式串t=“t1 t2 tm” 。 当当sitj(1in-m,1jm,mn)时,主串时,主串s的指针的指针i不必回溯,而模式串不必回溯,而模式串t的指针的指针j回溯到第回溯到第k(kk满足满足4-1式。式。t1t2tk-1= si-(k-1) si-(k-2) si-2 si-1 (4-1)
107、而已经得到的而已经得到的 “部分匹配部分匹配”的结果为:的结果为:tj-(k-1) tj-k tj-1=si-(k-1) si-(k-2) si-2 si-1 (4-2)由式由式(4-1)和式和式(4-2)得:得:t1t2tk-1=tj-(k-1) tj-k tj-1 (4-3) 该推导过程可用图该推导过程可用图4-3形象描述。实际上,式形象描述。实际上,式(4-3)描描述了模式串中存在相互重叠的子串的情况。述了模式串中存在相互重叠的子串的情况。图图4-3 KMP算法示例算法示例主串si模式串tkj0 当当j=1时时Maxk|1kjt1t2tk-1=tj-(k-1) tj-k tj-1 该集合不空时该集合不空时1 其它情况其它情况nextj=定义定义nextj函数为函数为在求得了在求得了nextj值之后,值之后,KMP算法的思想是:算法的思想是: 设目标串设目标串(主串主串)为为s,模式串为,模式串为t ,并设,并设i指针和指针和j指针分指针分别指示目标串和模式串中正待比较的字符,设别指示目标串和模式串中正待比较的字符,设i和和j的初的初值均为值均为1。若有。若有si=tj,则,则i和
108、和j分别加分别加1。否则,。否则,i不变,不变,j退退回到回到j=nextj的位置,再比较的位置,再比较si和和tj,若相等,则,若相等,则i和和j分分别加别加1。否则,。否则,i不变,不变,j再次退回到再次退回到j=nextj的位置,依的位置,依此类推。直到下列两种可能:此类推。直到下列两种可能:(1) j退回到某个下一个退回到某个下一个j值时字符比较相等,则指值时字符比较相等,则指针各自加针各自加1继续进行匹配。继续进行匹配。(2)退回到退回到j=0,将,将i和和j分别加分别加1,即从主串的下一个字符,即从主串的下一个字符si+1模式串的模式串的t1重新开始匹配。重新开始匹配。KMP算法如下算法如下#define Max_Strlen 1024int nextMax_Strlen;int KMP_index (StringType s , StringType t) /* 用用KMP算法进行模式匹配,匹配返回位置,否则返回算法进行模式匹配,匹配返回位置,否则返回-1 */ /*用静态存储方式保存字符串,用静态存储方式保存字符串, s和和t分别表示主串和模式串分别表示主串和模式串 *
109、/ int k=0 , j=0 ; /*初始匹配位置设置初始匹配位置设置 */while (ks.length)&(j= t.length) return(k-t.length) ; else return(-1) ; 很显然,很显然,KMP_index函数是在已知下一个函数值的函数是在已知下一个函数值的基础上执行的,以下讨论如何求基础上执行的,以下讨论如何求next函数值函数值? 由式由式(4-3)知,求模式串的知,求模式串的nextj值与主串值与主串s无关,只无关,只与模式串与模式串t本身的构成有关,则可把求本身的构成有关,则可把求next函数值的问题函数值的问题看成是一个模式匹配问题。由看成是一个模式匹配问题。由next函数定义可知:函数定义可知:当当j=1时:时:next1=0。设设nextj=k,即在模式串中存在:,即在模式串中存在:t1t2tk-1=tj-(k-1)tj-k tj-1 ,其中下标,其中下标k满足满足1kk满足上式,即:满足上式,即:nextj+1=nextj+1=k+1(2) 若有若有tktj :则表明在模式串中有:则表明在模式串中有:t1 t2tk-1 t
110、ktj-(k-1) tj-k tj-1 tj ,当,当tktj时应时应将模式向右滑动将模式向右滑动至以模式至以模式中的第中的第nextk个字符和主串中的第个字符和主串中的第j个字符相比较。个字符相比较。若若nextk= k,且,且tj = tk,则说明在主串中第,则说明在主串中第j+1字符字符之前存在一个长度为之前存在一个长度为k(即即nextk)的最长子串,与模的最长子串,与模式串中从第一个字符起长度为式串中从第一个字符起长度为k的子串相等。即的子串相等。即 nextj+1=k+1 同理,若同理,若tjtk,应,应将模式继续向右滑动将模式继续向右滑动至将模式中至将模式中的第的第nextk个字符和个字符和tj对齐,对齐,依此类推,直到,依此类推,直到tj和和模式串中的某个字符匹配成功或者不存在任何模式串中的某个字符匹配成功或者不存在任何k(1 kj)满足等式:满足等式:t1 t2tk-1 tk=tj-(k-1) tj-k tj-1 tj 则:则: nextj+1=1根据上述分析,根据上述分析, 求求next函数值的算法如下:函数值的算法如下:void next(StringType t
111、 , int next) /* 求模式求模式t的的next串串t函数值并保存在函数值并保存在next数组中数组中 */ int k=1 , j=0 ; next1=0;while (k1)个具有相同数据类型的数据元素个具有相同数据类型的数据元素a1,a2,an组成的有序序列组成的有序序列,且该,且该序列必须存储在一序列必须存储在一块地址连续的存储单元中块地址连续的存储单元中。 数组中的数据元素数组中的数据元素具有相同数据类型具有相同数据类型。 数组是一种随机存取结构,给定一组下标,就可数组是一种随机存取结构,给定一组下标,就可以访问与其对应的数据元素。以访问与其对应的数据元素。 数组中的数据元素个数是固定的。数组中的数据元素个数是固定的。5.1.1 数组的数组的抽象数据类型定义抽象数据类型定义 1 抽象数据类型定义抽象数据类型定义 ADT Array数据对象:数据对象:ji= 0,1,bi-1 , 1,2, ,n ; D = aj1j2jn | n0称为数组的维数称为数组的维数,b bi是数组第是数组第i维的维的长度长度,ji是数组元素是数组元素第第i维的下标维的下标,aj1j2jnE
112、lemSet 数据关系:数据关系:R = R1, R2, , RnRi=|0jkbk-1 , 1kn且且ki,0jibi-2, aj1j2 ji+1jnD 基本操作:基本操作: ADT Array 由上述定义知由上述定义知,n维数组中有维数组中有b1 b2 bn个数据个数据元素元素,每个数据元素都受到每个数据元素都受到n维关系的约束维关系的约束。2 直观的直观的n维数组维数组 以二维数组为例讨论。将以二维数组为例讨论。将二维数组看成是一个定长二维数组看成是一个定长的线性表的线性表,其其每个元素又是一个定长的线性表每个元素又是一个定长的线性表。 设二维数组设二维数组A=(aij)m n,则,则 A=(1,2,p) (p=m或或n)其中每个数据元素其中每个数据元素j是一个列向量是一个列向量(线性表线性表) : j =(a1j ,a2j ,amj) 1jn或或是一个行向量是一个行向量: i =(ai1 ,ai2 ,ain) 1im如图如图5-1所示。所示。 a11 a12 a1n a21 a22 a2n am1 am2 amnA= A=a11 a12 a1na21 a22 a2nam1 am
113、2 amna11 a21 am1a12 a22 am2a1n a2n amnA=图图5-1 二维数组图二维数组图例形式例形式(a) 矩阵矩阵表示形式表示形式(b) 列向量的一维数组形式列向量的一维数组形式(c) 行向量的一维数组形式行向量的一维数组形式5.2 数组的数组的顺序表示和实现顺序表示和实现 数组一般不做插入和删除操作,也就是说,数组数组一般不做插入和删除操作,也就是说,数组一旦建立,结构中的元素个数和元素间的关系就不再发一旦建立,结构中的元素个数和元素间的关系就不再发生变化。因此,一般都是生变化。因此,一般都是采用顺序存储的方法来表示数采用顺序存储的方法来表示数组组。 问题问题:计算机的计算机的内存结构是一维内存结构是一维( (线性线性) )地址结构地址结构,对于多维数组,将其存放对于多维数组,将其存放( (映射映射) )到内存一维结构时,有到内存一维结构时,有个个次序约定问题次序约定问题。即必须按某种次序将数组元素排成一。即必须按某种次序将数组元素排成一列序列,然后将这个线性序列存放到内存中。列序列,然后将这个线性序列存放到内存中。 二维数组是最简单的多维数组,以此为例说明
114、多维二维数组是最简单的多维数组,以此为例说明多维数组存放数组存放( (映射映射) )到内存一维结构时的到内存一维结构时的次序约定问题次序约定问题。通常有两种顺序存储方式通常有两种顺序存储方式 行优先顺序行优先顺序(Row Major Order) :将数组元素将数组元素按行排列,第按行排列,第i+1个行向量紧接在第个行向量紧接在第i个行向量后面。对个行向量后面。对二维数组,按行优先顺序存储的线性序列为:二维数组,按行优先顺序存储的线性序列为: a11,a12,a1n, a21,a22,a2n , am1,am2,amn PASCAL、C是按行优先顺序存储的,如图是按行优先顺序存储的,如图5-2(b)示。示。 列优先顺序列优先顺序(Column Major Order) :将数组将数组元素按列向量排列,第元素按列向量排列,第j+1个列向量紧接在第个列向量紧接在第j个列向量个列向量之后,对二维数组,按列优先顺序存储的线性序列为:之后,对二维数组,按列优先顺序存储的线性序列为: a11,a21,am1, a12,a22,am2, , an1,an2,anm FORTRAN是按列优先顺序存储的
115、,如图是按列优先顺序存储的,如图5-2(c)。图图5-2 二维数组及其顺序存储图二维数组及其顺序存储图例形式例形式 a11 a12 a1n a21 a22 a2n am1 am2 amnA=(a) 二维数组的表示形式二维数组的表示形式(b) 行优先顺序存储行优先顺序存储(c) 列列优先顺序存储优先顺序存储a11 a12 a1n第第 1行行a21 a22 a2n第第 2行行am1 am2 Amn 第第 m行行a11 a21 am1第第 1列列a12 a22 am2第第 2列列a1m a2m amn第第 n列列 设有二维数组设有二维数组A=(aij)m n,若每个元素占用的存储单,若每个元素占用的存储单元数为元数为l( (个个) ),LOCa11表示元素表示元素a11的首地址的首地址,即,即数组的数组的首地址首地址。1 以以“行优先顺序行优先顺序”存储存储 第第1行中的行中的每个元素对应的每个元素对应的( (首首) )地址是:地址是: LOCa1j=LOCa11+(j-1) l j=1,2, ,n(2) 第第2行中的行中的每个元素对应的每个元素对应的( (首首) )地址是:地址是: LOC
116、a2j=LOCa11+n l +(j-1) l j=1,2, ,n 第第m行中的行中的每个元素对应的每个元素对应的( (首首) )地址是:地址是:LOCamj=LOCa11+(m-1) n l +(j-1) l j=1,2, ,n 由此可知由此可知,二维数组中,二维数组中任一元素任一元素aij的的( (首首) )地址地址是:是:LOCaij=LOCa11+(i-1) n +(j-1) l (5-1)i=1,2, ,m j=1,2, ,n 根据根据(5-1)式式,对于三维数组,对于三维数组A=(aijk)m n p,若每个,若每个元素占用的存储单元数为元素占用的存储单元数为l( (个个) ),LOCa111表示元素表示元素a111的首地址的首地址,即,即数组的数组的首地址首地址。以以“行优先顺序行优先顺序”存储在存储在内存中内存中。 三维数组中任一元素三维数组中任一元素aijk的的( (首首) )地址是:地址是: LOC(aijk)=LOCa111+(i-1) n p+(j-1) p+(k-1) l (5-2) 推而广之推而广之,对,对n维数组维数组A=(aj1j2jn) ,若每个元素占
117、,若每个元素占用的存储单元数为用的存储单元数为l( (个个) ),LOCa11 1表示元素表示元素a11 1的的首地址首地址。则。则 以以“行优先顺序行优先顺序”存储在内存中存储在内存中。 n维数组中任一元素维数组中任一元素aj1j2jn的的( (首首) )地址是:地址是: LOCaj1j2jn=LOCa11 1+(b2 bn) (j1-1) + (b3 bn) (j2-1)+ + bn (jn-1-1)+ (jn-1) l (5-3)2 以以“列优先顺序列优先顺序”存储存储 第第1列中的列中的每个元素对应的每个元素对应的( (首首) )地址是:地址是: LOCaj1=LOCa11+(j-1) l j=1,2, ,m(2) 第第2列中的列中的每个元素对应的每个元素对应的( (首首) )地址是:地址是: LOCaj2=LOCa11+m l +(j-1) l j=1,2, ,m 第第n列中的列中的每个元素对应的每个元素对应的( (首首) )地址是:地址是:LOCajn=LOCa11+ (n-1) m l +(j-1) l j=1,2, ,m 由此可知由此可知,二维数组中,二维数组中任一元素
118、任一元素aij的的( (首首) )地址地址是:是:LOCaij=LOCa11+(i-1) m+(j-1) l (5-1)i=1,2, ,n j=1,2, ,m 5.3 矩阵的压缩存储矩阵的压缩存储 在科学与工程计算问题中,矩阵是一种常用的数学在科学与工程计算问题中,矩阵是一种常用的数学对象,在高级语言编程时,通常将一个矩阵描述为一个对象,在高级语言编程时,通常将一个矩阵描述为一个二维数组。这样,可以对其元素进行随机存取,各种矩二维数组。这样,可以对其元素进行随机存取,各种矩阵运算也非常简单。阵运算也非常简单。 对于对于高阶矩阵高阶矩阵,若其中,若其中非零元素呈某种规律分布非零元素呈某种规律分布或或者者矩阵中有大量的零元素矩阵中有大量的零元素,若仍然用常规方法存储,可,若仍然用常规方法存储,可能存储重复的非零元素或零元素,将造成存储空间的大能存储重复的非零元素或零元素,将造成存储空间的大量浪费。对这类矩阵进行压缩存储:量浪费。对这类矩阵进行压缩存储: 多个相同的非零元素只分配一个存储空间多个相同的非零元素只分配一个存储空间; 零元素不分配空间。零元素不分配空间。5.3.1 特殊矩阵特殊矩
119、阵特殊矩阵特殊矩阵:是指非零元素或零元素的分布有一定规律是指非零元素或零元素的分布有一定规律的矩阵。的矩阵。1 对称矩阵对称矩阵 若一个若一个n阶方阵阶方阵A=(aij)n n中的元素满足性质:中的元素满足性质:aij=aji 1i,jn且且ij则称则称A为对称矩阵,如图为对称矩阵,如图5-3所示。所示。图图5-3 对称矩阵示例对称矩阵示例1 5 1 3 73 0 2 5 17 0 6 1 35 0 8 0 01 8 9 2 6A= a11 a21 a22 a31 a32 a33 an1 an2 annA= 对称矩阵中的对称矩阵中的元素关于主对角线对称元素关于主对角线对称,因此,让,因此,让每一对对称元素每一对对称元素aij和和aji(ij)分配一个存储空间,则分配一个存储空间,则n2个个元素压缩存储到元素压缩存储到n(n+1)/2个存储空间,能节约近一半的个存储空间,能节约近一半的存储空间。存储空间。 不失一般性,假设按不失一般性,假设按“行优先顺序行优先顺序”存储下三角形存储下三角形( (包括对角线包括对角线) )中的元素。中的元素。 设用一维数组设用一维数组( (向量向量) )s
120、a0n(n+1)/2存储存储n阶对称阶对称矩阵,如图矩阵,如图5-4所示。为了便于访问,必须找出矩阵所示。为了便于访问,必须找出矩阵A中中的元素的下标值的元素的下标值(i,j)和向量和向量sak的的下标值下标值k之间的对之间的对应关系。应关系。sa a11 a21 a22 a31 a32 a33 an1 an2 annK 1 2 3 4 n(n-1)/2 n(n+1)/2图图5-4 对称矩阵的对称矩阵的压缩存储示例压缩存储示例 若若ij:ai j在下三角形中,直接保存在在下三角形中,直接保存在sa中。中。ai j之之前的前的i-1行共有元素个数:行共有元素个数: 1+2+(i-1)=i (i-1)/2而在第而在第i行上,行上,ai j之前恰有之前恰有j-1个元素,因此,元素个元素,因此,元素ai j保保存存在向量在向量sa中时的中时的下标值下标值k之间的对应关系之间的对应关系是:是: k=i (i-1)/2+j-1 ij 若若ij:则:则aij是在上三角矩阵中。因为是在上三角矩阵中。因为aij=aji,在向,在向量量sa中保存的是中保存的是aji 。依上述分析可得:。依上述分析可得:
121、k=j (j-1)/2+i-1 ij 对称矩阵元素对称矩阵元素ai j保存保存在向量在向量sa中时的中时的下标值下标值k与与(i,j)之间的对应关系)之间的对应关系是:是: i (i-1)/2+j-1 当当ij时时j (j-1)/2+i-1 当当ij时时K=1i,j n (5-4) 根据上述的下标对应关系,对于矩阵中的任意元根据上述的下标对应关系,对于矩阵中的任意元素素aij,均可在一维数组,均可在一维数组sa中唯一确定其位置中唯一确定其位置k;反之,反之,对所有对所有k=1,2, ,n(n+1)/2,都能确定,都能确定sak中的元素在矩中的元素在矩阵中的位置阵中的位置(i,j)。 称称sa0n(n+1)/2为为n阶对称矩阵阶对称矩阵A的压缩存储。的压缩存储。2 三角矩阵三角矩阵 以主对角线划分,三角矩阵有上三角和下三角两以主对角线划分,三角矩阵有上三角和下三角两种。种。 上三角矩阵的下三角(不包括主对角线)中的元素上三角矩阵的下三角(不包括主对角线)中的元素均为常数均为常数c( (一般为一般为0) )。下三角矩阵正好相反,它的主对。下三角矩阵正好相反,它的主对角线上方均为常数,如图角
122、线上方均为常数,如图5-5所示。所示。a11 a12 a1nc a22 a2nc c ann a11 c ca21 a22 can1 an2 ann 图图5-5 三角矩阵三角矩阵示例示例(b) 下下三角矩阵三角矩阵示例示例(a) 上上三角矩阵三角矩阵示例示例 三角矩阵中的重复元素三角矩阵中的重复元素c可共享一个存储空间,其可共享一个存储空间,其余的元素正好有余的元素正好有n(n+1)/2个,因此,三角矩阵可压缩存个,因此,三角矩阵可压缩存储到向量储到向量sa0n(n+1)/2中,其中中,其中c存放在向量的第存放在向量的第1个个分量中。分量中。 上三角矩阵元素上三角矩阵元素ai j保存保存在向量在向量sa中时的中时的下标值下标值k与与(i,j)之间的对应关系)之间的对应关系是:是: 下三角矩阵元素下三角矩阵元素ai j保存保存在向量在向量sa中时的中时的下标值下标值k与与(i,j)之间的对应关系)之间的对应关系是:是:i (i-1)/2+j-1 当当ij时时n (n+1)/2 当当ij时时K=1i,jn (5-6)3 对角矩阵对角矩阵 矩阵中,除了主对角线和主对角线上或下方若干矩阵中,除
123、了主对角线和主对角线上或下方若干条对角线上的元素之外,其余元素皆为零。即所有的非条对角线上的元素之外,其余元素皆为零。即所有的非零元素集中在以主对角线为了中心的带状区域中,如图零元素集中在以主对角线为了中心的带状区域中,如图5-6所示。所示。a11 a12 0 . 0a21 a22 a23 0 . 00 a32 a33 a34 0 . 0 . 0 . 0 0 an n-1 an n0 . 0 an-1 n-2 an-1 n-1 an-1 nA=图图5-6 三对角矩阵三对角矩阵示例示例 如上图三对角矩阵,非零元素仅出现在主对角如上图三对角矩阵,非零元素仅出现在主对角(ai i,1in)上、主对角线上的那条对角线上、主对角线上的那条对角线(ai i+1,1in-1) 、主对角线下的那条对角线上、主对角线下的那条对角线上(ai+1 i,1in-1)。显然,。显然,当当| i-j |1时,元素时,元素aij=0。 由此可知,一个由此可知,一个k对角矩阵对角矩阵( (k为奇数为奇数) )A是满足下述是满足下述条件:条件: 当当| i-j |(k-1)/2时,时, ai j=0 对角矩阵可按对角矩
124、阵可按行优先顺序行优先顺序或或对角线顺序对角线顺序,将其压缩,将其压缩存储到一个向量中,并且也能找到每个非零元素和向量存储到一个向量中,并且也能找到每个非零元素和向量下标的对应关系。下标的对应关系。 仍然以三对角矩阵为例讨论。仍然以三对角矩阵为例讨论。当当i=1,j=1、2,或或i=n, j=n-1、n或或1in-1,j=i-1、i、i+1的元素的元素aij外,其余元素都是外,其余元素都是0。 对这种矩阵,当以按对这种矩阵,当以按“行优先顺序行优先顺序”存储时,存储时, 第第1行和第行和第n行是行是2个非零元素,其余每行的非零元素都要是个非零元素,其余每行的非零元素都要是3个,则需存储的元素个数为个,则需存储的元素个数为3n-2。sa a11 a12 a21 a22 a23 a32 a33 a34 an n-1 annK 1 2 3 4 5 6 7 8 3n-3 3n-2图图5-7 三三对角矩阵的对角矩阵的压缩存储示例压缩存储示例 如图如图5-7所示所示三三对角矩阵的对角矩阵的压缩存储形式压缩存储形式。数组。数组sa中的元素中的元素sak与三对角矩阵中的元素与三对角矩阵中的元素aij存
125、在一一对应关存在一一对应关系,在系,在aij之前有之前有i-1行行, ,共有共有3 i-1个非零元素,在第个非零元素,在第i行,行,有有j-i+1个非零元素,这样,非零元素个非零元素,这样,非零元素aij的地址为:的地址为: LOCai j =LOCa11 +3 i-1+(j-i+1) l =LOCa11+(2 i+j) l上例中,上例中,a34对应着对应着sa10 , , k=2 i+j=2 3+4=10称称sa03 n-2是是n阶三对角矩阵阶三对角矩阵A的压缩存储。的压缩存储。 上述各种特殊矩阵,其非零元素的分布都是有规律上述各种特殊矩阵,其非零元素的分布都是有规律的,因此总能找到一种方法将它们压缩存储到一个向量的,因此总能找到一种方法将它们压缩存储到一个向量中,并且一般都能找到矩阵中的元素与该向量的对应关中,并且一般都能找到矩阵中的元素与该向量的对应关系,通过这个关系,仍能对矩阵的元素进行随机存取。系,通过这个关系,仍能对矩阵的元素进行随机存取。 5.3.2 稀疏矩阵稀疏矩阵稀疏矩阵稀疏矩阵(Sparse Matrix):对于稀疏矩阵,目前还对于稀疏矩阵,目前还没有一个确切的定义
126、。设矩阵没有一个确切的定义。设矩阵A是一个是一个n m的的矩阵中有矩阵中有s个非零元素,设个非零元素,设 =s/(n m),称称为为稀疏因子,如果稀疏因子,如果某一矩阵的稀疏因子某一矩阵的稀疏因子满足满足0.05时称为稀疏矩阵,如时称为稀疏矩阵,如图图5-8所示。所示。0 12 9 0 0 0 0 00 0 0 0 0 0 0 0-3 0 0 0 0 0 0 40 0 24 0 0 2 0 00 18 0 0 0 0 0 00 0 0 0 0 0 -7 0A=0 0 0 -6 0 0 0 0图图5-8 稀疏稀疏矩阵矩阵示例示例5.3.2.1 稀疏矩阵的压缩存储稀疏矩阵的压缩存储 对于稀疏矩阵,采用压缩存储方法时,只存储非对于稀疏矩阵,采用压缩存储方法时,只存储非0元素。必须存储非元素。必须存储非0元素的行下标值、列下标值、元素元素的行下标值、列下标值、元素值。因此,一个三元组值。因此,一个三元组(i, j, aij)唯一确定稀疏矩阵的一唯一确定稀疏矩阵的一个非零元素。个非零元素。 如图如图5-8的稀疏矩阵的稀疏矩阵A的三元组线性表为:的三元组线性表为:( (1,2,12), (1,3,
127、9), (3,1,-3), (3,8,4), (4,3,24), (5,2,18), (6,7,-7), (7,4,-6) ) 1 三元组顺序表三元组顺序表 若以行序为主序,稀疏矩阵中所有非若以行序为主序,稀疏矩阵中所有非0元素的三元组,元素的三元组,就可以得构成该稀疏矩阵的一个三元组顺序表。就可以得构成该稀疏矩阵的一个三元组顺序表。1 三元组顺序表三元组顺序表 若以行序为主序,稀疏矩阵中所有非若以行序为主序,稀疏矩阵中所有非0元素的三元元素的三元组,就可以得构成该稀疏矩阵的一个三元组顺序表。相组,就可以得构成该稀疏矩阵的一个三元组顺序表。相应的数据结构定义如下:应的数据结构定义如下: 三元组结点定义三元组结点定义 #define MAX_SIZE 101typedef int elemtype ;typedef struct int row ; /* 行下标行下标 */int col ; /* 列下标列下标 */elemtype value; /* 元素值元素值 */Triple ; 三元组顺序表定义三元组顺序表定义 typedef struct int rn ; /* 行数行数 *
128、/int cn ; /* 列数列数 */int tn ; /* 非非0元素个数元素个数 */Triple dataMAX_SIZE ; TMatrix ; 图图5-8所示的稀疏矩阵及其相应的转置矩阵所对应的所示的稀疏矩阵及其相应的转置矩阵所对应的三元组顺序表如图三元组顺序表如图5-9所示。所示。 图图5-9 稀疏稀疏矩阵及其转置矩阵的三元组顺序表矩阵及其转置矩阵的三元组顺序表798rn行数行数cn列数列数tn元素个数元素个数row col value1 2 121 3 93 1 -33 8 44 3 245 2 186 7 -77 4 -64 6 2(a) 原原矩阵的三元组表矩阵的三元组表897rn行数行数cn列数列数tn元素个数元素个数row col value1 3 -32 1 122 5 183 1 93 4 244 7 -67 6 -78 2 46 4 2(b)转置矩阵的三元组表转置矩阵的三元组表 矩阵的运算包括矩阵的转置、矩阵求逆、矩阵的加矩阵的运算包括矩阵的转置、矩阵求逆、矩阵的加减、矩阵的乘除等。在此,先讨论在这种压缩存储结构减、矩阵的乘除等。在此,先讨论在这种压缩存储结
129、构下的求矩阵的转置的运算。下的求矩阵的转置的运算。 一个一个m n的矩阵的矩阵A,它的转置,它的转置B是一个是一个n m的矩的矩阵,且阵,且bij=aji,0in,0jm,即,即B的行是的行是A的列,的列,B的列是的列是A的行。的行。 设稀疏矩阵设稀疏矩阵A是是按行优先顺序按行优先顺序压缩存储在三元组表压缩存储在三元组表a.data中,若仅仅是简单地交换中,若仅仅是简单地交换a.data中中i和和j的内容,得的内容,得到三元组表到三元组表b.data,b.data将是一个将是一个按列优先顺序按列优先顺序存储存储的稀疏矩阵的稀疏矩阵B,要得到按行优先顺序存储的,要得到按行优先顺序存储的b.data,就,就必须重新排列三元组表必须重新排列三元组表b.data中元素的顺序。中元素的顺序。 求转置矩阵的基本算法思想是:求转置矩阵的基本算法思想是: 将矩阵的行将矩阵的行、列下标值交换。即将三元组表中的列下标值交换。即将三元组表中的行行、列位置值列位置值i 、j相互相互交换交换; 重排三元组表重排三元组表中元素中元素的顺序。即交换后仍然是的顺序。即交换后仍然是按按行优先顺序行优先顺序排序的。排序的
130、。方法一方法一:算法思想算法思想:按稀疏矩阵按稀疏矩阵A的的三元组表三元组表a.data中的中的列次序列次序依次依次找到相应的三元组存入找到相应的三元组存入b.data中。中。 每找转置后矩阵的一个三元组,需从头至尾扫描整每找转置后矩阵的一个三元组,需从头至尾扫描整个三元组表个三元组表a.data 。找到之后自然就成为按行优先的转。找到之后自然就成为按行优先的转置矩阵的压缩存储表示。置矩阵的压缩存储表示。按方法一求转置矩阵的算法如下:按方法一求转置矩阵的算法如下:void TransMatrix(TMatrix a , TMatrix b) int p , q , col ;b.rn= ; =a.rn ; b.tn=a.tn ;/* 置置三元组表三元组表b.data的的行行、列数和非列数和非0元素个数元素个数 */if (b.tn=0) printf(“ The Matrix A=0n” );else q=0;for (col=1; col= ; col+) /* 每循环一次找到转置后的一个三元组每循环一次找到转置后的一个三元组 */for (p=0 ;pa.tn ; p+) /* 循
131、环次数是非循环次数是非0元素个数元素个数 */ if (a.datap.col=col) b.dataq.row=a.datap.col ; b.dataq.col=a.datap.row ; b.dataq.value=a.datap.value; q+ ; 算法分析算法分析:本算法主要的工作是在本算法主要的工作是在p和和col的两个循环的两个循环中完成的,故算法的时间复杂度为中完成的,故算法的时间复杂度为O(cn tn),即矩阵,即矩阵的列数和非的列数和非0元素的个数的乘积成正比。元素的个数的乘积成正比。而一般传统矩阵的转置算法为:而一般传统矩阵的转置算法为:for(col=1; col=n ;+col)for(row=0 ; row=m ;+row)bcolrow=arowcol ; 其时间复杂度为其时间复杂度为O(n m)。当非零元素的个数。当非零元素的个数tn和和m n同数量级时,算法同数量级时,算法TransMatrix的时间复杂度为的时间复杂度为O(m n2)。 由此可见,虽然节省了存储空间,但时间复杂度却由此可见,虽然节省了存储空间,但时间复杂度却大大增加。所以上述算法
132、只适合于稀疏矩阵中非大大增加。所以上述算法只适合于稀疏矩阵中非0元素元素的个数的个数tn远远小于远远小于m n的情况的情况。方法二方法二( (快速转置的算法快速转置的算法) ) 算法思想算法思想:直接按照稀疏矩阵直接按照稀疏矩阵A的的三元组表三元组表a.data的的次次序依次顺序转换序依次顺序转换,并将转换后的三元组,并将转换后的三元组放置于放置于三元组表三元组表b.data的的恰当位置恰当位置。 前提前提:若能预先确定原矩阵若能预先确定原矩阵A中每一列的中每一列的(即即B中每中每一行一行)第一个非第一个非0元素在元素在b.data中应有中应有的位置,则在作转的位置,则在作转置时就可直接放在置时就可直接放在b.data中恰当中恰当的位置。因此,应的位置。因此,应先求先求得得A中每一列的非中每一列的非0元素个数元素个数。附设两个辅助向量附设两个辅助向量num 和和cpot 。 numcol:统计:统计A中第中第col列中非列中非0元素的个数元素的个数; cpotcol :指示:指示A中第一个非中第一个非0元素在元素在b.data中的中的恰当恰当位置。位置。显然有位置对应关系:显然有位置对
133、应关系:cpot1=1cpotcol=cpotcol-1+numcol-1 例图例图5-8中的矩阵中的矩阵A和表和表5-9(a)的的相应的三元组表可相应的三元组表可以求得以求得numcol和和cpotcol的值如表的值如表5-1:numcol 1 2 2 1 0 1 1 1 col 1 2 3 4 5 6 7 8cpotcol 1 3 5 6 6 7 8 9表表5-1 numcol和和cpotcol的值表的值表快速转置算法如下:快速转置算法如下: void FastTransMatrix(TMatrix a, TMatrix b) int p , q , col , k ;int numMAX_SIZE , coptMAX_SIZE ;b.rn= ; =a.rn ; b.tn=a.tn ; /* 置置三元组表三元组表b.data的的行行、列数和非列数和非0元素个数元素个数 */ if (b.tn=0) printf(“ The Matrix A=0n” ) ;else for (col=1 ; col= ; +col) numcol=0 ; /* 向量向量num初始化为初始化为0 *
134、/ for (k=1 ; k=a.tn ; +k) +num a.datak.col ; /* 求原求原矩阵中每一列非矩阵中每一列非0元素个数元素个数 */for (cpot0=1, col=2 ; col= ; +col) cpotcol=cpotcol-1+numcol-1 ; /* 求第求第col列中第一个非列中第一个非0元在元在b.data中的序号中的序号 */for (p=1 ; p=a.tn ; +p) col=a.datap.col ; q=cpotcol ; b.dataq.row=a.datap.col ; b.dataq.col=a.datap.row ; b.dataq.value=a.datap.value ; +cpotcol ; /*至关重要至关重要!当本当本列中列中 */ 2 行逻辑链接的三元组顺序表行逻辑链接的三元组顺序表 将上述方法二中的辅助向量将上述方法二中的辅助向量cpot 固定在稀疏矩阵固定在稀疏矩阵的三元组表中,用来指示的三元组表中,用来指示“行行”的信息。得到另一种顺的信息。得到另一种顺序存储结构:序存储结构:行逻辑链接的三元组顺序表行逻辑链
135、接的三元组顺序表。其类型描述。其类型描述如下:如下:#define MAX_ROW 100typedef struct Triple dataMAX_SIZE ; /* 非非0元素的三元组表元素的三元组表 */ int rposMAX_ROW; /* 各行第一个非各行第一个非0位置表位置表 */ int rn ,cn , tn ; /* 矩阵的行、列数和非矩阵的行、列数和非0元个数元个数 */RLSMatrix ;稀疏矩阵的乘法稀疏矩阵的乘法设有两个矩阵:设有两个矩阵:A=(aij)m n ,B=(bij)n p则:则: C=(cij)m p 其中其中 cij=aik bkj 1kn , 1im ,1jp经典算法是三重循环:经典算法是三重循环:for ( i=1 ; i=m ; +i)for ( j=1 ; j=p ; +j) cij=0 ;for ( k=1 ; k=n ; +k) cij= cij+aik bkj;此算法的复杂度为此算法的复杂度为O(m n p)。 设有两个稀疏矩阵设有两个稀疏矩阵A=(aij)m n ,B=(bij)n p ,其,其存储存储结构采用行逻辑链接的三元
136、组顺序表。结构采用行逻辑链接的三元组顺序表。算法思想算法思想:对于对于A中的每个元素中的每个元素a.datap(p=1, 2, , a.tn),找到,找到B中所有满足条件:中所有满足条件: a.datap.col=b.dataq.row的元素的元素b.dataq,求得求得a.datap.value b.dataq.value,该乘积是,该乘积是cij中的一部中的一部分。求得所有这样的乘积并累加求和就能得到分。求得所有这样的乘积并累加求和就能得到cij。 为得到非为得到非0 0的乘积的乘积,只要,只要对对a.data1a.tn 中每个元中每个元素素(i,k,aik)(1ia.rn,) ,找到找到b.data中中所有相应的元素所有相应的元素(k,j,bkj)(1kb.rn,) 相相乘即可乘即可。则。则必须知道必须知道矩阵矩阵B中第中第k行的所有非行的所有非0元素,而元素,而b.rpos 向量中提供了相应的信息向量中提供了相应的信息。 b.rposrow指示了指示了矩阵矩阵B的第的第row行中第一个非行中第一个非0元元素在素在b.data 中的位置中的位置(序号序号),显然,显然,b.rpo
137、srow+1-1指指示了第示了第row行中最后一个非行中最后一个非0元素在元素在b.data 中的位置中的位置(序序号号) 。最后一。最后一行中最后一个非行中最后一个非0元素在元素在b.data 中的位置中的位置显然就是显然就是b.tn 。两个稀疏矩阵相乘的两个稀疏矩阵相乘的算法如下:算法如下:void MultsMatrix(RLSMatrix a, RLSMatrix b, RLSMatrix c) /* 求求矩阵矩阵A 、B的积的积C=A B,采用行逻辑链接的顺序表采用行逻辑链接的顺序表 * */ / elemtype ctempMax_Size ;int p , q , arow , ccol , brow , t ; if (!=b.rn) printf(“Errorn”) ; exit(0); else c.rn=a.rn ; =b. n ; c.tn=0 ; /* 初始化初始化C */if (a.tn*b.tn!=0) /* C 是非零矩阵是非零矩阵 */ for (arow=1 ; arow=a.rn ; +arow) ctemparow=0 ; /* 当前行累加器清零
138、当前行累加器清零 */ c.rposarow=c.tn+1; p=a.ropsarow; for ( ; pa.rposarow+1;+p) /* 对第对第arow行的每一个非行的每一个非0元素元素 */ brow=a.datap.col ; /* 找到元素在找到元素在b.data中的行号中的行号 */ if () t=( b.rposbrow+1; else t=b.tn+1 ; for (q=b.rposbrow ; qt ; +q) ccol=b.dataq.col ; /* 积元素在积元素在c中的列号中的列号 */ ctempccol+=a.datap.value*b.dataq.value ; /* 求出求出c中第中第arow行中的非行中的非0元素元素 */for (ccol=1 ; ccolMAX_SIZE) printf(“Errorn”) ; exit(0); elsec.datac.tn=(arow , ccol , ctempccol) ; 3 十字链表十字链表 对于稀疏矩阵,当非对于稀疏矩阵,当非0元素的个数和位置在操作过元素的个数和位置在操作过程中变化较大时,采
139、用链式存储结构表示比三元组的线程中变化较大时,采用链式存储结构表示比三元组的线性表更方便。性表更方便。 矩阵中非矩阵中非0元素的结点所含的域有:元素的结点所含的域有:行行、列列、值值、行指针行指针(指向同一行的下一个非指向同一行的下一个非0元元)、列指针列指针(指向同一指向同一列的下一个非列的下一个非0元元)。其次,十字交叉链表还有一个头结。其次,十字交叉链表还有一个头结点,结点的结构如图点,结点的结构如图5-10所示。所示。图图5-10 十字链十字链表结点结构表结点结构row col value down right rn cn tn down right(a) 结点结构结点结构(b) 头头结点结构结点结构 由定义知,稀疏矩阵中同一行的非由定义知,稀疏矩阵中同一行的非0元素的由元素的由right指针域链接成一个行链表指针域链接成一个行链表, 由由down指针域链接成一个指针域链接成一个列链表列链表。则每个非。则每个非0元素既是元素既是某个行链表中的一个结点某个行链表中的一个结点,同时又同时又是是某个列链表中的一个结点某个列链表中的一个结点,所有的,所有的非非0元素元素构构成一个成一个
140、十字交叉十字交叉的链表。称为的链表。称为十字链表十字链表。 此外,还可用两个此外,还可用两个一一维数组分别存储行维数组分别存储行链表的头指链表的头指针和列链表的头指针针和列链表的头指针。对于图。对于图5-11(a)的的稀疏矩阵稀疏矩阵A ,对,对应的十字交叉链表如图应的十字交叉链表如图5-11(b)所示,结点的描述如下:所示,结点的描述如下:typedef struct Clnode int row , col ; /* 行号和列号行号和列号 */ elemtype value ; /* 元素值元素值 */struct Clnode *down , *right ;OLNode ; /* 非非0元素结点元素结点 */typedef struct Clnode int rn; /* 矩阵的矩阵的行数行数 */ int cn; /* 矩阵的矩阵的列数列数 */int tn; /* 非非0元素总数元素总数 */OLNode *rhead ; OLNode *chead ; CrossList ;图图5-11 稀疏稀疏矩阵矩阵及其及其十字交叉链表十字交叉链表0 12 0 0 00 0 0 0
141、-40 5 0 0 00 0 3 0 0A=(a) 稀疏稀疏矩阵矩阵(b(b) 稀疏稀疏矩阵的十字交叉链表矩阵的十字交叉链表A.cheadA.rchead 1 2 12 3 2 5 2 5 -4 4 3 3 5.4 广义表广义表 广义表是线性表的推广和扩充,在人工智能领域广义表是线性表的推广和扩充,在人工智能领域中应用十分广泛。中应用十分广泛。 在第在第2章中,我们把线性表定义为章中,我们把线性表定义为n(n0 )个元素个元素a1, a2 , an的有穷序列,该序列中的所有元素具有相同的的有穷序列,该序列中的所有元素具有相同的数据类型且只能是原子项数据类型且只能是原子项(Atom)。所谓。所谓原子项可以是一原子项可以是一个数或一个结构,是指结构上不可再分的个数或一个结构,是指结构上不可再分的。若放松对元。若放松对元素的这种限制,容许它们具有其自身结构,就产生了广素的这种限制,容许它们具有其自身结构,就产生了广义表的概念。义表的概念。 广义表广义表(Lists,又称为列表又称为列表 ):是由是由n(n 0)个个元素组成的有穷序列:元素组成的有穷序列: LS=(a1,a2,an)其中其中a
142、i或者是原子项,或者是一个广义表。或者是原子项,或者是一个广义表。LS是广义是广义表的名字,表的名字,n为它的长度。若为它的长度。若ai是广义表,则称为是广义表,则称为LS的子表。的子表。习惯上:原子用习惯上:原子用小写字母小写字母,子表用,子表用大写字母大写字母。若广义表若广义表LS非空时:非空时: a1(表中第一个元素表中第一个元素)称为称为表头表头; 其余元素组成的子表称为其余元素组成的子表称为表尾表尾;(a2,a3,an) 广义表中所包含的元素广义表中所包含的元素(包括原子和子表包括原子和子表)的个数称的个数称为表的长为表的长 度。度。 广义表中括号的最大层数称为表深广义表中括号的最大层数称为表深 (度度)。有关广义表的这些概念的例子如表有关广义表的这些概念的例子如表5-2所示。所示。表表5-2 广义广义表及其示例表及其示例广广 义义 表表表长表长n 表深表深hA=()00B=(e)11C=(a,(b,c,d)22D=(A,B,C)33E=(a,E)2F=()12abecdABCD图图5-12 广义广义表的图形表示表的图形表示广义表的重要结论广义表的重要结论: 广义表的元素可以
143、是原子,也可以是子表,子表广义表的元素可以是原子,也可以是子表,子表的元素又可以是子表,的元素又可以是子表, 。即即广义表是一个多层次广义表是一个多层次的结构。的结构。 表表5-2中的中的广义表广义表D的图形表示如图的图形表示如图5 5-12所示所示。(2) (2) 广义表可以被其它广义表广义表可以被其它广义表所共享所共享,也可以,也可以共享共享其其它广义表。广义表它广义表。广义表共享共享其它广义表时通过表名引用。其它广义表时通过表名引用。(3) (3) 广义表本身可以是一个递归表。广义表本身可以是一个递归表。(4) (4) 根据对根据对表头、表尾表头、表尾的定义,任何一个非空广义表的定义,任何一个非空广义表的的表头表头可以是原子,也可以是子表,可以是原子,也可以是子表, 而表尾而表尾必定是必定是广义表。广义表。5.4.1 广义表的存储结构广义表的存储结构 由于广义表中的数据元素具有不同的结构,通常由于广义表中的数据元素具有不同的结构,通常用链式存储结构用链式存储结构表示,每个数据元素用一个结点表示。表示,每个数据元素用一个结点表示。因此,广义表中就有两类结点:因此,广义表中就有两类结
144、点: 一类是一类是表结点表结点,用来表示广义表项,由标志域,用来表示广义表项,由标志域,表头指针域,表尾指针域组成表头指针域,表尾指针域组成; 另一类是另一类是原子原子结点结点,用来表示原子项,由标志域,用来表示原子项,由标志域,原子的值域组成。原子的值域组成。如图如图5-13所示所示。 只要广义表非空,都是由只要广义表非空,都是由表头和表尾组成表头和表尾组成。即一个。即一个确定的确定的表头和表尾就唯一确定一个表头和表尾就唯一确定一个广义表。广义表。相应的数据结构定义如下:相应的数据结构定义如下:typedef struct GLNode int tag ; /* 标志域,为标志域,为1:表结点:表结点; ;为为0 :原子结点:原子结点 */union elemtype value; /* 原子结点的原子结点的值域值域 */struct struct GLNode *hp , *tp ; ptr ; /* ptr和和atom两成员共用两成员共用 */Gdata ; GLNode ; /* 广义表广义表结点类型结点类型 */标志标志tag=0 原子的原子的值值 标志标志tag=1 表头指
145、针表头指针hp 表尾指针表尾指针tp 图图5-13 广义广义表的链表结点结构示意图表的链表结点结构示意图(b) 表结点表结点(a) 原子结点原子结点例例: 对对A=(),B=(e),C=(a, (b, c, d) ),D=(A, B, C),E=(a, E)的广义表的存储结构的广义表的存储结构如图如图5 5-14所示所示。A=NULL1 0 eB10 aC10 b1 10 c1 0 dD1 1 11 10 aE1 图图5-14 广义广义表的存储结构示意图表的存储结构示意图对于上述对于上述存储结构,有如下几个特点:存储结构,有如下几个特点:(1) 若广义表为空,表头指针为空若广义表为空,表头指针为空;否则否则,表头指针,表头指针总是指向一个表结点,其中总是指向一个表结点,其中hp指向广义表的表头结点指向广义表的表头结点( (或为原子结点,或为表结点或为原子结点,或为表结点) ) ,tp指向广义表的表指向广义表的表尾尾( (表尾为空时,指针为空,否则必为表结点表尾为空时,指针为空,否则必为表结点) )。(2) 这种结构求这种结构求广义表的长度广义表的长度、深度深度、表头表头、表尾的表尾的操
146、作十分方便。操作十分方便。(3) 表结点太多表结点太多,造成空间浪费造成空间浪费。也可用。也可用图图5 5-15所示所示的结点结构的结点结构。图图5-15 广义广义表的链表结点结构示意图表的链表结点结构示意图(b) 表结点表结点(a) 原子结点原子结点tag=1 表头指针表头指针hp 表尾指针表尾指针tp tag=0 原子的原子的值值 表尾指针表尾指针tp 习习 题题 五五 什么是广义表?请简述广义表与线性表的区别?什么是广义表?请简述广义表与线性表的区别? 一个广义表是一个广义表是(a, (a, b), d, e, (a, (i, j), k) ,请画出,请画出该广义表的链式存储结构。该广义表的链式存储结构。 设有二维数组设有二维数组a68,每个元素占相邻的,每个元素占相邻的4个字个字节,存储器按字节编址,已知节,存储器按字节编址,已知a的起始地址是的起始地址是1000,试,试计算:计算: 数组数组a的最后一个元素的最后一个元素a57起始地址;起始地址; 按行序优先时,元素按行序优先时,元素a46起始地址;起始地址; 按行序优先时,元素按行序优先时,元素a46起始地址。起始地址。0
147、3 0 0 0 0 0 00 0 0 0 0 0 0 0-3 0 0 0 0 0 0 40 0 2 0 0 2 0 00 18 0 0 0 0 0 00 0 0 0 4 0 5 0A=0 0 -3 0 0 0 0 0 设设A和和B是稀疏矩阵,都以三元组作为存储结构,是稀疏矩阵,都以三元组作为存储结构,请写出矩阵相加的算法,其结果存放在三元组表请写出矩阵相加的算法,其结果存放在三元组表C中,中,并分析时间复杂度。并分析时间复杂度。 设有稀疏矩阵设有稀疏矩阵B如下图所示,请画出该稀疏矩阵如下图所示,请画出该稀疏矩阵的三元组表和十字链表存储结构。的三元组表和十字链表存储结构。第第6章章 树和二叉树树和二叉树 树型结构是一类非常重要的非线性结构。直观地,树型结构是一类非常重要的非线性结构。直观地,树型结构是树型结构是以分支关系定义的层次结构以分支关系定义的层次结构。 树在计算机领域中也有着广泛的应用,例如在编译树在计算机领域中也有着广泛的应用,例如在编译程序中,用树来表示源程序的语法结构;在数据库系统程序中,用树来表示源程序的语法结构;在数据库系统中,可用树来组织信息;在分析算法的行为时,可用
148、树中,可用树来组织信息;在分析算法的行为时,可用树来描述其执行过程等等。来描述其执行过程等等。 本章将详细讨论树和二叉树数据结构,主要介绍树本章将详细讨论树和二叉树数据结构,主要介绍树和二叉树的概念、术语,二叉树的遍历算法。树和二叉和二叉树的概念、术语,二叉树的遍历算法。树和二叉树的各种存储结构以及建立在各种存储结构上的操作及树的各种存储结构以及建立在各种存储结构上的操作及应用等。应用等。6.1 树的基本概念树的基本概念1 树的定义树的定义 树树(Tree)是是n(n0)个结点的有限集合个结点的有限集合T,若,若n=0时称时称为空树,否则:为空树,否则: 有且只有一个特殊的称为树的根有且只有一个特殊的称为树的根(Root)结点;结点; 若若n1时,其余的结点被分为时,其余的结点被分为m(m0)个个互不相交互不相交的子集的子集T1, T2, T3Tm,其中每个子集本身又是一棵树,其中每个子集本身又是一棵树,称其为根的子树,称其为根的子树(Subtree)。 这是树的递归定义,即用树来定义树,而只有一个这是树的递归定义,即用树来定义树,而只有一个结点的树必定仅由根组成,如图结点的树必定仅由
149、根组成,如图6-1(a)所示。所示。 6.1.1 树的定义和基本术语树的定义和基本术语2 树的基本术语树的基本术语 结点结点(node):一个数据元素及其若干指向其子树一个数据元素及其若干指向其子树的分支。的分支。 结点的度结点的度(degree) 、树的度树的度:结点所拥有的子结点所拥有的子树的棵数称为树的棵数称为结点的度结点的度。树中结点度的最大值称为。树中结点度的最大值称为树树的度的度。 图图6-1 树的示树的示例形式例形式AABDCEGFHIMJNKL(a) 只有根结点只有根结点(b) 一般的树一般的树 如图如图6-1(b)中结点中结点A的度是的度是3 ,结点,结点B的度是的度是2 ,结点,结点M的度是的度是0,树的度是,树的度是3 。 叶子叶子(left)结点结点、非叶子结点非叶子结点:树中树中度为度为0的的结结点称为点称为叶子结点叶子结点( (或终端结点或终端结点) )。相对应地,。相对应地,度不为度不为0的的结点称为结点称为非叶子结点非叶子结点(或非终端结点或分支结点或非终端结点或分支结点) )。除根结点外,分支结点又称为内部结点。除根结点外,分支结点又称为内部结点。 如
150、图如图6-1(b)中结点中结点H、I、J、K、L、M、N是叶子是叶子结点,而所有其它结点都是分支结点。结点,而所有其它结点都是分支结点。 孩子结点孩子结点、双亲结点双亲结点、兄弟结点兄弟结点 一个结点的一个结点的子树的根子树的根称为该结点的孩子结点称为该结点的孩子结点(child)或子结点或子结点;相应地,该结点是其孩子结点的双亲结点相应地,该结点是其孩子结点的双亲结点(parent)或父结点。或父结点。 如图如图6-1(b)中结点中结点B 、C、D是结点是结点A的子结点的子结点,而,而结点结点A是是结点结点B 、C、D的的父结点父结点;类似地结点类似地结点E 、F是是结点结点B的子结点的子结点,结点,结点B是是结点结点E 、F的的父结点。父结点。同一双亲结点的所有子结点互称为同一双亲结点的所有子结点互称为兄弟结点兄弟结点。 如图如图6-1(b)中结点中结点B 、C、D是兄弟结点;结点是兄弟结点;结点E 、F是兄弟结点。是兄弟结点。 层次层次、堂兄弟结点堂兄弟结点 规定树中根结点的层次为规定树中根结点的层次为1,其余结点的层次等于,其余结点的层次等于其双亲结点的层次加其双亲结点的层次加
151、1。 若某结点在第若某结点在第l(l1)层,则其子结点在第层,则其子结点在第l+1层。层。 双亲结点在同一层上的所有结点互称为双亲结点在同一层上的所有结点互称为堂兄弟结点堂兄弟结点。如图如图6-1(b)中结点中结点E、F、G、H、I、J。 结点的层次路径结点的层次路径、祖先祖先、子孙子孙 从根结点开始,到达某结点从根结点开始,到达某结点p所经过的所有结点成所经过的所有结点成为为结点结点p的的层次路径层次路径( (有且只有一条有且只有一条) )。 结点结点p的层次路径上的所有结点(的层次路径上的所有结点(p除外)称为除外)称为p的的祖先祖先(ancester) 。 以某一结点为根的子树中的任意结点称为该结点的以某一结点为根的子树中的任意结点称为该结点的子孙结点子孙结点(descent)。 树的深度树的深度(depth):树中结点的最大层次值,又树中结点的最大层次值,又称为树的高度,如图称为树的高度,如图6-1(b)中树的高度为中树的高度为4。 有序树和无序树有序树和无序树:对于一棵树,若其中每一个对于一棵树,若其中每一个结点的子树(若有)具有一定的次序,则该树称为结点的子树(若有)具有一
152、定的次序,则该树称为有有序树序树,否则称为,否则称为无序树无序树。 森林森林(forest):是是m(m0)棵互不相交的棵互不相交的树的集树的集合。显然,若将一棵树的根结点删除,剩余的子树就合。显然,若将一棵树的根结点删除,剩余的子树就构成了森林。构成了森林。3 树的表示形式树的表示形式 倒悬树倒悬树。是最常用的表示形式,如图是最常用的表示形式,如图6-1(b)。 嵌套集合嵌套集合。是一些集合的集体,对于任何两个集是一些集合的集体,对于任何两个集合,或者不相交,或者一个集合包含另一个集合。图合,或者不相交,或者一个集合包含另一个集合。图6-2(a)是图是图6-1(b)树的嵌套集合形式。树的嵌套集合形式。 广义表形式广义表形式。图图6-2(b)是树的广义表形式。是树的广义表形式。 凹入法表示形式凹入法表示形式。见见P120 树的表示方法的多样化说明了树结构的重要性。树的表示方法的多样化说明了树结构的重要性。图图6-2 树的表示树的表示形式形式(a) 嵌套集合嵌套集合形式形式(b) 广义表广义表形式形式(A(B(E(K,L),F),C(G(M,N),D(H,I,J)HIJDFBKLECM
153、NGA6.1.2 树的抽象数据类型定义树的抽象数据类型定义ADT Tree数据对象数据对象D:D是具有相同数据类型的数据元素的集是具有相同数据类型的数据元素的集合合。数据关系数据关系R:若:若D为空集为空集,则称为空树则称为空树; 基本操作:基本操作: ADT Tree 详见详见p118119。6.2 二叉树二叉树6.2.1 二叉树的定义二叉树的定义1 二叉树的定义二叉树的定义 二叉树二叉树(Binary tree)是是n(n0)个结点的有限集合。个结点的有限集合。若若n=0时称为空树,否则:时称为空树,否则: 有且只有一个特殊的称为树的根有且只有一个特殊的称为树的根(Root)结点;结点; 若若n1时,其余的结点被分成为时,其余的结点被分成为二个互不相交二个互不相交的子的子集集T1,T2,分别称之为左,分别称之为左、右子树,并且左右子树,并且左、右子树右子树又都是二叉树。又都是二叉树。 由此可知,二叉树的由此可知,二叉树的定义是递归定义是递归的。的。 二叉树在树结构中起着非常重要的作用。因为二叉二叉树在树结构中起着非常重要的作用。因为二叉树结构简单,存储效率高,树的操作算法相对简单,
154、且树结构简单,存储效率高,树的操作算法相对简单,且任何树都很容易转化成二叉树结构。上节中引入的有关任何树都很容易转化成二叉树结构。上节中引入的有关树的术语也都适用于二叉树。树的术语也都适用于二叉树。2 二叉树的基本形态二叉树的基本形态 二叉树有二叉树有5种基本形态,如图种基本形态,如图6-3所示。所示。AAAA(a)(b)(c)(d)(e)(a) 空空二叉树二叉树(b) 单结点单结点二叉树二叉树(c) 右子树为空右子树为空(d) 左子树为空左子树为空(e) 左左、右子树都不空右子树都不空图图6-3 二叉二叉树的基本树的基本形态形态6.2.2 二叉树的性质二叉树的性质性质性质1:在非空二叉树中,第在非空二叉树中,第i i层上至多有层上至多有2i-1个结点个结点(i1)。证明证明:用数学归纳法证明。用数学归纳法证明。 当当i=1时:只有一个根结点,时:只有一个根结点,21-1=20 =1,命题成立。,命题成立。 现假设对现假设对i1时,处在第时,处在第i-1层上至多有层上至多有2(i-1)-1个结点。个结点。 由归纳假设知,第由归纳假设知,第i-1层上至多有层上至多有2i-2个结点。由于二
155、个结点。由于二叉树每个结点的度最大为叉树每个结点的度最大为2,故在第,故在第i i层上最大结点数为层上最大结点数为第第i-1层上最大结点数的层上最大结点数的2倍。倍。 即即 22i-22i-1 证毕证毕性质性质2:深度为深度为k的二叉树至多有的二叉树至多有2k-1个结点个结点(k1) 。证明证明:深度为深度为k的二叉树的最大的结点数为二叉树中每的二叉树的最大的结点数为二叉树中每层上的最大结点数之和。层上的最大结点数之和。 由性质由性质1知知,二叉树的第,二叉树的第1层层、第第2层层 第第k层上的结层上的结点数至多有:点数至多有: 20、21 2k-1 。 总的总的结点数至多有:结点数至多有: 20+21+ + +2k-1=2k-1 证毕证毕 性质性质3:对任何一棵二叉树,若其叶子结点数为对任何一棵二叉树,若其叶子结点数为n0,度为度为2的结点数为的结点数为n2,则,则n0=n2+1。证明:证明:设二叉树中度为设二叉树中度为1的结点数为的结点数为n1,二叉树中总结,二叉树中总结点数为点数为N,因为二叉树中所有结点均小于或等于,因为二叉树中所有结点均小于或等于2,则有:,则有:N=n0+n
156、1+n2再看二叉树中的分支数:再看二叉树中的分支数: 除根结点外,其余每个结点都有唯一的一个进入分除根结点外,其余每个结点都有唯一的一个进入分支,而所有这些分支都是由度为支,而所有这些分支都是由度为1和和2的结点射出的。设的结点射出的。设B为二叉树中的分支总数,则有:为二叉树中的分支总数,则有: N=B+1 Bn1+2 n2 N=B+1=n1+2 n2+1 n0+n1+n2=n1+2 n2+1 即即 n0=n2+1 证毕证毕满二叉树和完全二叉树满二叉树和完全二叉树 一棵深度为一棵深度为k且有且有2k-1个结点的二叉树称为个结点的二叉树称为满二叉满二叉树树(Full Binary Tree)。 如图如图6-4(a) 就是一棵深度为就是一棵深度为4的满二叉树。的满二叉树。894101151213614157213894101152112673(a) 满二叉树满二叉树(b) 完全二叉树完全二叉树1362455674213(c) 非完全二叉树非完全二叉树图图6-4 特殊形态的特殊形态的二叉二叉树树满二叉树的特点满二叉树的特点: 基本特点是每一层上的结点数总是最大结点数。基本特点是每一层上的结点
157、数总是最大结点数。 满二叉树的所有的支结点都有左满二叉树的所有的支结点都有左、右子树。右子树。 可对满二叉树的结点进行连续编号,若规定从根可对满二叉树的结点进行连续编号,若规定从根结点开始,按结点开始,按“自上而下自上而下、自左至右自左至右”的原则进行。的原则进行。完全二叉树完全二叉树( (Complete Binary Tree) ):如果深度为:如果深度为k,由,由n个结点的二叉树,当且仅当其每一个结点都与深度为个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从的满二叉树中编号从1到到n的结点一一对应,该二叉树称的结点一一对应,该二叉树称为完全二叉树。为完全二叉树。 或深度为或深度为k的满二叉树中编号从的满二叉树中编号从1到到n的前的前n个结点构个结点构成了一棵深度为成了一棵深度为k的完全二叉树。的完全二叉树。其中其中 2k-1 n2k-1 。 完全二叉树是满二叉树的一部分,而满二叉树是完完全二叉树是满二叉树的一部分,而满二叉树是完全二叉树的特例。全二叉树的特例。完全二叉树的特点完全二叉树的特点: 若完全二叉树的深度为若完全二叉树的深度为k ,则所有的叶子结点都出现
158、,则所有的叶子结点都出现在第在第k k层或层或k-1层。对于任一结点,如果其右子树的最大层。对于任一结点,如果其右子树的最大层次为层次为l,则其左子树的最大层次为,则其左子树的最大层次为l或或l+1 1。性质性质4:n个结点的完全二叉树深度为:个结点的完全二叉树深度为: 2n + +1 1。 其中符号:其中符号: x 表示不大于表示不大于x的最大整数。的最大整数。 x 表示不小于表示不小于x的最小整数。的最小整数。证明:证明:假设完全二叉树的深度为假设完全二叉树的深度为k,则根据性质,则根据性质2及完及完全二叉树的定义有:全二叉树的定义有:2k-1-1n2k-1 或或 2 k-1n2k 取对数得:取对数得:k12n1,则其双亲结点编号是,则其双亲结点编号是 i/2 。 如果如果2in:则结点:则结点i为叶子结点,无左孩子;否则,为叶子结点,无左孩子;否则,其左孩子结点编号是其左孩子结点编号是2i。 如果如果2i+1n:则结点:则结点i无右孩子;否则,其右孩子无右孩子;否则,其右孩子结点编号是结点编号是2i+1。 证明证明:用数学归纳法证明。首先证明用数学归纳法证明。首先证明和和,然后,
159、然后由由和和导出导出。 当当i=1时时,由完全二叉树的定义知,结点,由完全二叉树的定义知,结点i的左孩子的左孩子的编号是的编号是2,右孩子的编号是,右孩子的编号是3。 若若2n,则二叉树中不存在编号为,则二叉树中不存在编号为2的结点,说明的结点,说明结点结点i的左的左孩子孩子不存在。不存在。 若若3n,则二叉树中不存在编号为,则二叉树中不存在编号为3的结点,说明的结点,说明结点结点i的右的右孩子孩子不存在。不存在。 现假设对于编号为现假设对于编号为j(1ji)的结点的结点,(2)(2)和和(3)(3)成立。成立。即:即: 当当2jn :结点:结点j的左孩子编号是的左孩子编号是2j;当;当2jn时时,结点结点j的左孩子结点不存在。的左孩子结点不存在。 当当2j+1n :结点:结点j的右孩子编号是的右孩子编号是2j+1;当;当2j+1n时,结点时,结点j的右孩子结点不存在。的右孩子结点不存在。 当当i=j+1时,由完全二叉树的定义知,若结点时,由完全二叉树的定义知,若结点i的左的左孩子结点存在,则其左孩子结点的编号一定等于编号为孩子结点存在,则其左孩子结点的编号一定等于编号为j的右孩子的编
160、号加的右孩子的编号加1,即结点,即结点i的左孩子的编号为:的左孩子的编号为: (2j+1)+1=2(j+1)=2i如图如图6-5所示,且有所示,且有2in。相反,若。相反,若2in,则左孩子结,则左孩子结点不存在。同样地,若结点点不存在。同样地,若结点i的右孩子结点存在,则其的右孩子结点存在,则其右孩子的编号为:右孩子的编号为:2i+1,且有,且有2i+1n。相反,若。相反,若2i+1n,则左孩子结点不存在。结论,则左孩子结点不存在。结论(2)(2)和和(3)(3)得证。得证。 再由再由(2)(2)和和(3)(3)来证明来证明(1) 。 当当i=1时时,显然编号为,显然编号为1的的是根结点,无双亲结点。是根结点,无双亲结点。 当当i1时,设编号为时,设编号为i的结点的双亲结点的编号为的结点的双亲结点的编号为m,若编号为若编号为i的结点是其双亲结点的左孩子,则由的结点是其双亲结点的左孩子,则由(2)有:有:i=2m ,即,即m= i/2 ;若编号为若编号为i的结点是其的结点是其双亲结点的右孩子,则由双亲结点的右孩子,则由(3)有:有:i=2m+1 ,即,即m= (i-1) /2 ; 当当
161、i1时时,其双亲结点的编号为,其双亲结点的编号为 i/2 。 证毕证毕ii+12i2i+12i+22i+3i/2(a) i和和i+1结点在同一层结点在同一层i+12i+22i+3i2i2i+1(b) i和和i+1结点不在同一层结点不在同一层图图6-5 完全完全二叉二叉树中结点树中结点i和和i+1的左右孩子的左右孩子6.2.3 二叉树的存储结构二叉树的存储结构1 顺序存储结构顺序存储结构 二叉树存储结构的类型定义:二叉树存储结构的类型定义:#define MAX_SIZE 100 typedef telemtype sqbitreeMAX_SIZE; 用一组地址连续的存储单元依次用一组地址连续的存储单元依次“自上而下自上而下、自左自左至右至右”存储完全二叉树的数据元素。存储完全二叉树的数据元素。 对于完全二叉树上编号为对于完全二叉树上编号为i的结点元素存储在一维数的结点元素存储在一维数组的下标值为组的下标值为i-1的分量中的分量中,如图,如图6-6(c)所示。所示。 对于一般的二叉树,将其每个结点与完全二叉树上对于一般的二叉树,将其每个结点与完全二叉树上的结点相对照,的结点相对照,存储在
162、一维数组中存储在一维数组中,如图,如图6-6(d)所示。所示。abcdhiejklfg(a) 完全二叉树完全二叉树(b) 非完全二叉树非完全二叉树abcdefgh1 2 3 4 5 6 7 8 9 10 11 12 a b c d e f g h i j k l (c) 完全二叉树的顺序存储形式完全二叉树的顺序存储形式1 2 3 4 5 6 7 8 9 10 11a b c d e h f g(d) 非完全二叉树的顺序存储形式非完全二叉树的顺序存储形式图图6-6 二叉二叉树及其树及其顺序存储形式顺序存储形式 最坏的情况下,一个深度为最坏的情况下,一个深度为k且只有且只有k个结点的单支个结点的单支树需要长度为树需要长度为2k-1的一维数组的一维数组。2 链式存储结构链式存储结构 设计不同的结点结构可构成不同的链式存储结构。设计不同的结点结构可构成不同的链式存储结构。(1) 结点的类型及其定义结点的类型及其定义 二叉链表结点二叉链表结点。有三个域:一个数据域,两个分。有三个域:一个数据域,两个分别指向左右子结点的指针域,如图别指向左右子结点的指针域,如图6-7(a)所示。所示。 typed
163、ef struct BTNode ElemType data ;struct BTNode *Lchild , *Rchild ;BTNode ; 三三叉链表结点叉链表结点。除二叉链表的三个域外,再增加一。除二叉链表的三个域外,再增加一个指针域,用来指向结点的父结点,如图个指针域,用来指向结点的父结点,如图6-7(b)所示。所示。typedef struct BTNode_3 ElemType data ;struct BTNode_3 *Lchild , *Rchild , *parent ;BTNode_3 ; Lchild data RchildLchild data parent Rchild(a) 二叉链表结点二叉链表结点(b) 三三叉链表结点叉链表结点图图6-7 链表结点结构链表结点结构形式形式(2) 二叉树的链式存储形式二叉树的链式存储形式 例有一棵一般的二叉树,如图例有一棵一般的二叉树,如图6-8(a)所示。以二叉链所示。以二叉链表和三叉链表方式存储的结构图分别如图表和三叉链表方式存储的结构图分别如图6-8(b) 、 6-8(c)所示。所示。图图6-8 二叉树及其二叉树
164、及其链式存储结构链式存储结构(a) 二叉树二叉树afedcbg(c) 三三叉链表叉链表 a b c d e f g T(b) 二二叉链表叉链表 a b c d e g f T6.3 遍历二叉树及其应用遍历二叉树及其应用遍历二叉树遍历二叉树(Traversing Binary Tree):是指是指按指定按指定的规律的规律对二叉树中的对二叉树中的每个结点访问一次且仅访问一次每个结点访问一次且仅访问一次。 所谓所谓访问访问是指对结点做某种处理。如:输出信息是指对结点做某种处理。如:输出信息、修改结点的值等修改结点的值等。 二叉树是一种非线性结构,每个结点都可能有左二叉树是一种非线性结构,每个结点都可能有左、右两棵子树,因此,需要寻找一种规律,使二叉树上的右两棵子树,因此,需要寻找一种规律,使二叉树上的结点能排列在一个线性队列上,从而便于遍历。结点能排列在一个线性队列上,从而便于遍历。 二叉树的基本组成:根结点二叉树的基本组成:根结点、左子树左子树、右子树。若右子树。若能依次遍历这三部分,就是遍历了二叉树。能依次遍历这三部分,就是遍历了二叉树。 若以若以L、D、R分别表示遍历左子树、遍历根结点
165、和分别表示遍历左子树、遍历根结点和遍历右子树,遍历右子树,则有六种遍历方案:则有六种遍历方案:DLR、LDR、LRD、DRL、RDL、RLD。若规定若规定先左后右先左后右,则只有,则只有前三种前三种情况情况三种情况,分别是:三种情况,分别是:DLR先先( (根根) )序遍历。序遍历。LDR中中( (根根) )序遍历。序遍历。LRD后后( (根根) )序遍历。序遍历。 对于二叉树的遍历,分别讨论递归遍历算法和非递对于二叉树的遍历,分别讨论递归遍历算法和非递归遍历算法。递归遍历算法具有非常清晰的结构,但初归遍历算法。递归遍历算法具有非常清晰的结构,但初学者往往难以接受或怀疑,不敢使用。实际上,递归算学者往往难以接受或怀疑,不敢使用。实际上,递归算法是由系统通过使用堆栈来实现控制的。而非递归算法法是由系统通过使用堆栈来实现控制的。而非递归算法中的控制是由设计者定义和使用堆栈来实现的。中的控制是由设计者定义和使用堆栈来实现的。6.3.1 先序遍历二叉树先序遍历二叉树1 递归算法递归算法算法的递归定义是:算法的递归定义是: 若二叉树为空,则遍历结束;否则若二叉树为空,则遍历结束;否则 访问根结点
166、;访问根结点; 先序遍历左子树先序遍历左子树(递归调用本算法递归调用本算法); 先序遍历右子树先序遍历右子树(递归调用本算法递归调用本算法)。先序遍历的递归算法先序遍历的递归算法void PreorderTraverse(BTNode *T) if (T!=NULL) visit(T-data) ; /* 访问根结点访问根结点 */PreorderTraverse(T-Lchild) ;PreorderTraverse(T-Rchild) ; 说明:说明:visit()函数是访问结点的数据域,其要求视具体函数是访问结点的数据域,其要求视具体问题而定。树采用二叉链表的存储结构,用指针变量问题而定。树采用二叉链表的存储结构,用指针变量T T来指向。来指向。2 非递归算法非递归算法设设T是指向二叉树根结点的指针变量,非递归算法是:是指向二叉树根结点的指针变量,非递归算法是:若二叉树为空,则返回;否则,令若二叉树为空,则返回;否则,令p=T; 访问访问p所指向的结点;所指向的结点; q=p-Rchild ,若,若q不为空,则不为空,则q进栈;进栈; p=p-Lchild ,若,若p不为空,转不
167、为空,转(1),否则转,否则转(4); 退栈到退栈到p ,转,转(1),直到栈空为止。,直到栈空为止。算法实现算法实现:#define MAX_NODE 50void PreorderTraverse( BTNode *T) BTNode *StackMAX_NODE ,*p=T, *q ;int top=0 ;if (T=NULL) printf(“ Binary Tree is Empty!n”) ;else do visit( p- data ) ; q=p-Rchild ; if ( q!=NULL ) stack+top=q ; p=p-Lchild ; if (p=NULL) p=stacktop ; top- ; while (p!=NULL) ;6.3.2 中序遍历二叉树中序遍历二叉树1 递归算法递归算法算法的递归定义是:算法的递归定义是: 若二叉树为空,则遍历结束;否则若二叉树为空,则遍历结束;否则 中序遍历左子树中序遍历左子树(递归调用本算法递归调用本算法); 访问根结点;访问根结点; 中序遍历右子树中序遍历右子树(递归调用本算法递归调用本算法)。中序遍历的递归算法
168、中序遍历的递归算法void InorderTraverse(BTNode *T) if (T!=NULL) InorderTraverse(T-Lchild) ;visit(T-data) ; /* 访问根结点访问根结点 */InorderTraverse(T-Rchild) ; /*图图6-8(a) 的二叉树,输出的次序是:的二叉树,输出的次序是: cbegdfa */2 非递归算法非递归算法设设T是指向二叉树根结点的指针变量,非递归算法是:是指向二叉树根结点的指针变量,非递归算法是:若二叉树为空,则返回;否则,令若二叉树为空,则返回;否则,令p=T 若若p不为空,不为空,p进栈,进栈, p=p-Lchild ; 否则否则(即即p为空为空),退栈到,退栈到p,访问,访问p所指向的结点;所指向的结点; p=p-Rchild ,转,转(1);直到栈空为止。直到栈空为止。算法实现算法实现:#define MAX_NODE 50void InorderTraverse( BTNode *T) BTNode *StackMAX_NODE ,*p=T ; int top=0 , bool=1 ;
169、 if (T=NULL) printf(“ Binary Tree is Empty!n”) ; else do while (p!=NULL) stack+top=p ; p=p-Lchild ; if (top=0) bool=0 ; else p=stacktop ; top- ; visit( p-data ) ; p=p-Rchild ; while (bool!=0) ; 6.3.3 后序遍历二叉树后序遍历二叉树1 递归算法递归算法算法的递归定义是:算法的递归定义是: 若二叉树为空,则遍历结束;否则若二叉树为空,则遍历结束;否则 后序遍历左子树后序遍历左子树(递归调用本算法递归调用本算法); 后序遍历右子树后序遍历右子树(递归调用本算法递归调用本算法) ; 访问根结点访问根结点 。后序遍历的递归算法后序遍历的递归算法void PostorderTraverse(BTNode *T) if (T!=NULL) PostorderTraverse(T-Lchild) ;PostorderTraverse(T-Rchild) ; visit(T-data) ; /* 访问根结点访
170、问根结点 */ /*/*图图6-8(a) 的二叉树,输出的次序是:的二叉树,输出的次序是: cgefdba */cgefdba */ 遍历二叉树的算法中基本操作是访问结点,因此,遍历二叉树的算法中基本操作是访问结点,因此,无论是哪种次序的遍历,对有无论是哪种次序的遍历,对有n个结点的二叉树,其时个结点的二叉树,其时间复杂度均为间复杂度均为O(n) 。 如图如图6-9所示的二叉树表示表达式:所示的二叉树表示表达式:(a+b*(c-d)-e/f)按不同的次序遍历此二叉树,将访问的结点按先后次序按不同的次序遍历此二叉树,将访问的结点按先后次序排列起来的次序是:排列起来的次序是: 其先序序列为:其先序序列为: -+a*b-cd/ef 其中序序列为:其中序序列为: a+b*c-d-e/f 其后序序列为:其后序序列为: abcd-*+ef/-/fe-dcb*a+图图6-9 表达式表达式 (a+b*(c-d)-e/f)二叉树二叉树2 非递归算法非递归算法 在后序遍历中,根结点是最后被访问的。因此,在在后序遍历中,根结点是最后被访问的。因此,在遍历过程中,当搜索指针指向某一根结点时,不能立即遍历过程中
171、,当搜索指针指向某一根结点时,不能立即访问,而要先遍历其左子树,此时访问,而要先遍历其左子树,此时根结点进栈根结点进栈。当其左。当其左子树遍历完后再搜索到该根结点时,还是不能访问,还子树遍历完后再搜索到该根结点时,还是不能访问,还需遍历其右子树。所以,此需遍历其右子树。所以,此根结点还需再次进栈根结点还需再次进栈,当其,当其右子树遍历完后再退栈到到该根结点时,才能被访问。右子树遍历完后再退栈到到该根结点时,才能被访问。 因此,设立一个状态标志变量因此,设立一个状态标志变量tag :0 : 结点暂不能访问结点暂不能访问1 : 结点可以被访问结点可以被访问tag= 其次,设两个堆栈其次,设两个堆栈S1、S2 ,S1保存结点,保存结点,S2保存结保存结点的点的状态标志变量状态标志变量tag 。S1和和S2共用一个栈顶共用一个栈顶指针。指针。 设设T是指向根结点的指针变量,非递归算法是:是指向根结点的指针变量,非递归算法是:若二叉树为空,则返回;否则,令若二叉树为空,则返回;否则,令p=T; 第一次经过根结点第一次经过根结点p,不访问:,不访问: p进栈进栈S1 , tag 赋值赋值0,进栈,
172、进栈S2,p=p-Lchild 。 若若p不为空,转不为空,转(1),否则,取状态标志值,否则,取状态标志值tag : 若若tag=0:对栈:对栈S1,不访问,不出栈;修改,不访问,不出栈;修改S2栈顶栈顶元素值元素值(tag赋值赋值1) ,取,取S1栈顶元素的右子树,即栈顶元素的右子树,即p=S1top-Rchild ,转,转(1); 若若tag=1:S1退栈,访问该结点;退栈,访问该结点;直到栈空为止。直到栈空为止。算法实现算法实现:#define MAX_NODE 50void PostorderTraverse( BTNode *T) BTNode *S1MAX_NODE ,*p=T ;int S2MAX_NODE , top=0 , bool=1 ;if (T=NULL) printf(“Binary Tree is Empty!n”) ;else do while (p!=NULL) S1+top=p ; S2top=0 ; p=p-Lchild ; if (top=0) bool=0 ; else if (S2top=0) p=S1top-Rchild ; S2top=1
173、 ; else p=S1top ; top- ; visit( p-data ) ; p=NULL ; /* 使循环继续进行而不至于死循环使循环继续进行而不至于死循环 */ while (bool!=0) ;6.3.4 层次遍历二叉树层次遍历二叉树 层次遍历二叉树,是从根结点开始遍历,按层次次层次遍历二叉树,是从根结点开始遍历,按层次次序序“自上而下自上而下,从左至右从左至右”访问树中的各结点。访问树中的各结点。 为保证是按层次遍历,必须设置一个队列,初始化为保证是按层次遍历,必须设置一个队列,初始化时为空。时为空。 设设T是指向根结点的指针变量,层次遍历非递归算是指向根结点的指针变量,层次遍历非递归算法是:法是:若二叉树为空,则返回;否则,令若二叉树为空,则返回;否则,令p=T,p入队;入队; 队首元素出队到队首元素出队到p;访问访问p所指向的结点;所指向的结点; 将将p所指向的结点的左、右子结点依次入队。直到所指向的结点的左、右子结点依次入队。直到队空为止。队空为止。#define MAX_NODE 50void LevelorderTraverse( BTNode *T) BTN
174、ode *QueueMAX_NODE ,*p=T ;int front=0 , rear=0 ;if (p!=NULL) Queue+rear=p; /* 根结点入队根结点入队 */while (frontdata ); if (p-Lchild!=NULL) Queue+rear=p; /* 左结点入队左结点入队 */ if (p-Rchild!=NULL) Queue+rear=p; /* 左结点入队左结点入队 */ “遍历遍历”是二叉树最重要的基本操作,是各种其它是二叉树最重要的基本操作,是各种其它操作的基础,二叉树的许多其它操作都可以通过遍历来操作的基础,二叉树的许多其它操作都可以通过遍历来实现。如建立二叉树的存储结构、求二叉树的结点数、实现。如建立二叉树的存储结构、求二叉树的结点数、求二叉树的深度等。求二叉树的深度等。6.3.5 二叉树遍历算法的应用二叉树遍历算法的应用1 二叉树的二叉链表创建二叉树的二叉链表创建 按满二叉树方式建立按满二叉树方式建立 (补充补充) 在此补充按满二叉树的方式对结点进行编号建立链式在此补充按满二叉树的方式对结点进行编号建立链式二叉树。对每个结点,
175、输入二叉树。对每个结点,输入i、ch。i : 结点编号,按从小到大的顺序输入结点编号,按从小到大的顺序输入ch : 结点内容,假设是字符结点内容,假设是字符 在建立过程中借助一个一维数组在建立过程中借助一个一维数组Sn ,编号为,编号为i的结的结点保存在点保存在Si中中。算法实现算法实现:#define MAX_NODE 50typedef struct BTNode char data ;struct BTNode *Lchild , *Rchild ;BTNode ;BTNode *Create_BTree(void) /* 建立链式二叉树,返回指向根结点的指针变量建立链式二叉树,返回指向根结点的指针变量 */ BTNode *T , *p , *sMAX_NODE ; char ch ; int i , j ;while (1) scanf(“%d”, &i) ;if (i=0) break ; /* 以编号以编号0作为输入结束作为输入结束 */else ch=getchar() ; p=(BTNode *)malloc(sizeof(BTNode) ; pdata=ch ; p
176、Lchild=pRchild=NULL ; si=p ; if (i=1) T=p ; else j=i/2 ; /* j是是i的双亲结点编号的双亲结点编号 */ if (i%2=0) sj-Lchild=p ; else sj-Rchild=p ; return(T) ; 按先序遍历方式建立按先序遍历方式建立 对一棵二叉树进行对一棵二叉树进行“扩充扩充”,就可以得到有该二叉,就可以得到有该二叉树所扩充的二叉树。有两棵二叉树树所扩充的二叉树。有两棵二叉树T1及其扩充的二叉树及其扩充的二叉树T2如图如图6-10所示。所示。图图6-10 二叉树二叉树T1及其扩充及其扩充二叉树二叉树T2ABCDEFG(a) 二叉树二叉树T1(b) T1的扩充的扩充二叉树二叉树T2ABCDEFG? 二叉树的扩充方法是:在二叉树中结点的每一个空二叉树的扩充方法是:在二叉树中结点的每一个空链域处增加一个扩充的结点链域处增加一个扩充的结点(总是叶子结点,用方框总是叶子结点,用方框“”表示表示)。对于二叉树的结点值:。对于二叉树的结点值: 是是char类型:扩充结点值为类型:扩充结点值为“?”; 是是int类型:扩充
177、结点值为类型:扩充结点值为0或或-1; 下面的算法是二叉树的前序创建的递归算法,读入下面的算法是二叉树的前序创建的递归算法,读入一棵二叉树对应的扩充二叉树的前序遍历的结点值序列。一棵二叉树对应的扩充二叉树的前序遍历的结点值序列。每读入一个结点值就进行分析:每读入一个结点值就进行分析: 若是扩充结点值:令根指针为若是扩充结点值:令根指针为NULL; 若是若是(正常正常)结点值:动态地为根指针分配一个结结点值:动态地为根指针分配一个结点,将该值赋给根结点,然后递归地创建根的左子点,将该值赋给根结点,然后递归地创建根的左子树和右子树。树和右子树。算法实现算法实现:#define NULLKY ?#define MAX_NODE 50typedef struct BTNode char data ;struct BTNode *Lchild , *Rchild ;BTNode ;BTNode *Preorder_Create_BTree(BTNode *T) /* 建立链式二叉树,返回指向根结点的指针变量建立链式二叉树,返回指向根结点的指针变量 */ char ch ; ch=getchar(
178、) ; getchar(); if (ch=NULLKY) T=NULL; return(T) ; else T=(BTNode *)malloc(sizeof(BTNode) ;Tdata=ch ;Preorder_Create_BTree(T-Lchild) ;Preorder_Create_BTree(T-Rchild) ;return(T) ; 当希望创建图当希望创建图6-10(a)所示的二叉树时,输入的字符所示的二叉树时,输入的字符序列应当是:序列应当是:ABD?E?G?CF?2 求二叉树的叶子结点数求二叉树的叶子结点数 可以直接利用先序遍历二叉树算法求二叉树的叶子可以直接利用先序遍历二叉树算法求二叉树的叶子结点数。只要将先序遍历二叉树算法中结点数。只要将先序遍历二叉树算法中vist()函数简单地函数简单地进行修改就可以。进行修改就可以。算法实现算法实现:#define MAX_NODE 50int search_leaves( BTNode *T) BTNode *StackMAX_NODE ,*p=T;int top=0, num=0;if (T!=NULL) stack
179、+top=p ; while (top0) p=stacktop- ; if (p-Lchild=NULL&p-Rchild=NULL) num+ ; if (p-Rchild!=NULL ) stack+top=p-Rchild; if (p-Lchild!=NULL ) stack+top=p-Lchild; return(num) ;3 求二叉树的深度求二叉树的深度 利用层次遍历算法可以直接求得二叉树的深度。利用层次遍历算法可以直接求得二叉树的深度。算法实现算法实现:#define MAX_NODE 50int search_depth( BTNode *T) BTNode *StackMAX_NODE ,*p=T;int front=0 , rear=0, depth=0, level ;/* level总是指向访问层的最后一个结点在队列的位置总是指向访问层的最后一个结点在队列的位置 */if (T!=NULL) Queue+rear=p; /* 根结点入队根结点入队 */level=rear ; /* 根是第根是第1层的最后一个节点层的最后一个节点 */while (fron
180、tLchild!=NULL) Queue+rear=p; /* 左结点入队左结点入队 */ if (p-Rchild!=NULL) Queue+rear=p; /* 左结点入队左结点入队 */ if (front=level) /* 正访问的是当前层的最后一个结点正访问的是当前层的最后一个结点 */ depth+ ; level=rear ; 遍历二叉树是按一定的规则将树中的结点排列成一遍历二叉树是按一定的规则将树中的结点排列成一个线性序列个线性序列,即是对非线性结构的线性化操作。如何找,即是对非线性结构的线性化操作。如何找到到遍历过程中动态得到遍历过程中动态得到的每个结点的直接前驱和直接后的每个结点的直接前驱和直接后继继( (第一个和最后一个除外第一个和最后一个除外)?)?如何保存这些信息如何保存这些信息? ? 设一棵二叉树有设一棵二叉树有n个结点个结点,则有,则有n-1条边条边(指针连线指针连线) , 而而n个结点共有个结点共有2n个指针域个指针域(Lchild和和Rchild) ,显然,显然有有n+1个空闲指针域个空闲指针域未用未用。则可以利用这些。则可以利用这些空闲的指针空闲的
181、指针域来存放结点的域来存放结点的直接前驱和直接后继信息。直接前驱和直接后继信息。对结点的指针域做如下规定对结点的指针域做如下规定: 6.4 线索树线索树 若结点有左孩子,则若结点有左孩子,则Lchild指向其左孩子,否则,指向其左孩子,否则,指向其直接前驱;指向其直接前驱; 若结点有右孩子若结点有右孩子,则则Rchild指向其右孩子指向其右孩子,否则,否则,指向其直接后继;指向其直接后继;为避免混淆为避免混淆, ,对结点结构加以改进,增加两个标志域,对结点结构加以改进,增加两个标志域,如图如图6-10所示。所示。Lchild Ltag data Rchild Rtag图图6-10 线索二叉树的结点结构线索二叉树的结点结构0:Lchild域指示结点的左孩子域指示结点的左孩子1 1:Lchild域指示结点的前驱域指示结点的前驱Ltag=0 0:Rchild域指示结点的右孩子域指示结点的右孩子1 1:Rchild域指示结点的后继域指示结点的后继Rtag= 用这种结点结构构成的二叉树的存储结构;叫做线用这种结点结构构成的二叉树的存储结构;叫做线索链表;指向结点前驱和后继的指针叫做线索;按照某索
182、链表;指向结点前驱和后继的指针叫做线索;按照某种次序遍历,加上线索的二叉树称之为线索二叉树。种次序遍历,加上线索的二叉树称之为线索二叉树。线索二叉树的结点结构与示例线索二叉树的结点结构与示例typedef struct BiThrNode ElemType data;struct BiTreeNode *Lchild , *Rchild ; int Ltag , Rtag ;BiThrNode ; 如图如图6-11是二叉树及相应的各种线索树示例。是二叉树及相应的各种线索树示例。AFHIEGBDC(a) 二叉树二叉树 (b) 先序线索树的逻辑形式先序线索树的逻辑形式 结点序列:结点序列:ABDCEGFHIAFHIEGBDCNIL(d) 后序线索树的逻辑形式后序线索树的逻辑形式 结点序列:结点序列:DBGEHIFCA(c) 中序线索树的逻辑形式中序线索树的逻辑形式 结点序列:结点序列:DBAGECHFIAFHIEGBDCNILNILAFHIEGBDCNIL 0 A 0 0 B 1 0 C 0 1 D 1 0 E 1 0 F 0 1 G 1 1 H 1 1 F 1 (e) 中序线索树的链表结
183、构中序线索树的链表结构图图6-11 线索二叉树及其存储结构线索二叉树及其存储结构说明说明:画线索二叉树时,画线索二叉树时,实线实线表示指针,指向其左表示指针,指向其左、右右孩子;孩子;虚线虚线表示线索,指向其直接前驱或直接后继。表示线索,指向其直接前驱或直接后继。 在线索树上进行遍历,只要先找到序列中的第一个在线索树上进行遍历,只要先找到序列中的第一个结点,然后就可以依次找结点的直接后继结点直到后继结点,然后就可以依次找结点的直接后继结点直到后继为空为止。为空为止。 如何在线索树中找结点的直接后继如何在线索树中找结点的直接后继? ?以图以图6-11(d) ,(e)所示的中序线索树为例:所示的中序线索树为例: 树中树中所有叶子结点的右链都是所有叶子结点的右链都是线索线索。右链直接指右链直接指示了结点的直接后继示了结点的直接后继,如结点,如结点G的直接后继是结点的直接后继是结点E。 树中树中所有非叶子结点的右链都是所有非叶子结点的右链都是指针指针。根据中序遍。根据中序遍历的规律,历的规律,非叶子结点的直接后继是遍历其右子树时非叶子结点的直接后继是遍历其右子树时访问的第一个结点访问的第一个结
184、点,即右子树中最左下的,即右子树中最左下的( (叶子叶子) )结点。结点。如结点如结点C的直接后继的直接后继:沿右指针找到右子树的根结点:沿右指针找到右子树的根结点F,然后沿左链往下直到,然后沿左链往下直到Ltag=1的结点即为的结点即为C的直接后的直接后继结点继结点H。 如何在线索树中找结点的直接前驱如何在线索树中找结点的直接前驱? ?若若结点的结点的Ltag=1,则左链是线索,指示其直接前驱;否则,遍历,则左链是线索,指示其直接前驱;否则,遍历左子树时访问的最后一个结点左子树时访问的最后一个结点( (即沿左子树中最右往下即沿左子树中最右往下的结点的结点) ) 为其为其直接前驱结点。直接前驱结点。 对于后序遍历的线索树中找结点的直接后继比较复对于后序遍历的线索树中找结点的直接后继比较复杂,可分以下三种情况杂,可分以下三种情况: 若若结点是二叉树的根结点结点是二叉树的根结点:其:其直接后继为空直接后继为空; 若若结点是其父结点的左孩子或右孩子且其父结点结点是其父结点的左孩子或右孩子且其父结点没有右子树没有右子树:直接后继为其直接后继为其父父结点结点; 若若结点是其父结点的左孩子且其父结
185、点有右子树结点是其父结点的左孩子且其父结点有右子树:直接后继是对其直接后继是对其父父结点的右子树按后序遍历的第一个结点的右子树按后序遍历的第一个结点结点。6.4.1 线索化二叉树线索化二叉树 二叉树的线索化二叉树的线索化指的是依照某种遍历次序使二叉树指的是依照某种遍历次序使二叉树成为线索二叉树的过程。成为线索二叉树的过程。 线索化的过程就是线索化的过程就是在遍历过程中修改空指针使其指向在遍历过程中修改空指针使其指向直接前驱或直接后继直接前驱或直接后继的过程。的过程。 仿照线性表的存储结构,在二叉树的线索链表上也添仿照线性表的存储结构,在二叉树的线索链表上也添加一个头结点加一个头结点head,头结点的指针域的安排是:,头结点的指针域的安排是: Lchild域:指向二叉树的根结点;域:指向二叉树的根结点; Rchild域:指向中序遍历时的最后一个结点;域:指向中序遍历时的最后一个结点; 二叉树中序序列中的二叉树中序序列中的第一个结点第一个结点Lchild指针域指针域和和最后一个结点最后一个结点Rchild指针域指针域均指向头结点均指向头结点head。 如同为二叉树建立了一个双向线索链表,对
186、一棵线如同为二叉树建立了一个双向线索链表,对一棵线索二叉树既可从头结点也可从最后一个结点开始按寻找索二叉树既可从头结点也可从最后一个结点开始按寻找直接后继进行遍历。显然,这种遍历不需要堆栈,如图直接后继进行遍历。显然,这种遍历不需要堆栈,如图6-12所示所示。结点类型定义结点类型定义#define MAX_NODE 50typedef enmuLink , Thread PointerTag ;/* Link=0表示指针,表示指针, Thread=1表示线索表示线索 */typedef struct BiThrNode ElemType data;struct BiTreeNode *Lchild , *Rchild ; PointerTag Ltag , Rtag ;BiThrNode;(a) 二叉树二叉树(b) 中序线索树的逻辑形式中序线索树的逻辑形式AFHIEGBDCNILNILAFHIEGBDC图图6-12 中序线索二叉树及其存储结构中序线索二叉树及其存储结构(c) 中序线索二叉链表 0 A 0 0 B 1 0 C 0 1 D 1 0 E 1 0 F 0 1 G 1 1 H 1
187、 1 F 1Thrt 0 1head1 先序线索化二叉树先序线索化二叉树void preorder_Threading(BiThrNode *T) BiThrNode *stackMAX_NODE;BiThrNode *last=NULL, *p ;int top=0 ;if (T!=NULL) stack+top=T;while (top0) p=stacktop- ; if (p-Lchild!=NULL) p-Ltag=0 ; else p-Ltag=1 ; p-Lchild!=last ; if (last!=NULL) if (last-Rchild!=NULL) last-Rtag=0 ; else last-Rtag=1 ; last-Rchild!=p ; last=p ; if (p-Rchild!=NULL) stack+top=p-Rchild ; if (p-Lchild!=NULL) stack+top=p-Lchild ; Last-Rtag=1; /* 最后一个结点是叶子结点最后一个结点是叶子结点 */2 中序线索化二叉树中序线索化二叉树void inor
188、der_Threading(BiThrNode *T) BiThrNode *stackMAX_NODE;BiThrNode *last=NULL, *p=T ;int top=0 ;while (p!=NULL|top0)if (p!=NULL) stack+top=p; p=p-Lchild; else p=stacktop- ; if (p-Lchild!=NULL) p-Ltag=0 ; else p-Ltag=1 ; p-Lchild!=last ; if (last!=NULL) if (last-Rchild!=NULL) last-Rtag=0 ; else last-Rtag=1 ; last-Rchild!=p ; last=p ; P=p-Rchild; last-Rtag=1; /* 最后一个结点是叶子结点最后一个结点是叶子结点 */6.4.2 线索二叉树的遍历线索二叉树的遍历 在线索二叉树中,由于有线索存在,在某些情况下在线索二叉树中,由于有线索存在,在某些情况下可以方便地找到指定结点在某种遍历序列中的直接前驱可以方便地找到指定结点在某种遍历序列中的直接前驱或
189、直接后继。此外,在线索二叉树上进行某种遍历比在或直接后继。此外,在线索二叉树上进行某种遍历比在一般的二叉树上进行这种遍历要容易得多,不需要设置一般的二叉树上进行这种遍历要容易得多,不需要设置堆栈,且算法十分简洁。堆栈,且算法十分简洁。1 先序线索二叉树的先序遍历先序线索二叉树的先序遍历void preorder_Thread_bt(BiThrNode *T) BiThrNode *p=T ;while (p!=NULL) visit(p-data) ; if (p-Ltag=0) p=p-Lchild ; else p=p-Rchild 2 中序线索二叉树的中序遍历中序线索二叉树的中序遍历void inorder_Thread_bt(BiThrNode *T) BiThrNode *p ;if (T!=NULL) p=T;while (p-Ltag=0 ) p=p-Lchild; /* 寻找最左的结点寻找最左的结点 */while (p!=NULL) visit(p-data) ; if (p-Rtag=1) p=p-Rchild ; /* 通过右线索找到后继通过右线索找到后继 */
190、else /* 否则,右子树的最左结点为后继否则,右子树的最左结点为后继 */ p=p-Rchild ; while (p-Ltag=0 ) p=p-Lchild; 6.5 树与森林树与森林 本节将讨论树的存储结构、树及森林与二叉树之间本节将讨论树的存储结构、树及森林与二叉树之间的相互转换、树的遍历等。的相互转换、树的遍历等。6.5.1 树的存储结构树的存储结构 树的存储结构根据应用的不同而不同树的存储结构根据应用的不同而不同。1 双亲表示法双亲表示法(顺序存储结构顺序存储结构) 用一组连续的存储空间来存储树的结点用一组连续的存储空间来存储树的结点,同时在同时在每个结点中附加一个每个结点中附加一个指示器指示器(整数域整数域) ,用以指示双亲结用以指示双亲结点的位置点的位置(下标值下标值) 。数组元素及数组的类型定义如下。数组元素及数组的类型定义如下:#define MAX_SIZE 100typedef struct PTNode ElemType data ;int parent ;PTNode ;typedef struct PTNode NodesMAX_SIZE ;int ro
191、ot; /* 根结点位置根结点位置 */int num ; /* 结点数结点数 */ Ptree ; 图图6-13所示是一棵树及其双亲所示是一棵树及其双亲表示的存储结构表示的存储结构。这种存储结构利这种存储结构利用了任一结点的父结点唯一的性质用了任一结点的父结点唯一的性质。可以方便地直接找到可以方便地直接找到任一结点的父任一结点的父结点结点,但求结点的子结点时需要扫但求结点的子结点时需要扫描整个数组描整个数组。FGHIRABCDE图图6-13 树的双亲存储结构树的双亲存储结构R -10A 01B 02C 03D 14E 15F 36G 67H 68I 692 孩子链表表示法孩子链表表示法 树中每个结点有多个指针域,每个指针指向其一棵树中每个结点有多个指针域,每个指针指向其一棵子树的根结点子树的根结点。有。有两种结点结构两种结点结构。 定长结点结构定长结点结构 指针域的数目就是树的度指针域的数目就是树的度。 其特点是其特点是:链表结构简单,但指针域的浪费明显:链表结构简单,但指针域的浪费明显。结点结构如图结点结构如图6-14(a) 所示所示。在一棵有在一棵有n个结点,度为个结点,度为k的
192、树中必有的树中必有n(k-1)+1空指针域空指针域。 不定长结点结构不定长结点结构 树中每个结点的指针域数量不同,是该结点的度,树中每个结点的指针域数量不同,是该结点的度,如图如图6-14(b) 所示。没有多余的指针域,但操作不便。所示。没有多余的指针域,但操作不便。图图6-14 孩子表示法的结点结构孩子表示法的结点结构data child1 child2 childn(a) 定长结点结构定长结点结构(b) 不不定长结点结构定长结点结构data k child1 child2 childk 复合链表结构复合链表结构 对于树中的每个结点,其孩子结点用带头结点的单对于树中的每个结点,其孩子结点用带头结点的单链表表示,表结点和头结点的结构如图链表表示,表结点和头结点的结构如图6-15所示所示。 n个结点的树有个结点的树有n个个(孩子孩子)单链表单链表(叶子结点的孩子链叶子结点的孩子链表为空表为空),而,而n个头结点又组成一个线性表且以顺序存储个头结点又组成一个线性表且以顺序存储结构表示结构表示。data firstchild(a) 头结点头结点childno next(b) 表结点表结点图图
193、6-15 孩子链表结点结构孩子链表结点结构数据结构类型定义如下:数据结构类型定义如下:#define MAX_NODE 100typedef struct listnode int childno ; /* 孩子结点编号孩子结点编号 */struct listno *next ;CTNode; /* 表结点结构表结点结构 */typedef struct ElemType data ;CTNode *firstchild ;HNode; /* 头结点结构头结点结构 */typedef struct HNode nodesMAX_NODE ;int root; /* 根结点位置根结点位置 */int num ; /* 结点数结点数 */CLinkList; /* 头结点结构头结点结构 */ 图图6-13所示的树所示的树T的孩子链表表示的存储结构如图的孩子链表表示的存储结构如图6-16所示。所示。nodes789 35 012 6 0123456789MAX_NODE-1rootnumA B C D E F G H I R 49图图6-16 图图6-13的树的树T的孩子链表存储结构的孩子链
194、表存储结构3 孩子兄弟表示法孩子兄弟表示法(二叉树表示法二叉树表示法) 以二叉链表作为树的存储结构,其结点形式如图以二叉链表作为树的存储结构,其结点形式如图6-17(a)所示所示。 两个指针域:分别指向结点的第一个子结点和下一两个指针域:分别指向结点的第一个子结点和下一个兄弟结点。结点类型定义如下:个兄弟结点。结点类型定义如下:typedef struct CSnode ElemType data ;struct CSnode *firstchild, *nextsibing ;CSNode; 图图6-17(b)所示树的孩子兄弟表示的存储结构如图所示树的孩子兄弟表示的存储结构如图6-17(c)。图图6-17 树及孩子兄弟存储结构树及孩子兄弟存储结构(b) 树树 FGRABCDE(c) 孩子兄弟存储结构孩子兄弟存储结构 R A D C G B F E 孩子结点孩子结点兄弟结点兄弟结点firstchild data nextsibing(a) 结点结构结点结构6.5.2 森林与二叉树的转换森林与二叉树的转换 由于二叉树和树都可用二叉链表作为存储结构,对由于二叉树和树都可用二叉链表作为存储结
195、构,对比各自的结点结构可以看出,以二叉链表作为媒介可以比各自的结点结构可以看出,以二叉链表作为媒介可以导出树和二叉树之间的一个对应关系。导出树和二叉树之间的一个对应关系。 从物理结构来看,树和二叉树的二叉链表是相同从物理结构来看,树和二叉树的二叉链表是相同的,只是对指针的逻辑解释不同而已。的,只是对指针的逻辑解释不同而已。 从树的二叉链表表示的定义可知,任何一棵和树从树的二叉链表表示的定义可知,任何一棵和树对应的二叉树,其右子树一定为空。对应的二叉树,其右子树一定为空。 图图6-18直观地展示了树和二叉树之间的对应关系。直观地展示了树和二叉树之间的对应关系。图图6-18 树与二叉树的对应关系树与二叉树的对应关系二叉树二叉树 CERADB R A D C B E 树树 RABCDE对应关系对应关系 R A D C B E C B E R A D 存储存储解释解释存储存储解释解释1 树转换成二叉树树转换成二叉树 对于一般的树,可以方便地转换成一棵唯一的二叉对于一般的树,可以方便地转换成一棵唯一的二叉树与之对应。将树转换成二叉树在树与之对应。将树转换成二叉树在“孩子兄弟表示法孩子兄弟表示法”
196、中已给出,其详细步骤是:中已给出,其详细步骤是: 加虚线加虚线。在树的每层按从。在树的每层按从“左至右左至右”的顺序在兄的顺序在兄弟结点之间加虚线相连。弟结点之间加虚线相连。 去连线去连线。除最左的第一个子结点外,父结点与所。除最左的第一个子结点外,父结点与所有其它子结点的连线都去掉。有其它子结点的连线都去掉。 旋转旋转。将树顺时针旋转。将树顺时针旋转450,原有的实线左斜。,原有的实线左斜。 整型整型。将旋转后树中的所有虚线改为实线,并向。将旋转后树中的所有虚线改为实线,并向右斜。该转换过程如图右斜。该转换过程如图6-19所示。所示。 这样转换后的二叉树的特点是这样转换后的二叉树的特点是: 二叉树的二叉树的根结点没有右子树根结点没有右子树,只有左,只有左子树;子树; 左子结点仍然是原来树中相应结点的左子结点仍然是原来树中相应结点的左子结点,而所有沿右链往下的右子结点左子结点,而所有沿右链往下的右子结点均是原来树中该结点的兄弟结点。均是原来树中该结点的兄弟结点。图图6-19 树向二叉树的转换过程树向二叉树的转换过程(a) 一般的树一般的树 FGRABCDEFGRABCDE(b) 加虚线
197、加虚线,去连线后去连线后 (C) 转换后的二叉树转换后的二叉树FGRACDBE2 二叉树转换成树二叉树转换成树 对于一棵转换后的二叉树,如何还原成原来的树对于一棵转换后的二叉树,如何还原成原来的树? 其步骤是:其步骤是: 加虚线加虚线。若某结点。若某结点i是其父结点的左子树的根结点,是其父结点的左子树的根结点,则将该结点则将该结点i的右子结点以及沿右子链不断地搜索所的右子结点以及沿右子链不断地搜索所有的右子结点,将所有这些右子结点与有的右子结点,将所有这些右子结点与i结点的父结结点的父结点之间加虚线相连,点之间加虚线相连,如图如图6-20(a)所示所示。 去连线去连线。去掉二叉树中所有父结点与其右子结点。去掉二叉树中所有父结点与其右子结点之间的连线,之间的连线,如图如图6-20(b)所示所示。 规整化规整化。将图中各结点按层次排列且将所有的虚。将图中各结点按层次排列且将所有的虚线变成实线,线变成实线,如图如图6-20(c)所示。所示。图图6-20 二叉树向树的转换过程二叉树向树的转换过程(C) 还原后的树还原后的树FGRABCDE(b) 去连线后去连线后 (a) 加虚线后加虚线后 FG
198、RACDBECFGRADBE3 森林转换成二叉树森林转换成二叉树 当一般的树转换成二叉树后,二叉树的右子树必为当一般的树转换成二叉树后,二叉树的右子树必为空空。若把森林中的第二棵树。若把森林中的第二棵树( (转换成二叉树后转换成二叉树后) )的根结点的根结点作为第一棵树作为第一棵树(二叉树二叉树)的根结点的兄弟结点,则可导出的根结点的兄弟结点,则可导出森林转换成二叉树的转换算法如下:森林转换成二叉树的转换算法如下: 设设F=T1, T2, ,Tn是森林,则按以下规则可转换成是森林,则按以下规则可转换成一棵二叉树一棵二叉树B=(root,LB,RB) 若若n=0,则,则B是空树是空树。 若若n0,则二叉树,则二叉树B的根是森林的根是森林T1的根的根root(T1),B的左子树的左子树LB是是B(T11,T12, ,T1m) ,其中,其中T11,T12, ,T1m是是T1的子树的子树(转换后转换后),而其右子树,而其右子树RB是从森林是从森林F=T2, T3, ,Tn转换而成的二叉树转换而成的二叉树。转换步骤转换步骤: 将将F=T1, T2, ,Tn 中的每棵树转换成二叉树中的每棵树转换成
199、二叉树。 按给出的森林中树的次序,从最后一棵二叉树开按给出的森林中树的次序,从最后一棵二叉树开始,每棵二叉树作为前一棵二叉树的根结点的右子始,每棵二叉树作为前一棵二叉树的根结点的右子树,依次类推,则第一棵树的根结点就是转换后生树,依次类推,则第一棵树的根结点就是转换后生成的二叉树的根结点,成的二叉树的根结点,如图如图6-21所示所示。ACBDGMLHK(a) 森林森林图图6-21 森林转换成二叉树的过程森林转换成二叉树的过程(b) 森林中每棵树森林中每棵树 对应的二叉树对应的二叉树ABCDGLKHM(c) 森林对应的二叉树森林对应的二叉树ABCDGLKHM4 二叉树转换成森林二叉树转换成森林 若若B=(root,LB,RB)是一棵二叉树,则可以将其是一棵二叉树,则可以将其转换成由若干棵树构成的森林:转换成由若干棵树构成的森林:F=T1, T2, ,Tn 。转换算法转换算法: 若若B是空树,则是空树,则F为空为空。 若若B非空,则非空,则F中第一棵树中第一棵树T1的根的根root(T1)就是二就是二叉树的根叉树的根root, T1中根结点的子森林中根结点的子森林F1是由树是由树B的左的左
200、子树子树LB转换而成的森林转换而成的森林;F中除中除T1外其余树组成的外其余树组成的的森林的森林F=T2, T3, ,Tn 是由是由B右子树右子树RB转换得到的转换得到的森林森林。 上述转换规则是递归的上述转换规则是递归的,可以写出其递归算法。以可以写出其递归算法。以下给出具体的还原步骤。下给出具体的还原步骤。 去连线去连线。将二叉树。将二叉树B的根结点与其右子结点以及的根结点与其右子结点以及沿右子结点链方向的所有右子结点的连线全部去掉,沿右子结点链方向的所有右子结点的连线全部去掉,得到若干棵孤立的二叉树,每一棵就是原来森林得到若干棵孤立的二叉树,每一棵就是原来森林F中中的树依次对应的二叉树,的树依次对应的二叉树,如图如图6-22(b)所示所示。 二叉树的还原二叉树的还原。将各棵孤立的二叉树按二叉树还。将各棵孤立的二叉树按二叉树还原为树的方法还原成一般的树,原为树的方法还原成一般的树,如图如图6- 22(c)所示所示。图图6-22 二叉树还原成森林的过程二叉树还原成森林的过程ACBDMGLHK(c) 还原成森林还原成森林(a) 二叉树二叉树ABCDGLKHM(b) 去连线后去连线后AB
201、CDMGLKH6.5.3 树和森林的遍历树和森林的遍历1 树的遍历树的遍历 由树结构的定义可知,树的遍历有二种方法。由树结构的定义可知,树的遍历有二种方法。 先序遍历先序遍历:先访问根结点,然后:先访问根结点,然后依次先序遍历依次先序遍历完完每棵子树。每棵子树。如图如图6-23的树,先序遍历的次序是:的树,先序遍历的次序是:ABCDEFGIJHK 后序遍历后序遍历:先:先依次后序遍历完依次后序遍历完每棵子树,然后访每棵子树,然后访问根结点。问根结点。如图如图6-23的树,后序遍历的次序是:的树,后序遍历的次序是:CDBFGIJHEKA说明说明: 树的树的先序遍历先序遍历实质上与将树转换成二叉树实质上与将树转换成二叉树后对二叉树的后对二叉树的先序遍历先序遍历相同。相同。 树的树的后序遍历后序遍历实质上与将树转换成二叉树实质上与将树转换成二叉树后对二叉树的后对二叉树的中序遍历中序遍历相同。相同。ABDCKGJIFHE图图6-23 树树2 森林的遍历森林的遍历 设设F=T1, T2, ,Tn是森林,对是森林,对F的遍历有二种方法。的遍历有二种方法。 先序遍历先序遍历:按:按先序遍历先序遍历树
202、的方式树的方式依次依次遍历遍历F中的中的每棵树。每棵树。 中序遍历中序遍历:按:按后序遍历后序遍历树的方式树的方式依次依次遍历遍历F中的中的每棵树。每棵树。6.6 赫夫曼树及其应用赫夫曼树及其应用 赫夫曼赫夫曼(Huffman)树又称最优树,是一类带权路径树又称最优树,是一类带权路径长度最短的树,有着广泛的应用。长度最短的树,有着广泛的应用。6.6.1 最优二叉树最优二叉树(Huffman树树)1 基本概念基本概念 结点路径结点路径:从树中一个结点到另一个结点的之间:从树中一个结点到另一个结点的之间的分支构成这两个结点之间的路径。的分支构成这两个结点之间的路径。 路径长度路径长度:结点路径上的分支数目称为路径长度:结点路径上的分支数目称为路径长度。 树的路径长度树的路径长度:从树根到每一个结点的路径长度:从树根到每一个结点的路径长度之和之和。 例图例图6-23的树的树。A到到F :结点路径:结点路径 AEF ;路径长;路径长度度( (即边的数目即边的数目) ) 2 ;树的路径长度:;树的路径长度:3 3 1+5 2+2 3=19 结点的带权路径长度结点的带权路径长度:从该结点的到树的根
203、结点:从该结点的到树的根结点之间的路径长度与结点的权之间的路径长度与结点的权( (值值) )的乘积的乘积。权权( (值值) ):各种:各种开销开销、代价代价、频度频度等的抽象称呼等的抽象称呼。 树的带权路径长度树的带权路径长度:树中:树中所有叶子结点所有叶子结点的带权路的带权路径长度之和,记做:径长度之和,记做: WPL=w1 l1+w2 l2+ +wn ln=wi li (i=1,2, ,n)其中:其中:n为叶子结点的个数为叶子结点的个数;wi为第为第i个结点的权值个结点的权值; li为第为第i个结点的路径长度。个结点的路径长度。 Huffman树树:具有:具有n个叶子结点个叶子结点(每个结点的权值每个结点的权值为为wi) 的二叉树不止一棵,但在所有的这些二叉树中,的二叉树不止一棵,但在所有的这些二叉树中,必定存在一棵必定存在一棵WPL值最小值最小的树,称这棵树为的树,称这棵树为Huffman树树(或称最优树或称最优树) 。 在许多判定问题时,利用在许多判定问题时,利用Huffman树可以得到最佳树可以得到最佳判断算法。判断算法。 如图如图6-24是权值分别为是权值分别为2、3、6、
204、7,具有,具有4个叶子结个叶子结点的二叉树,它们的带权路径长度分别为点的二叉树,它们的带权路径长度分别为:(a) WPL=2 2+3 2+6 2+7 2=36 ;(b) WPL=2 1+3 2+6 3+7 3=47 ;(c) WPL=7 1+6 2+2 3+3 3=34 。 其中其中(c)的的 WPL值最小,可以证明是值最小,可以证明是Huffman树。树。236736726723(a)(b)(c)图图6-24 具有相同叶子结点,不同带权路径长度的二叉树具有相同叶子结点,不同带权路径长度的二叉树2 Huffman树的构造树的构造 根据根据n个权值个权值w1, w2, ,wn,构造成,构造成n棵二叉树的棵二叉树的集合集合F=T1, T2, ,Tn,其中每棵二叉树只有一个权值,其中每棵二叉树只有一个权值为为wi的根结点,没有左、右子树;的根结点,没有左、右子树; 在在F中中选取两棵根结点权值最小选取两棵根结点权值最小的树作为左的树作为左、右右子树构造一棵新的二叉树,且新的二叉树根结点权子树构造一棵新的二叉树,且新的二叉树根结点权值为其左值为其左、右子树根结点的权值之和;右子树根结点的权值之
205、和; 在在F中删除这两棵树,同时将新得到的树加入中删除这两棵树,同时将新得到的树加入F中;中; 重复重复、,直到,直到F只含一颗树为止。只含一颗树为止。 构造构造Huffman树时树时,为了规范为了规范,规定规定F=T1,T2, ,Tn中权值小的二叉树作为新构造的二叉树的左子树中权值小的二叉树作为新构造的二叉树的左子树,权值大的二叉树作为新构造的二叉树的右子树权值大的二叉树作为新构造的二叉树的右子树;在取值;在取值相等时,相等时,深度小的二叉树作为新构造的二叉树的左子树深度小的二叉树作为新构造的二叉树的左子树,深度大的二叉树作为新构造的二叉树的右子树深度大的二叉树作为新构造的二叉树的右子树。 图图6-25是权值集合是权值集合W=8, 3, 4, 6, 5, 5构造构造Huffman树的过程树的过程。所构造的所构造的Huffman树的树的WPL是是: WPL=6 2+3 3+4 3+8 2+5 3+5 3 =79345568第一步5568第二步34768第三步34755108第四步5510634713第六步1111100000855101863471331图图6-25 Huffman树的
206、构造过程树的构造过程第五步85510186347136.6.2 赫夫曼编码及其算法赫夫曼编码及其算法1 Huffman编码编码 在电报收发等数据通讯中在电报收发等数据通讯中,常需要将传送的文字转常需要将传送的文字转换成由二进制字符换成由二进制字符0、1组成的字符串来传输组成的字符串来传输。为了使收为了使收发的速度提高发的速度提高,就要求电文就要求电文编码要尽可能地短编码要尽可能地短。此外,。此外,要设计要设计长短不等长短不等的编码,还必须保证的编码,还必须保证任意字符的编码都任意字符的编码都不是另一个字符编码的前缀不是另一个字符编码的前缀,这种编码称为,这种编码称为前缀编码前缀编码。 Huffman树可以用来构造编码长度不等且译码不产树可以用来构造编码长度不等且译码不产生二义性的编码生二义性的编码。 设电文中的字符集设电文中的字符集C=c1,c2, ,ci, ,cn,各个字符出,各个字符出现的次数或频度集现的次数或频度集W=w1,w2, ,wi, ,wn。Huffman编码方法编码方法 以以字符集字符集C作为叶子结点作为叶子结点,次数或频度集次数或频度集W作为结作为结点的权值点的权值来
207、构造来构造 Huffman树树。规定。规定Huffman树中左分支树中左分支代表代表“0”,右分支代表,右分支代表“1” 。 从根结点到每个叶子结点所经历的路径分支上的从根结点到每个叶子结点所经历的路径分支上的“0”或或“1”所组成的字符串所组成的字符串,为该结点所对应的编码为该结点所对应的编码,称之为称之为Huffman编码编码。 由于每个字符都是叶子结点,不可能出现在根结点由于每个字符都是叶子结点,不可能出现在根结点到其它字符结点的路径上,所以一个字符的到其它字符结点的路径上,所以一个字符的Huffman编编码不可能是另一个字符的码不可能是另一个字符的Huffman编码的前缀编码的前缀。 若字符集若字符集C=a, b, c, d, e, f所对应的权值集合为所对应的权值集合为W=8, 3, 4, 6, 5, 5,如图,如图6-25所示所示,则字符,则字符a,b, c,d, e,f所所对应的对应的Huffman编码分别是编码分别是:10,010,011,00 ,110,111。2 Huffman编码算法实现编码算法实现(1) 数据结构设计数据结构设计 Huffman树中没有度为树中没
208、有度为1的结点棵有的结点棵有n个叶子结点的个叶子结点的Huffman树共有树共有2n-1个结点个结点,则可存储在大小为,则可存储在大小为2n-1的的一维数组中。实现编码的结点结构如图一维数组中。实现编码的结点结构如图6-26所示所示。原因原因: 求编码需从叶子结点出发走一条从叶子到根的路求编码需从叶子结点出发走一条从叶子到根的路径;径; 译码需从根结点出发走一条到叶子结点的路径。译码需从根结点出发走一条到叶子结点的路径。 结点类型定义结点类型定义:#define MAX_NODE 200 /* Max_Node2n-1 */ typedef struct unsigned int Weight ; /* 权值域权值域 */unsinged int Parent , Lchild , Rchild ; HTNode ;Weight Parent Lchild RchildWeight:权值域; Parent:双亲结点下标Lchild, Rchild:分别标识左、右子树的下标图图6-26 Huffman编码的结点结构编码的结点结构(2) Huffman树的生成树的生成算法实现算法实现voi
209、d Create_Huffman(unsigned n, HTNode HT , unsigned m) /* 创建一棵叶子结点数为创建一棵叶子结点数为n的的Huffman树树 */ unsigned int w ; int k , j ;for (k=1 ; km ; k+) if (k=n) printf(“n Please Input Weight : w=?”); scanf(“%d”, &w) ;HTk.weight=w ; /* 输入时输入时,所有叶子结点都有权值所有叶子结点都有权值 */else HTk.weight=0; /* 非叶子结点没有权值非叶子结点没有权值 */HTk.Parent=HTk.Lchild=HTk.Rchild=0 ; /* 初始化向量初始化向量HT */for (k=n+1; km ; k+) unsigned w1=32767 , w2=w1 ; /* w1 , w2分别保存权值最小的两个权值分别保存权值最小的两个权值 */ int p1=0 , p2=0 ; /* p1 , p2保存两个最小权值的下标保存两个最小权值的下标 */for (j=
210、1 ; j=k-1 ; j+) if (HTk.Parent=0) /* 尚未合并尚未合并 */ if (HTj.Weightw1) w2=w1 ; p2=p1 ; w1=HTj.Weight ; p1=j ; else if (HTj.Weightw2) w2=HTj.Weight ; p2=j ; /* 找到权值最小的两个值及其下标找到权值最小的两个值及其下标 */HTk.Lchild=p1 ; HTk.Rchild=p2 ;HTk.weight=w1+w2 ;HTp1.Parent=k ; HTp2.Parent=k ; 说明说明:生成生成Huffman树后,树的根结点的下标是树后,树的根结点的下标是2n-1 ,即,即m-1 。(3) Huffman编码算法编码算法 根据出现频度根据出现频度( (权值权值) )Weight,对叶子结点的对叶子结点的Huffman编码有两种方式编码有两种方式: 从叶子结点到根逆向处理,求得每个叶子结点对从叶子结点到根逆向处理,求得每个叶子结点对应字符的应字符的Huffman编码。编码。 从根结点开始遍历整棵二叉树,求得每个叶子结从根结点开始遍历整棵
211、二叉树,求得每个叶子结点对应字符的点对应字符的Huffman编码。编码。 由由Huffman树的生成知,树的生成知,n个叶子结点的树共有个叶子结点的树共有2n-1个结点,叶子结点存储在数组个结点,叶子结点存储在数组HT中的下标值为中的下标值为1n。 编码是叶子结点的编码,只需对数组编码是叶子结点的编码,只需对数组HT1n的的n个权值进行编码;个权值进行编码; 每个字符的编码不同,但编码的最大长度是每个字符的编码不同,但编码的最大长度是n。 求编码时先设一个通用的指向字符的指针变量,求求编码时先设一个通用的指向字符的指针变量,求得编码后再复制。得编码后再复制。算法实现算法实现void Huff_coding(unsigned n , Hnode HT , unsigned m) /* m应为应为n+1,编码的最大长度编码的最大长度n加加1 */ int k , sp ,fp ;char *cd , *HCm ;cd=(char *)malloc(m*sizeof(char) ;/* 动态分配求编码的工作空间动态分配求编码的工作空间 */cdn=0 /* 编码的结束标志编码的结束标志 */
212、for (k=1 ; kn+1 ; k+) /* 逐个求字符的编码逐个求字符的编码 */ sp=n ; p=k ; fp=HTk.parent ; for ( ; fp!=0 ;p=fp , fp=HTp.parent) /* 从叶子结点到根逆向求编码从叶子结点到根逆向求编码 */ if (HTfp.parent=p) cd-sp=0 ; else cd-sp=1 ;HCk=(char *)malloc(n-sp)*sizeof(char) ; /* 为第为第k个字符分配保存编码的空间个字符分配保存编码的空间 */trcpy(HCk , &cdsp) ;free(cd) ;习习 题题 六六 假设在树中,假设在树中, 结点结点x是结点是结点y的双亲时,用的双亲时,用(x,y)来来表示树边。已知一棵树的树边集合为表示树边。已知一棵树的树边集合为 (e,i), (b,e), (b,d), (a,b), (g,j), (c,g), (c,f), (h,l), (c,h), (a,c) ,用树型表示法,用树型表示法表示该树,并回答下列问题:表示该树,并回答下列问题: 哪个是根结点哪个是根结点?
213、哪些是叶子结点哪些是叶子结点? 哪个是哪个是g的双的双亲亲? 哪些是哪些是g的祖先的祖先? 哪些是哪些是g的孩子的孩子? 那些是那些是e的子的子孙孙? 哪些是哪些是e的兄弟的兄弟? 哪些是哪些是f的兄弟的兄弟? b和和n的层次各是多少的层次各是多少? 树的深度是多少树的深度是多少? 以结以结点点c为根的子树的深度是多少为根的子树的深度是多少? 一棵深度为一棵深度为h的满的满k叉树有如下性质:叉树有如下性质: 第第h层上的层上的结点都是叶子结点,其余各层上每个结点都有结点都是叶子结点,其余各层上每个结点都有k棵非空棵非空子树。子树。 如果按层次顺序如果按层次顺序(同层自左至右同层自左至右)从从1开始对全部开始对全部结点编号,问:结点编号,问: 各层的结点数是多少各层的结点数是多少? ? 编号为编号为i的结点的双亲结点的结点的双亲结点(若存在若存在)的编号是多的编号是多少少? 编号为编号为i的结点的第的结点的第j个孩子结点个孩子结点(若存在若存在)的编号的编号是多少是多少? 编号为编号为i的结点的有右兄弟的条件是什么的结点的有右兄弟的条件是什么? 其右其右兄弟的编号是多少兄弟的编号是多少?
214、 设有如图设有如图6-27所示的二叉树。所示的二叉树。 分别用顺序存储方法和链接存储方法画出该二分别用顺序存储方法和链接存储方法画出该二叉树的存储结构。叉树的存储结构。 写出该二叉树的先序、中序、后序遍历序列。写出该二叉树的先序、中序、后序遍历序列。 已知一棵二叉树的先序遍历序列和中序遍历序列已知一棵二叉树的先序遍历序列和中序遍历序列分别为分别为ABDGHCEFI和和GDHBAECIF,请画出这棵二叉,请画出这棵二叉树,然后给出该树的后序遍历序列。树,然后给出该树的后序遍历序列。 设一棵二叉树的中序遍历序列设一棵二叉树的中序遍历序列和后序遍历序列分别为和后序遍历序列分别为BDCEAFHG和和DECBHGFA ,请画出这棵二叉树,请画出这棵二叉树,然后给出该树的先序序列。然后给出该树的先序序列。图图6-27 二叉树二叉树adebfgchkmn 已知一棵二叉树的中序遍历序列和后序遍历序列已知一棵二叉树的中序遍历序列和后序遍历序列分别为分别为dgbaekchif和和gdbkeihfca,请画出这棵二叉树对,请画出这棵二叉树对应的中序线索树和后序线索树。应的中序线索树和后序线索树。 以二叉链表
215、为存储结构,请分别写出求二叉树的以二叉链表为存储结构,请分别写出求二叉树的结点总数及叶子结点总数的算法。结点总数及叶子结点总数的算法。 设设图图6-27所示的二叉树是森林所示的二叉树是森林F所对应的二叉树,所对应的二叉树,请画出森林请画出森林F。 设有一棵树,如图设有一棵树,如图6-28所示。所示。 请分别用双亲表示法请分别用双亲表示法、孩子表示法孩子表示法、孩子兄弟孩子兄弟表示法给出该树的存储结构。表示法给出该树的存储结构。 请给出该树的先序遍历序列和后序遍历序列。请给出该树的先序遍历序列和后序遍历序列。 请将这棵树转换成二叉树。请将这棵树转换成二叉树。 设给定权值集合设给定权值集合w=3,5,7,8,11,12 ,请构造关于,请构造关于w的一棵的一棵huffman树,并求其加权路径长度树,并求其加权路径长度WPL 。 假设用于通信的电文是由字符集假设用于通信的电文是由字符集a, b, c, d, e, f, g, h中的字符构成中的字符构成, 这这8个字符在电文中出现的概率分别个字符在电文中出现的概率分别为为0.07, 0.19, 0.02, 0.06, 0.32, 0.03, 0
216、.21, 0.10 。 请画出对应的请画出对应的huffman树树( (按左子树根结点的权按左子树根结点的权小于等于右子树根结点的权的次序构造小于等于右子树根结点的权的次序构造) )。 求出每个字符的求出每个字符的huffman编码。编码。adebfgmh kcn图图6-28 一般的树一般的树第第7章章 图图 图图(Graph)是一种比线性表和树更为复杂的数据结是一种比线性表和树更为复杂的数据结构。构。 线性结构线性结构:是研究数据元素之间的一对一关系。是研究数据元素之间的一对一关系。在这种结构中,除第一个和最后一个元素外,任何一个在这种结构中,除第一个和最后一个元素外,任何一个元素都有唯一的一个直接前驱和直接后继。元素都有唯一的一个直接前驱和直接后继。 树结构树结构:是研究数据元素之间的一对多的关系。是研究数据元素之间的一对多的关系。在这种结构中,每个元素对下在这种结构中,每个元素对下(层层)可以有可以有0个或多个元个或多个元素相联系,对上素相联系,对上(层层)只有唯一的一个元素相关,数据元只有唯一的一个元素相关,数据元素之间有明显的层次关系。素之间有明显的层次关系。 图结构图结构:
217、是研究数据元素之间的多对多的关系。是研究数据元素之间的多对多的关系。在这种结构中,任意两个元素之间可能存在关系。即结在这种结构中,任意两个元素之间可能存在关系。即结点之间的关系可以是任意的,图中任意元素之间都可能点之间的关系可以是任意的,图中任意元素之间都可能相关。相关。 图的应用极为广泛,已渗入到诸如语言学、逻辑学、图的应用极为广泛,已渗入到诸如语言学、逻辑学、物理、化学、电讯、计算机科学以及数学的其它分支。物理、化学、电讯、计算机科学以及数学的其它分支。7.1 图的基本概念图的基本概念7.1.1 图的定义和术语图的定义和术语 一个图一个图(G)定义为一个偶对定义为一个偶对(V,E) ,记为,记为G=(V,E) 。其中:其中: V是是顶点顶点(Vertex)的非空有限集合,记为的非空有限集合,记为V(G);E是无序集是无序集V&V的一个子集,记为的一个子集,记为E(G) ,其元素是图的,其元素是图的弧弧(Arc)。 将顶点集合为空的图称为空图。其形式化定义为:将顶点集合为空的图称为空图。其形式化定义为:G=(V ,E)V=v|v data objectE=| v,w Vp(v,w)P
218、(v,w)表示从顶点表示从顶点v到顶点到顶点w有一条直接通路。有一条直接通路。 弧弧(Arc) :表示两个顶点表示两个顶点v和和w之间存在一个关系,之间存在一个关系,用顶点偶对用顶点偶对表示。通常根据图的顶点偶对将图分表示。通常根据图的顶点偶对将图分为有向图和无向图。为有向图和无向图。 有向图有向图(Digraph): 若图若图G的关系集合的关系集合E(G)中,顶中,顶点偶对点偶对的的v和和w之间是之间是有序有序的,称图的,称图G是有向图。是有向图。 在有向图中,若在有向图中,若 E(G) ,表示从顶点,表示从顶点v到顶点到顶点w有一条有一条弧弧。 其中:其中:v称为称为弧尾弧尾(tail)或或始点始点(initial node),w称为称为弧头弧头(head)或或终点终点(terminal node) 。 无向图无向图(Undigraph): 若图若图G的关系集合的关系集合E(G)中,中,顶点偶对顶点偶对的的v和和w之间是之间是无序无序的,称图的,称图G是无向图。是无向图。 在无向图中,若在无向图中,若 E(G) ,有,有 E(G) ,即即E(G)是对称,则用无序对是对称,则用无序对
219、(v,w) 表示表示v和和w之间的一条之间的一条边边(Edge),因此,因此(v,w) 和和(w,v)代表的是同一条边。代表的是同一条边。例例1:设有有向图:设有有向图G1和无向图和无向图G2,形式化定义分别是:,形式化定义分别是:G1=(V1 ,E1)V1=a,b,c,d,eE1=, , ,G2=(V2 ,E2)V2=a,b,c,dE2=(a,b), (a,c), (a,d), (b,d), (b,c), (c,d)它们所对应的图如图它们所对应的图如图7-1所示。所示。 完全无向图完全无向图:对于无向图,若图中顶点数为对于无向图,若图中顶点数为n ,用用e表示边的数目,则表示边的数目,则e 0,n(n-1)/2 。具有。具有n(n-1)/2条条边的无向图称为完全无向图。边的无向图称为完全无向图。完全无向图另外的定义是:完全无向图另外的定义是: 对于无向图对于无向图G=(V,E),若,若 vi,vj V ,当,当vivj时,时,有有(vi ,vj) E,即,即图中任意两个不同的顶点间都有一条无图中任意两个不同的顶点间都有一条无向边向边,这样的无向图称为,这样的无向图称为完全无向图完全无
220、向图。abcd(b) 无向图无向图G2 图图7-1 图的示例图的示例(a) 有向图有向图G1 acbde 完全有向图完全有向图:对于有向图,若图中顶点数为对于有向图,若图中顶点数为n ,用用e表示弧的数目,则表示弧的数目,则e 0,n(n-1) 。具有。具有n(n-1)条边条边的有向图称为完全有向图。的有向图称为完全有向图。完全有向图另外的定义是:完全有向图另外的定义是: 对于有向图对于有向图G=(V,E),若,若 vi,vj V ,当,当vi vj时,时,有有 E E ,即,即图中任意两个不同的顶图中任意两个不同的顶点间都有一条弧点间都有一条弧,这样的有向图称为,这样的有向图称为完全有向图完全有向图。 有很少边或弧的图(有很少边或弧的图(enn)的图称为)的图称为稀疏图稀疏图,反,反之称为之称为稠密图稠密图。权权(Weight):与图的边和弧相关的数。权可以表示从与图的边和弧相关的数。权可以表示从一个顶点到另一个顶点的距离或耗费。一个顶点到另一个顶点的距离或耗费。 子图和生成子图子图和生成子图:设有图设有图G=(V,E)和和G=(V,E),若,若V V且且E E ,则称图,则称图G是
221、是G的的子图子图;若;若V=V且且E E,则称图,则称图G是是G的一个的一个生成子图生成子图。 顶点的邻接顶点的邻接(Adjacent):对于无向图对于无向图G=(V,E),若边,若边(v,w) E,则称顶点,则称顶点v和和w 互为互为邻接点邻接点,即,即v和和w相邻接。边相邻接。边(v,w)依附依附(incident)与顶点与顶点v和和w 。 对于有向图对于有向图G=(V ,E),若有向弧,若有向弧 E,则称,则称顶点顶点v “邻接到邻接到”顶点顶点w,顶点,顶点w “邻接自邻接自”顶点顶点v ,弧,弧 与顶点与顶点v和和w “相关联相关联” 。 顶点的度、入度、出度顶点的度、入度、出度:对于无向图对于无向图G=(V,E), vi V,图,图G中中依附依附于于vi的边的数目称为顶点的边的数目称为顶点vi的的度度(degree),记为,记为TD(vi)。 显然,在无向图中,所有顶点度的和是图中边的显然,在无向图中,所有顶点度的和是图中边的2倍。倍。 即即 TD(vi)=2e i=1, 2, , n ,e为图的边数。为图的边数。 对有向图对有向图G=(V,E),若,若 vi V ,图,图
222、G中中以以vi作为起作为起点点的有向边的有向边(弧弧)的数目称为顶点的数目称为顶点vi的的出度出度(Outdegree),记为记为OD(vi) ;以以vi作为终点作为终点的有向边的有向边(弧弧)的数目称为顶的数目称为顶点点vi的的入度入度(Indegree),记为,记为ID(vi) 。顶点。顶点vi的的出度出度与与入入度度之和称为之和称为vi的的度度,记为,记为TD(vi) 。即。即TD(vi)=OD(vi)+ID(vi) 路径路径(Path)、路径长度、回路、路径长度、回路(Cycle) :对无对无向图向图G=(V,E),若从顶点,若从顶点vi经过若干条边能到达经过若干条边能到达vj,称,称顶点顶点vi和和vj是是连通连通的,又称顶点的,又称顶点vi到到vj有有路径路径。 对有向图对有向图G=(V,E),从顶点,从顶点vi到到vj有有有向路径有向路径,指,指的是从顶点的是从顶点vi经过若干条有向边经过若干条有向边(弧弧)能到达能到达vj。 或或路径路径是图是图G中连接两顶点之间所经过的顶点序列。中连接两顶点之间所经过的顶点序列。即即 Path=vi0vi1vim ,vij V且且(v
223、ij-1, vij) E j=1,2, ,m或或 Path=vi0vi1 vim ,vij V且且 E j=1,2, ,m 路径上边或有向边路径上边或有向边(弧弧)的数目称为该的数目称为该路径路径的的长度长度。 在一条路径中,若在一条路径中,若没有重复相同没有重复相同的顶点,该路径称的顶点,该路径称为为简单路径简单路径;第一个顶点和最后一个顶点相同的路径称;第一个顶点和最后一个顶点相同的路径称为为回路回路(环环);在一个回路中,若除第一个与最后一个顶;在一个回路中,若除第一个与最后一个顶点外,其余顶点不重复出现的回路称为点外,其余顶点不重复出现的回路称为简单回路简单回路(简单环简单环)。 连通图、图的连通分量连通图、图的连通分量:对无向图对无向图G=(V,E),若若 vi ,vj V,vi和和vj都是连通的,则称图都是连通的,则称图G是是连通图连通图,否则称为否则称为非连通图非连通图。若。若G是非连通图,则是非连通图,则极大的连通子极大的连通子图图称为称为G的的连通分量连通分量。 对有向图对有向图G=(V,E),若,若 vi ,vj V,都有,都有以以vi为起为起点点, vj 为终点为
224、终点以及以以及以vj为起点,为起点,vi为终点的有向路径,为终点的有向路径,称图称图G是是强连通图强连通图,否则称为,否则称为非强连通图非强连通图。若。若G是非强是非强连通图,则连通图,则极大的强连通子图极大的强连通子图称为称为G的的强连通分量强连通分量。 “极大极大”的含义:指的是对子图再增加图的含义:指的是对子图再增加图G中的其它中的其它顶点,子图就不再连通。顶点,子图就不再连通。 生成树、生成森林生成树、生成森林:一个连通图一个连通图(无向图无向图)的生成的生成树是一个极小连通子图,它树是一个极小连通子图,它含有图中全部含有图中全部n个顶点个顶点和只和只有足以构成一棵树的有足以构成一棵树的n-1条边条边,称为图的,称为图的生成树生成树,如图,如图7-2所示。所示。 关于无向图的生成树的几个结论:关于无向图的生成树的几个结论: 一棵有一棵有n个顶点的生成树有且仅有个顶点的生成树有且仅有n-1条边;条边; 如果一个图有如果一个图有n个顶点和小于个顶点和小于n-1条边,则是非连条边,则是非连通图;通图;adbc图图7-2 图图G2的一棵生成树的一棵生成树 如果多于如果多于n-1条边,则
225、一定条边,则一定有环;有环; 有有n-1条边的图不一定是生条边的图不一定是生成树。成树。 有向图的有向图的生成森林生成森林是这样一个子图,由若干棵是这样一个子图,由若干棵有向有向树树组成,含有图中全部顶点。组成,含有图中全部顶点。有向树有向树是只有一个顶点的入度为是只有一个顶点的入度为0 ,其余顶点的入度均,其余顶点的入度均为为1的有向图,如图的有向图,如图7-3所示。所示。 网网:每个边每个边(或弧或弧)都附加一个权值的图,称为都附加一个权值的图,称为带权带权图图。带权的连通图带权的连通图(包括弱连通的有向图包括弱连通的有向图)称为称为网或网络网或网络。网络是工程上常用的一个概念,用来表示一个工程或某网络是工程上常用的一个概念,用来表示一个工程或某种流程,如图种流程,如图7-4所示。所示。图图7-3 有向图及其生成森林有向图及其生成森林abcdedce(a) 有向图有向图(b) 生成森林生成森林acbcb354126abcde3图图7-4 带权有向图带权有向图7.1.2 图的抽象数据类型定义图的抽象数据类型定义 图是一种数据结构图是一种数据结构,加上一组基本操作就构成了图加上一组基本
226、操作就构成了图的抽象数据类型的抽象数据类型。图的抽象数据类型定义如下:图的抽象数据类型定义如下:ADT Graph数据对象数据对象V:具有相同特性的数据元素的集合:具有相同特性的数据元素的集合,称为称为顶点集顶点集。数据关系数据关系R:R=VRVR=| v,w Vp(v,w) ,表示表示 从从v到到w的弧,的弧,P(v,w)定义了弧定义了弧的信息的信息 基本操作基本操作P: Create_Graph() : 图的创建操作图的创建操作。初始条件:无。初始条件:无。 操作结果:生成一个没有顶点的空图操作结果:生成一个没有顶点的空图G。GetVex(G, v) : 求图中的顶点求图中的顶点v的值。的值。初始条件:图初始条件:图G存在,存在,v是图中的一个顶点。是图中的一个顶点。操作结果:生成一个没有顶点的空图操作结果:生成一个没有顶点的空图G。 DFStraver(G,V):从:从v出发对图出发对图G深度优先遍历。深度优先遍历。 初始条件:图初始条件:图G存在。存在。 操作结果:对图操作结果:对图G深度优先遍历,每个顶点访问深度优先遍历,每个顶点访问且只访问一次。且只访问一次。 BFStra
227、ver(G,V):从:从v出发对图出发对图G广度优先遍历。广度优先遍历。 初始条件:图初始条件:图G存在。存在。 操作结果:对图操作结果:对图G广度优先遍历,每个顶点访问广度优先遍历,每个顶点访问且只访问一次。且只访问一次。 ADT Graph 详见详见p156157。7.2 图的存储结构图的存储结构 图的存储结构比较复杂,其复杂性主要表现在:图的存储结构比较复杂,其复杂性主要表现在: 任意顶点之间可能存在联系,无法以数据元素任意顶点之间可能存在联系,无法以数据元素在存储区中的物理位置来表示元素之间的关系。在存储区中的物理位置来表示元素之间的关系。 图中顶点的度不一样,有的可能相差很大,若图中顶点的度不一样,有的可能相差很大,若按度数最大的顶点设计结构,则会浪费很多存储单按度数最大的顶点设计结构,则会浪费很多存储单元,反之按每个顶点自己的度设计不同的结构,又元,反之按每个顶点自己的度设计不同的结构,又会影响操作。会影响操作。 图的常用的存储结构有:图的常用的存储结构有:邻接矩阵邻接矩阵、邻接链表邻接链表、十十字链表字链表、邻接多重表邻接多重表和和边表边表。7.2.1 邻接矩阵邻接矩阵(
228、数组数组)表示法表示法 基本思想基本思想:对于有对于有n个顶点的图,用一维数组个顶点的图,用一维数组vexsn存储顶点信息,用二维数组存储顶点信息,用二维数组Ann存储顶点之间存储顶点之间关系的信息。该二维数组称为关系的信息。该二维数组称为邻接矩阵邻接矩阵。在邻接矩阵中,。在邻接矩阵中,以顶点在以顶点在vexs数组中的下标代表顶点,邻接矩阵中的元数组中的下标代表顶点,邻接矩阵中的元素素Aij存放的是顶点存放的是顶点i到顶点到顶点j之间关系的信息。之间关系的信息。1 无向图的数组表示无向图的数组表示(1) 无权图的邻接矩阵无权图的邻接矩阵 无向无权图无向无权图G=(V,E)有有n(n1)个顶点,其邻接矩个顶点,其邻接矩阵是阵是n阶对称方阵,如图阶对称方阵,如图7-5所示。其元素的定义如下:所示。其元素的定义如下:1 若若(vi , vj) E,即,即vi , vj邻接邻接0 若若(vi , vj) E,即,即vi , vj不邻接不邻接Aij=(a) 无向图无向图 abcd图图7-5 无向无权图的数组存储无向无权图的数组存储(b) 顶点矩阵顶点矩阵vexsabcd0 1 1 11 0 1
229、11 1 0 11 1 1 0(c) 邻接矩阵邻接矩阵(2) 带权图的邻接矩阵带权图的邻接矩阵 无向带权图无向带权图G=(V,E) 的邻接矩阵如图的邻接矩阵如图7-6所示。其所示。其元素的定义如下:元素的定义如下:(a) 带权无向图带权无向图 (b) 顶点矩阵顶点矩阵图图7-6 无向带权图的数组存储无向带权图的数组存储(c) 邻接矩阵邻接矩阵354126abcde3vexsabcde 6 2 6 3 4 32 3 1 4 3 5 3 5 Wij 若若(vi , vj) E,即,即vi , vj邻接,权值为邻接,权值为wij 若若(vi , vj) E,即,即vi , vj不邻接时不邻接时Aij=(3) 无向图邻接矩阵的特性无向图邻接矩阵的特性 邻接矩阵是邻接矩阵是对称方阵对称方阵; 对于顶点对于顶点vi,其,其度数度数是第是第i行的非行的非0元素的个数;元素的个数; 无向图的无向图的边数边数是上是上(或下或下)三角形矩阵中非三角形矩阵中非0元素元素个数。个数。2 有向图的数组表示有向图的数组表示(1) 无权图的邻接矩阵无权图的邻接矩阵 若有向无权图若有向无权图G=(V,E)有有n(n1
230、)个顶点,则其邻个顶点,则其邻接矩阵是接矩阵是n阶对称方阵阶对称方阵,如图,如图7-7所示。元素定义如下:所示。元素定义如下:1 若若 E,从,从vi到到vj有弧有弧Aij=0 若若 E 从从vi到到vj 没有弧没有弧(a) 有向图有向图acbde图图7-7 有向无权图的数组存储有向无权图的数组存储(b) 顶点矩阵顶点矩阵vexsabcde(c) 邻接矩阵邻接矩阵0 1 1 0 10 0 0 0 00 0 0 1 11 1 0 0 00 0 0 1 0(2) 带权图的邻接矩阵带权图的邻接矩阵 有向带权图有向带权图G=(V,E)的邻接矩阵如图的邻接矩阵如图7-8所示。其所示。其元素的定义如下:元素的定义如下:wij 若若 E,即,即vi , vj邻接,权值为邻接,权值为wij 若若 E,即,即vi , vj不邻接时不邻接时Aij=图图7-8 带权有向图的数组存储带权有向图的数组存储(b) 顶点矩阵顶点矩阵vexsabcde(c) 邻接矩阵邻接矩阵 6 2 3 3 1 4 5 (a) 带权有向图带权有向图 354126abcde3 有向图邻接矩阵的特性有向图邻接矩阵的特性 对于顶点对于顶点
231、vi,第,第i行的非行的非0元素的个数是其元素的个数是其出度出度OD(vi);第;第i列的非列的非0元素的个数是其元素的个数是其入度入度ID(vi) 。 邻接矩阵中非邻接矩阵中非0元素的个数就是图的弧的数目。元素的个数就是图的弧的数目。3 图的邻接矩阵的操作图的邻接矩阵的操作 图的邻接矩阵的实现比较容易,定义两个数组分别图的邻接矩阵的实现比较容易,定义两个数组分别存储存储顶点信息顶点信息(数据元素数据元素)和和边或弧的信息边或弧的信息(数据元素之间数据元素之间的关系的关系) 。其。其存储结构形式定义存储结构形式定义如下:如下:#define INFINITY MAX_VAL /* 最大值最大值 */ /* 根据图的权值类型,分别定义为最大整数或实数根据图的权值类型,分别定义为最大整数或实数 */#define MAX_VEX 30 /* 最大顶点数目最大顶点数目 */typedef enum DG, AG, WDG,WAG GraphKind ; /* 有向图,无向图,带权有向图,带权无向图有向图,无向图,带权有向图,带权无向图 */typedef struct ArcType Vex
232、Type vex1, vex2 ; /* 弧或边所依附的两个顶点弧或边所依附的两个顶点 */ArcValType ArcVal ; /* 弧或边的权值弧或边的权值 */ArcInfoType ArcInfo ; /* 弧或边的其它信息弧或边的其它信息 */ArcType ; /* 弧或边的结构定义弧或边的结构定义 */typedef struct GraphKind kind ; /* 图的种类标志图的种类标志 */int vexnum , arcnum ; /* 图的当前顶点数和弧数图的当前顶点数和弧数 */VexType vexsMAX_VEX ; /* 顶点向量顶点向量 */AdjType adjMAX_VEXMAX_VEX;MGraph ; /* 图的结构定义图的结构定义 */ 利用上述定义的数据结构,可以方便地实现图的各利用上述定义的数据结构,可以方便地实现图的各种操作。种操作。(1) 图的创建图的创建AdjGraph *Create_Graph(MGraph * G) printf(“请输入图的种类标志:请输入图的种类标志:”) ;scanf(“%d”, &G-kind)
233、;G-vexnum=0 ; /* 初始化顶点个数初始化顶点个数 */return(G) ; (2) 图的顶点定位图的顶点定位 图的顶点定位操作实际上是确定一个顶点在图的顶点定位操作实际上是确定一个顶点在vexs数数组中的位置组中的位置(下标下标) ,其过程完全等同于在顺序存储的线,其过程完全等同于在顺序存储的线性表中查找一个数据元素。性表中查找一个数据元素。算法实现算法实现:int LocateVex(MGraph *G , VexType *vp) int k ; for (k=0 ; kvexnum ; k+) if (G-vexsk=*vp) return(k) ; return(-1) ; /* 图中无此顶点图中无此顶点 */ (3) 向图中增加顶点向图中增加顶点 向图中增加一个顶点的操作,类似在顺序存储的线向图中增加一个顶点的操作,类似在顺序存储的线性表的末尾增加一个数据元素。性表的末尾增加一个数据元素。算法实现算法实现:int AddVertex(MGraph *G , VexType *vp) int k , j ;if (G-vexnum=MAX_VEX) printf
234、(“Vertex Overflow !n”) ; return(-1) ; if (LocateVex(G , vp)!=-1) printf(“Vertex has existed !n”) ; return(-1) ; k=G-vexnum ; G-vexsG-vexnum+=*vp ;if (G-kind=DG|G-kind=AG) for (j=0 ; jvexnum ; j+)G-adjjk.ArcVal=G-adjkj.ArcVal=0 ; /* 是不带权的有向图或无向图是不带权的有向图或无向图 */elsefor (j=0 ; jvexnum ; j+) G-adjjk.ArcVal=INFINITY ; G-adjkj.ArcVal=INFINITY ; /* 是带权的有向图或无向图是带权的有向图或无向图 */return(k) ;(4) 向图中增加一条弧向图中增加一条弧 根据给定的弧或边所依附的顶点,修改邻接矩阵中根据给定的弧或边所依附的顶点,修改邻接矩阵中所对应的数组元素。所对应的数组元素。算法实现算法实现: int AddArc(MGraph *G , ArcTy
235、pe *arc) int k , j ;k=LocateVex(G , &arc-vex1) ;j=LocateVex(G , &arc-vex1) ;if (k=-1|j=-1) printf(“Arcs Vertex do not existed !n”) ;return(-1) ;if (G-kind=DG|G-kind=WDG) G-adjkj.ArcVal=arc-ArcVal;G-adjkj.ArcInfo=arc-ArcInfo ; /* 是有向图或带权的有向图是有向图或带权的有向图*/else G-adjkj.ArcVal=arc-ArcVal ;G-adjjk.ArcVal=arc-ArcVal ;G-adjkj.ArcInfo=arc-ArcInfo ;G-adjjk.ArcInfo=arc-ArcInfo ; /* 是无向图或带权的无向图是无向图或带权的无向图,需对称赋值需对称赋值 */return(1) ; 7.2.2 邻接链表法邻接链表法 基本思想:基本思想:对图的每个顶点建立一个单链表,存储对图的每个顶点建立一个单链表,存储该顶点所有邻接顶点及其相关信息。每
236、一个单链表设一该顶点所有邻接顶点及其相关信息。每一个单链表设一个表头结点。个表头结点。 第第i个单链表表示依附于顶点个单链表表示依附于顶点Vi的边的边(对有向图是以对有向图是以顶点顶点Vi为头或尾的弧为头或尾的弧)。1 结点结构与邻接链表示例结点结构与邻接链表示例 链表中的结点称为链表中的结点称为表结点表结点,每个结点由三个域组成,每个结点由三个域组成,如图如图7-9(a)所示。其中邻接点域所示。其中邻接点域(adjvex)指示与顶点指示与顶点Vi邻邻接的顶点在图中的位置接的顶点在图中的位置(顶点编号顶点编号),链域,链域(nextarc)指向指向下一个与顶点下一个与顶点Vi邻接的表结点,数据域邻接的表结点,数据域(info)存储和边或存储和边或弧相关的信息,如权值等。对于无权图,如果没有与边弧相关的信息,如权值等。对于无权图,如果没有与边相关的其他信息,可省略此域。相关的其他信息,可省略此域。 每个链表设一个表头结点每个链表设一个表头结点(称为称为顶点结点顶点结点),由两个,由两个域组成,如图域组成,如图7-9(b)所示。链域所示。链域(firstarc)指向链表中的指向链表中的第一
237、个结点,数据域第一个结点,数据域(data) 存储顶点名或其他信息。存储顶点名或其他信息。adjvex info nextarc表结点表结点:data firstarc顶点结点顶点结点:图图7-9 邻接链表结点结构邻接链表结点结构 在图的邻接链表表示中,所有在图的邻接链表表示中,所有顶点结点顶点结点用一个向量用一个向量 以顺序结构形式存储,可以随机访问任意顶点的链表,以顺序结构形式存储,可以随机访问任意顶点的链表,该向量称为该向量称为表头向量表头向量,向量的下标指示顶点的序号。,向量的下标指示顶点的序号。 用邻接链表存储图时,对无向图,其邻接链表是唯用邻接链表存储图时,对无向图,其邻接链表是唯一的,如图一的,如图7-10所示;对有向图,其邻接链表有两种形所示;对有向图,其邻接链表有两种形式,如图式,如图7-11所示。所示。图图7-10 无向图及其邻接链表无向图及其邻接链表v1v2v3v4v501234MAX_VEX-1v1 v2v3 v4 v5 213 02 0314 204 23 (a) 有向图有向图v1v2v3v4v513 014 2 3 01234MAX_VEX-1v1 2 v2
238、 0 v3 3v4 1 v5 1 (b) 正邻接链表,出度直观正邻接链表,出度直观2 02 2 01234MAX_VEX-1v1 1v2 2v3 1v4 2 v5 1 3 04 (c) 逆邻接链表,入度直观逆邻接链表,入度直观图图7-11 有向图及其邻接链表有向图及其邻接链表2 邻接表法的特点邻接表法的特点 表头向量中每个分量就是一个单链表的头结点,表头向量中每个分量就是一个单链表的头结点,分量个数就是图中的顶点数目;分量个数就是图中的顶点数目; 在边或弧稀疏的条件下,用邻接表表示比用邻接在边或弧稀疏的条件下,用邻接表表示比用邻接矩阵表示节省存储空间;矩阵表示节省存储空间; 在无向图,顶点在无向图,顶点Vi的度是第的度是第i个链表的结点数;个链表的结点数; 对对有向图有向图可以建立可以建立正邻接表正邻接表或或逆邻接表逆邻接表。正邻接。正邻接表是以顶点表是以顶点Vi为出度为出度(即为弧的起点即为弧的起点)而建立的邻接而建立的邻接表;逆邻接表是以顶点表;逆邻接表是以顶点Vi为入度为入度(即为弧的终点即为弧的终点)而而建立的邻接表;建立的邻接表; 在有向图中,第在有向图中,第i个链表中的结点
《数据结构严蔚敏.ppt》由会员工****分享,可在线阅读,更多相关《数据结构严蔚敏.ppt》请在金锄头文库上搜索。