一.绪论
数据结构主要研究非数值性程序设计中所出现的计算机操作对象以及他们之间的关系和运算等
术语
数据(Data):信息 对在计算机科学中指所有能输入到计算机中并被计算机程序处理的符号的总称;
例如 在结客观事物的符号表示,构体中,结构体中的所描绘的内容都是数据
数据元素(Data Element):数据的基本单位
数据项(Data Item):是有独立含义的最小单位 数据项构成数据元素
数据元素可以包含一个或者几个数据项,例如数据库表里边的某一列,就是一个数据项,每一行就是一个数据元素
数据的基本单位,在计算程序中通常作为一个整体进行考虑和处理,有时,一个数据元素可由若干个数 据项(data item)组成; 如:一本书的数目信息为一个数据元素, 而书目信息的每一项(书名、作者名等) 为一个数据项, 数据项是数据不可分割的最小单位. 在数组中int a[10]里面有10个元素,结构体整体才是一个数据元素
数据对象(Data Object):元素的集合
是性质相同的数据元素的集合,是数据的一个子集;
如:整数数据对象N = {0, ±1, ±2, ±3, …},字母字符数据对象C = {‘A’, ‘B’, ‘C’, …, ‘Z’}
数据结构(Data Structure):
是相互之间存在的一种或者多种特定关系的数据元素的集合.(通常有四类基本结构)
集合: 结构中的数据元素除了”同属于一个集合”的关系外,别无其他关系;
线性结构: 结构中的数据元素之前存在一对一关系 除第一个元素外,其他元素只有一个前驱,除最后一个元素外 其他元素只有一个后继
树形结构: 结构中的数据元素之间存在一对多关系 即一个元素只有一个前驱 但可以有多个后继
图状或网状结构: 结构中的数据元素存在多对多关系 元素之间的逻辑关系可以是任意的
从上可以看出 数据结构就是带有结构的数据元素的集合
数据结构三要素
(1)逻辑结构: 数据之间关系的描述,虚的
逻辑结构形式上用二元组,B=(K,R),K是结点的集合,R是K上关系的集合,例如<k,k’>代表k是k’前驱,k’是k的后继,为相邻结点 逻辑结构独立于存储结构 而存储结构依赖逻辑结构
(1.1) 线性结构:有且只有一个开始结点和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后 继(1对1),典型的有:线性表、栈、队列、数组、串
(1.2) 非线性结构:每个结点可以有不止一个直接前驱和直接后继(非1对1),典型的树、图、集合
(2) 存储结构: 数据结构在计算机中映射称为存储结构,实的
存储结构是逻辑关系的映象与元素本身的映象,存储结构实质上是内存分配
注:在计算机中存储信息的最小单位叫做位(bit),8位表示一个字节(byte),二个字节称为一个字(word),字节,字或更多得二进制位可称为位串,这个位串称为元素或结点。当数据元素由若干个数据项组成时,则位串中对应于每个数据项的子位串称为数据域
(2.1)顺序存储结构(sequentialstorage structure):把逻辑上相连的结点存储在物理位置上相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现。一般采用数组或者结构体数组来描述。
(2.2)链式存储结构(LinkedStorage Structure):其不要求逻辑上相邻的结点,在物理位置上相邻,结点间的逻辑关系由附加的(指针)引用字段表示。一个结点的引用字段往往指导下一个结点的存放位置。一般采用链表来描述
(2.3)索引存储方式(index):索引存储方式是采用附加的索引表的方式来存储节点信息的一种存储方式。索引表由若干索引项组成。索引存储方式中索引项的一般形式为(关键字、地址)。其中,关键字是能够唯一标识一个节点的数据项。
(2.4)散列存储方式(hash):根据结点的关键字通过散列函数直接计算出该结点的存储地址
(3)数据运算:一些操作,增、删、改、查
数据类型(Data Type):类型
一个值的集合和定义在这个值集上的一组操作的总称(例如,C语言中的整型变量,其值集为某个区间上的整数),定义在其上的操作为加减乘数模运算等算术运算),按”值”的不同特性,高级程序语言中的数据类型可分为两类
非结构的原子类型: 原子类型是不可分解的.例如:C的基本类型(整型,实型,字符型和枚举类型)、指针类型和空类型.
结构类型: 结构类型的值是由若干成为按照某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的,也可以是结构的. 例如:数组的值由若干分量组成,每个分量可以是整数,可以是数组等.在某种意义上,数据结构可以看成是”一组具有相同结构的值”,则数据结构可以看成由一种数据结构和定义在其上的一组操作组成.
抽象数据类型(Abstract Data Type, ADT): 指一个数学模型及定义在该模型上的一组操作.抽象数据类型的定义仅取决于它的一组逻辑特性内,而与其在计算机内部表示和实现无关,即无论其部结构如何变化,只要它的数学特性不变,都不影响其外部的使用.
抽象数据类型 (Abstract Data Type,ADT):模型、类型
一个含抽象数据类型的软件模块通常应包含”定义,表示和实现“3个部分
抽象数据类型的定义由一个值域和定义在该值域的一组操作组成.若按照其值的不同特性,可分为3种类型
原子类型(atomic data type)属原子类型的变量的值是不可分解的.这类抽象数据类型较少,因为一般情况下,已有的固有数据类型足以满足需求,但有时也有必要定义新的原子数据类型,例如整位为100的整数;
结构类型
固定聚合类型(fixed-aggregate data type)属该类型的变量,其值由确定数目的成分按某种结构组成.例如,复数是两个实数依确定的次序关系组成.
可变聚合类型(variable-aggregate date type)和固定聚合类型相比较,构成可变聚合类型”值”的成分的数目是不确定.例如,可定义一个”有序整数序列”的抽象数据类型,其中序列的长度是可变的.固定聚合类型与可变聚合类型可以统称为
多形数据类型(polymorphic data type)是指其值的成分不确定的数据类型,抽象数据类型中,不论其元素具有何种特性,元素之间的关系相同,基本操作也相同,从抽象数据类型的角度看,具有相同的数学抽象特性,故称之为多形数据类型.
二.算法
1.概念
算法+数据结构=程序,说明数据结构和算法是程序的两大要素,二者相辅相成,缺一不可。算法是程序的灵魂。
算法:算法是指解决问题的一种方法或一个过程
程序:程序是算法用某种程序设计语言的具体实现
算法和程序都是用来表达解决问题的逻辑步骤,但算法独立于具体的计算机,与具体的程序设计语言无关,而程序正好相反;程序是算法,但算法不一定是程序。
2.算法的特性
有限性:算法必须在执行有穷步之后结束,而每一步都必须在有穷时间内完成。
确定性:算法中每一步操作的含义都必须是确定的,不能有二义性。
输 入:一个算法可以有零个或多个输入。
输 出:一个算法有一个或多个输出。
可行性:一个算法必须是可行的,即算法中每一操作都能通过已知的一组基本操作来实现。
数据结构的表示(存储结构)用类型定义(typedef)来描述。数据元素类型约定为ElemType
3.时间复杂度
算法执行时间 :一个算法的执行时间大致上等于其所有语句执行时间的总和,对于语句的执行时间是指该条语句的执行次数和执行一次所需时间的乘积。
语句频度:指该语句在一个算法中重复执行的次数,算法中所有语句频度之和 记作T(n),基本运算频度为f(n), T(n)=O(f(n)) 表示随问题规模n的增大,算法的执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度,所以算时间复杂度相当于算频度
时间复杂度就是执行语句被调用了多少次
时间复杂度依赖于问题规模,和数据的初始状态
选取T(n)=O(f(n))中增长速度最快的项,且系数得写1,如果f(n)跟n没有关系时,即频度是个常量,即可以明确表示的数字时,写O(1)
最坏时间复杂度和平均时间复杂度 最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
规则
T(n)=T1(n)+T2(n)=O(f(n)+g(n))=O(max(f(n),g(n))
T(n)=T1(n)*T2(n)=O(f(n)*g(n))=O(f(n)*g(n))
一般计算最深层循环内的语句频度是时间复杂度
常用时间复杂度
常用的时间复杂度:O(1) <O(log2n) < O(n) < O(n log2n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
怎么计算时间复杂度
(1)如果只调用了一次,如:
x=5;
if(x<-4)
{x=x+4;}
else
{x=x+3;}
在大括号中的内容,只会调用一个语句,那么O(n)=1;
(2)如果调用了两次,如:
x=5;
if(x<-4)
{x=x+4;}
else
{x=x+3;}
x=x+56;
在大括号中的内容,只会调用一个语句,但是在最后,还有一个计算公式要调用语句;总共加起来就是调用2次。那么O(n)=2;
(3)用1个FOR循环调用
for(x=0;x<n;x++)
{x=x+1;}
x会从0到n-1循环,执行的语句就是将当前x值加入新的x中,总共调用n次;那么O(n)=n;
(4)用2个嵌套FOR循环调用
for(x=0;x<n;x++)
{
for(y=1;y<=n;y++)
{x=x+y;}
}
遇到嵌套循环,可以先将外面的FOR语句中的变量固定为初始值x=0,主要看里面的FOR语句的时间复杂度,很明显,里面语句执行次数是从1到n总共调用n次,O(n)=n;这还只是x=0时的调用。x可以从0到n-1,共n次。每次调用都会执行n次调用y的情况,因此,执行语句x=x+y;总共会调用n*n次。O(n)=n^2。
数执行语句的执行次数,就是时间复杂度。注意:
(1)找到正确的执行语句。
(2)for循环中的初始值和终止值。
for(i=0;i<n;i++) i值变化是从0到n-1,共n次。
for(i=0;i<=n;i++) i值变化是从0到n,共n+1次。
(3)注意for循环的调用顺序,从里面到外面进行的。
注:访问第i个结点,属于随机 时间复杂度为o(1); 不需要移动的为o(1) 需要移动的为o(n);
4.空间复杂度
关于算法的存储空间需求,类似于算法的时间复杂度, 我们采用空间复杂度作为算法所需存储空间的度量S,记作: S(n)=O(f(n)) ,其中,n为问题的规模,O表示数量级。
对于输入数据所占的具体存储量只取决于问题本身,与算法无关,因此,只需分析算法在实现时所需辅助空间即可。若所需辅助空间相对于输入数据而言是个常数,则称该算法为原地工作,辅助空间为O(1)
三.线性表
1.定义
线性表(Linear_List)简称为表:n(n≥0)个具有相同类型的数据元素的有限序列 其中n为表长,当n=0 时该线性表是一个空表。若用L命名线性表,则其一般表示如下:
L=(a1, a2, ..., ai, ai+1, ..., an)
线性表是一种最简单,最基本,也是最常用的一种线性结构(线性结构的特点是数据元素之间存在一种线性关系)。它有顺序结构存储和链式结构存储,它的主要基本操作有插入、删除和查找等。在一个线性表中,数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。
线性表的长度: 线性表中数据元素的个数
空表 :长度等于零的线性表
除第一个元素外,其他每一个元素有且仅有一个直接前驱。
除最后一个元素外,其他每一个元素有且仅有一个直接后继。
2.特点
同一性:线性表由同类数据元素组成,每一个ai必须属于同一数据对象
有穷性:线性表由有限个数据元素组成, 表长度就是表中数据元素的个数
有序性:线性表中表中相邻数据元素之间存在着序偶关系<ai, ai+1>
- 表中元素的个数有限。
- 表中元素具有逻辑上的顺序性,在序列中各元素排序有其先后次序。
- 表中元素都是数据元素,每一个表元素都是单个元素。
- 表中元素的数据类型都相同。这意味着每一个表元素占有相同数量的存储空间。
唯一首元素,唯一尾元素,除首元素外,任何元素都有一个前驱,除尾元素外,任何元素都有一个后继,每个元素有一个位序
注:注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆
3.线性表顺序存储
线性表的顺序存储(Sequential Mapping,简称顺序表):指用一组地址连续的存储单元依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中。
由于C语言的一维数组在内存中所占的正是一个地址连续的存储区域,顺序存储最核心的则是数组
采用顺序存储结构的线性表通常称为顺序表
假设线性表中有n个元素,每个元素占k个单元(字节),第一个元素的地址为loc(a1),则可以计算出第i个元素的地址loc(ai):
loc(ai) =loc(a1)+(i-1)×k
其中loc(a1) 称为基址。 区分元素的序号和数组的下标,如a1的序号为1,而其对应的数组下标为0
顺序表的特点:随机存取,查找时间复杂度O(1),删除、插入得移动大量元素
顺序表结构描述
结构类型的声明:
typedef struct List
{
ElemType data[MAXSIZE]; //数组存储数据元素
int length; //线性表当前长度
}SqList, *list;
注:顺序表的运算
int isEmpty(SqList &L);//判断表是否为空
int getElem(SqList L, int i);//返回第i个位置的值
int listInsert(SqList &L, int i, int e);//在指定位置第i处插入数据e
int listDelete(SqList &L, int i);//删除指定位置的元素
void printList(SqList &L);//打印线性表
int listLength(SqList &L);//求线性表的长度
void initList(SqList &L);//初始化线性表
int locateElem(SqList &L, int x)//返回该值的位置
int destroylist(sqlist &l)//销毁链表
4.顺序表的动态分配
静态分配的顺序表因为内存是固定的,内存分配少了,容易产生溢出,内存分配多了又浪费内存,所以最好是动态分配,但仍然属于顺序表,而不是链表
malloc()函数用来动态分配内存空间
注:静态分配的释放是由程序决定的,主函数运行结束,才释放内存
动态分配的释放是手动释放的,关键字为free(参数);
realloc() 函数用来重新分配内存空间,其原型为:
void* realloc (void* ptr, size_t size);
【参数说明】ptr 为需要重新分配的内存空间指针,size 为新的内存空间的大小。
realloc() 对 ptr 指向的内存重新分配 size 大小的空间,size 可比原来的大或者小,还可以不变。当 malloc()、calloc() 分配的内存空间不够用时,就可以用 realloc() 来调整已分配的内存。
如果 ptr 为 NULL,它的效果和 malloc() 相同,即分配 size 字节的内存空间。
如果 size 的值为 0,那么 ptr 指向的内存空间就会被释放,但是由于没有开辟新的内存空间,所以会返回空指针;类似于调用 free()。
几点注意:
- 指针 ptr 必须是在动态内存空间分配成功的指针,形如如下的指针是不可以的:int *i; int a[2];会导致运行时错误,可以简单的这样记忆:用 malloc()、calloc()、realloc() 分配成功的指针才能被 realloc() 函数接受。
- 成功分配内存后 ptr 将被系统回收,一定不可再对 ptr 指针做任何操作,包括 free();相反的,可以对 realloc() 函数的返回值进行正常操作。
- 如果是扩大内存操作会把 ptr 指向的内存中的数据复制到新地址(新地址也可能会和原地址相同,但依旧不能对原指针进行任何操作);如果是缩小内存操作,原始据会被复制并截取新长度。
【返回值】分配成功返回新的内存地址,可能与 ptr 相同,也可能不同;失败则返回 NULL。
注意:如果分配失败,ptr 指向的内存不会被释放,它的内容也不会改变,依然可以正常使用
补:引用的用法
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。是属于同一个存储单元
引用的声明方法:类型标识符 &引用名=目标变量名;
【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
说明:
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
引用应用
1、引用作为参数
引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C 中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
2、常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
原因在于foo( )和"hello world"串都会产生一个临时对象,而在C 中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const 。
3、引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。这条可以参照Effective C [1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C [1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C [1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
4、引用和多态
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
【例7】:
class A;
class B:public A{……};
B b;
A &Ref = b; // 用派生类对象初始化基类对象的引用
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
5.链表
- 头插法:插入的顺序和链表中的顺序是相反的
- 尾插法:插入的顺序和链表中的顺序是一样的
(1)特点
逻辑次序和物理次序不一定相同,物理内存不要求连续 元素之间的逻辑关系用指针表示
(2)结点
为了正确地表示结点间的逻辑关系,必须在存储线性表的每个数据元素值的同时,存储指示其后继结点的地址(或位置)信息,这两部分信息组成的存储映象叫做结点(Node)
(3)单链表
单链表(Singly linked list):链表中的每个结点只有一个指针域,我们将这种链表称为单链表
单链表包括两个域:数据域用来存储结点的值;指针域用来存储数据元素的直接后继的地址(或位置)
头指针:指向链表第一个结点的指针
尾指针:指向最后一个结点的指针
尾标志:终端结点的指针域为空 NULL
头结点:在第一个元素结点之前附设一个类型相同的结点,头结点简化了对边界的处理——插入、删除、构造等
工作/遍历指针:用来遍历或操作
数据域(data):存储数据元素
指针域(next):存储指向后继结点的地址
单链表的结点只有一个指针域 每个结点内容如图
结点类型描述
typedef struct Node / * 结点类型定义 * / {
ElemType data; struct Node * next; }LNode, *LinkList; /* LinkList为结构指针类型*/
单链表的删除、插入操作时间浪费在查找上,顺序表的删除、插入操作浪费在移动元素上
单链表插入和删除得得到前驱结点的地址
头指针不能轻易移动 链表不能断 单链表运算与顺序表一样
(4)双链表
双链表的结点有两个指针 分别是prior next二个指针
结点类型描述
typedef struct Node //结点类型描述 {
ElemType data; struct Node *prior, *next; //两个指针 }DNode, *DLinkList; //指针类型
头指针不能轻易移动 链表不能断 单链表运算与顺序表一样
(5)循环链表
循环单链表:在单链表基础之上,尾结点的next指针指向头结点
循环双链表:在双链表基础之上,尾结点的next指针指向头结点,头结点的prior指针指向尾结点
(6)静态链表
一些高级语言不支持指针,所以用数组来模拟单链表,两个域,一个数据域存数据,一个指针域(并不是C当中的指针),是存下一个元素的位置
分配空间要大,插入和删除不需要移动元素,指针表示的是下一个元素在数组当中的位置
区别
顺序表:采用顺序存储结构——静态/动态存储分配,即用一段地址连续的存储单元依次存储线性表的数据元素,数据元素之间的逻辑关系通过存储位置(下标)来实现
链表:采用链接存储结构——动态存储分配,即用一组任意的存储单元存放线性表的元素,用指针来反映数据元素之间的逻辑关系
结点的存储密度:顺序表只存储数据元素,链表中指针的结构性开销
从空间上讲,若线性表中元素个数变化较大或者未知,最好使用链表实现;如果用户事先知道线性表的大致长度,使用顺序表的空间效率会更高
从时间上讲,若线性表频繁查找却很少进行插入和删除操作,
或其操作和元素在表中的位置密切相关时,宜采用顺序表作为存储结构,若线性表需频繁插入和删除时,则宜采用链表做存储结构
四.栈
1.定义
栈(Stack)是限定仅在表的一端进行插入和删除操作的线性表(受限的线性表) 允许插入,删除的一端称为栈顶(top),另一端称为栈底(base) 不含任何数据元素的栈称为空栈 栈就相当于乒乓球盒,一个一个往上放,只有一个出口
假设栈S =(a0,a1,…,an-1),则称a0为栈底元素,an-1为栈顶元素。栈中元素按a1,a2,…,an-1的顺序进栈,退栈从栈顶元素开始出栈。所以,栈的修改是按后进先出的原则进行的。因此,栈又称为后进先出(LIFO:last in first out)的线性
2.特点
只有栈顶允许插入和删除
后进先出(Last In First Out,LIFO
注:术语
栈顶(top):允许插入和删除的一端称为栈顶
栈底(bottom):另一端称为栈底
插入:入栈、进栈、压栈
删除:出栈、弹栈
栈的操作特性:后进先出(Last In First Out,LIFO)
3.栈存储结构
顺序存储
顺序栈(Sequence Stack)是指利用顺序存储分配方式来实现的栈,即利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,通常用一维数组存储数据元素,并预设一个最大空间。把数组中下标为0的一端作为栈底,为了指示栈中元素的位置,定义变量top来指示栈顶元素在顺序栈中的位置。
顺序栈分为静态顺序存储和动态顺序存储,静态顺序存储的栈一次性分配空间,在栈满后不能追加空间进行入栈操作,而动态顺序存储是在静态顺序存储的基础上增加了可追加空间的功能。
静态存储是将top定义为整数,而动态存储是将top定义为指针。top可以指向栈顶元素的下一个位置,也可以指向栈顶元素
结点类型描述
typedef struct list{
ElemType data[MAXSIZE]; //元素数组 int top;//栈顶指针 并不是C的指针 } SqStack;
链式存储
链栈:栈的链式存储结构 为方便操作,用链头作为栈顶 通常不会出现栈满的情况
typedef struct linknode {
ElemType data;//保存数据 struct linknode *next;//保存下个结点的地址 }LiStack;
4.递归
直接递归调用就是在函数A(或过程)中直接引用(调用)函数A本身
间接递归调用就是在函数A(或过程)中调用另外一个函数B,而该函数B又引用(调用)了函数A
函数的递归调用------------->>转化成非递归--------------->循环 栈
栈的补充:
1.卡特兰公式:计算多少种不同的退栈序列 1/(n+1) C2n n
2. 栈的顺序存储 : 顺序栈初始化:top是数组下标 top=-1 栈上溢:栈满了 栈下溢:下标小于0
入栈操作:先top指针加1或减1 ,再入栈, 出栈操作刚好相反
3. 共享栈:两个栈共用一个存储
初始化:top1=-1,top2=maxsize 栈满的标志:top2-top1=1
4.链栈:头插法建立链表,链头即为栈顶
5.运算符优先级
操作符 | # | ( | *,/ | +,- | ) |
Isp(内) | 0 | 1 | 5 | 3 | 6 |
Icp(外) | 0 | 6 | 4 | 2 | 1 |
按顺序扫描,栈内大,则出栈,栈外大,进栈,同一级运算符号栈内>栈外,括号匹配时,退栈,匹配了,扫描下一个,操作数则直接输出
6.递归 将递归转化成非递归的方法 1.循环 2.栈
五.队列
1.队列的基本概念
队列 (Queue) :也是运算受限的线性表。是一种先进先出 (First In First Out ,简称 FIFO) 的线性表。只允许在表的一端进行插入,而在另一端进行删除。
队首 (front) :允许进行删除的一端称为队首。
队尾 (rear) :允许进行插入的一端称为队尾。
队列中没有元素时称为空队列。在空队列中依次加入元素 a 1 , a 2 , …, a n 之后, a 1 是队首元素, a n 是队尾元素。显然退出队列的次序也只能是 a 1 , a 2 , …, a n ,即队列的修改是依先进先出的原则进行的,如图 3-5 所示。
(1)顺序队列
利用一组连续的存储单元 ( 一维数组 ) 依次存放从队首到队尾的各个元素,称为顺序队列。对于队列,和顺序栈相类似,也有动态和静态之分。这里介绍静态顺序队列.其类型定义如
下:
typedef int datatype;
#define MAX_QUEUE_SIZE 100
typedef struct queue{
datatype queue_array[MAX_QUEUE_SIZE]; int front; int rear; }sp_queue;
设立一个队首指针 front ,一个队尾指针rear ,分别指向队首和队尾元素。
◆ 初始化: front=rear =0。
◆ 入队:将新元素插入 rear 所指的位置,然后rear 加 1 。
◆ 出队:删去 front 所指的元素,然后加 1 并返回被删元素。
◆ 队列为空: front=rear 。
◆ 队满: rear = MAX_QUEUE_SIZE - 1 或front=rear 。
在非空队列里,队首指针始终指向队头元素,而队尾指针始终指向队尾元素的下一位置。顺序队列中存在“假溢出”现象。因为在入队和出队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。该现象称为假溢出。如图 3-6 所示是数组大小为 5 的顺序队列中队首、队尾指针和队列中元素的变化情况。
代码实现
/* 顺序队列接口定义头文件*/
#define true 1
#define false 0
/* 队的最大长度 */
#define MAX_QUEUE_SIZE 100
/* 队列的数据类型 */
typedef int datatype;
/* 静态链的数据结构 */
typedef struct queue{
datatype sp_queue_array[MAX_QUEUE_SIZE];
/* 队头 */
int front;
/* 队尾 */
int rear;
}sp_queue;
/* 静态顺序链的接口定义 */
/* 静态链的初始化 */
sp_queue queue_init();
/* 判断队列是否为空,若为空
* 返回true
* 否则返回false
*/
int queue_empty(sp_queue q);
/* 插入元素e为队q的队尾新元素
* 插入成功返回true
* 队满返回false
*/
int queue_en(sp_queue *q, datatype e);
/* 队头元素出队
* 用e返回出队元素,并返回true
* 若队空返回false
*/
int queue_de(sp_queue *q, datatype *e);
/* 清空队 */
void queue_clear(sp_queue *q);
/* 获得队头元素
* 队列非空,用e返回队头元素,并返回true
* 否则返回false
*/
int get_front(sp_queue, datatype *e );
/* 获得队长 */
int queue_len(sp_queue q);
/* 遍历队 */
void queue_traverse(sp_queue q, void(*visit)(sp_queue q));
void visit(sp_queue s);
/* 接口实现文件 */
#include<stdio.h>
#include<stdlib.h>
#include"sp_queue.h"
sp_queue queue_init()
{
sp_queue q;
q.front = q.rear = 0;
return q;
}
int queue_empty(sp_queue q)
{
return q.front == q.rear;
}
int queue_en(sp_queue *q, datatype e)
{
/* 队满 */
if (q -> rear == MAX_QUEUE_SIZE)
return false;
/* 入队 */
q -> sp_queue_array[q -> rear] = e;
printf("q.sp_queue_array[%d]=%d\n", q -> rear, e);
q -> rear += 1;
return true;
}
int queue_de(sp_queue *q, datatype *e)
{
/* 队空 */
if(queue_empty(*q))
return false;
/* 出队 */
q -> rear -= 1;
*e = q -> sp_queue_array[q -> rear];
return true;
}
void queue_clear(sp_queue *q)
{
q -> front = q -> rear = 0;
}
int get_front(sp_queue q, datatype *e)
{
/* 队空 */
if(q.front == q.rear)
return false;
/* 获取队头元素 */
*e = q.sp_queue_array[q.front];
return true;
}
int queue_len(sp_queue q)
{
return (q.rear - q.front);
}
void queue_traverse(sp_queue q, void (*visit)(sp_queue q))
{
visit(q);
}
void visit(sp_queue q)
{
/* 队空 */
if (q.front == q.rear)
printf("队列为空\n");
int temp = q.front;
while(temp != q.rear)
{
printf("%d ",q.sp_queue_array[temp]);
temp += 1;
}
printf("\n");
}
int main()
{
sp_queue q = queue_init();
queue_en(&q, 1);
queue_en(&q, 2);
printf("length=%d\n", queue_len(q));
queue_en(&q, 3);
printf("length=%d\n", queue_len(q));
queue_en(&q, 4);
printf("length=%d\n", queue_len(q));
queue_en(&q, 5);
printf("length=%d\n", queue_len(q));
queue_en(&q, 6);
printf("length=%d\n", queue_len(q));
queue_traverse(q,visit);
datatype *e = (datatype *)malloc(sizeof(*e));
queue_de(&q,e);
printf("queue_de(),e=%d length=%d\n", *e, queue_len(q));
queue_traverse(q, visit);
queue_clear(&q);
queue_traverse(q, visit);
printf("length:%d\n", queue_len(q));
}
注意:结构体变量作为函数的参数和其他普通变量一样,值只会在函数体内被修改,想要通过函数更改结构体的值,可以通过结构体指针作为函数的参数实现.
队列的链式表示和实现
队列的链式存储结构简称为链队列,它是限制仅在表头进行删除操作和表尾进行插入操作的单链表。需要两类不同的结点:数据元素结点,队列的队
首指针和队尾指针的结点,如图 3-8 所示。
数据元素结点类型定义:
typedef struct q_node{
datatype data; struct q_node *next;}q_node;
指针结点类型:
typedef struct {
q_node *front;
q_node *rear;
}link_queue;
链队运算及指针变化
链队的操作实际上是单链表的操作,只不过是删除 在表头进行,插入在表尾进行。插入、删除时分别修改 不同的指针。链队运算及指针变化如图 3-9 所示。
代码实现
/* 链式栈接口的定义头文件 */
#define true 1
#define false 0
/* 队列的数据类型 */
typedef int datatype;
/* 静态链的数据结构 */
typedef struct q_node{
datatype data;
struct q_node *next;
}q_node,*link_node;
typedef struct l_queue{
/* 队头指针 */
q_node *front;
/* 队尾指针 */
q_node *rear;
}*link_queue;
/* 静态顺序链的接口定义 */
/* 静态链的初始化 */
link_queue queue_init();
/* 判断队列是否为空,若为空
* 返回true
* 否则返回false
*/
int queue_empty(link_queue q);
/* 插入元素e为队q的队尾新元素
* 插入成功返回true
* 队满返回false
*/
int queue_en(link_queue q, datatype e);
/* 队头元素出队
* 用e返回出队元素,并返回true
* 若队空返回false
*/
int queue_de(link_queue q, datatype *e);
/* 清空队 */
void queue_clear(link_queue q);
/* 销毁队 */
void queue_destroy(link_queue q);
/* 获得队头元素
* 队列非空,用e返回队头元素,并返回true
* 否则返回false
*/
int get_front(link_queue q, datatype *e );
/* 获得队长 */
int queue_len(link_queue q);
/* 遍历队 */
void queue_traverse(link_queue q, void(*visit)(link_queue q));
void visit(link_queue q);
/* 接口的实现文件 */
#include<stdio.h>
#include<stdlib.h>
#include"lp_queue.h"
link_queue queue_init()
{
/* 新建头结点 */
link_node new_node = (link_node)malloc(sizeof(q_node));
new_node -> next = NULL;
/* 指针结点 */
link_queue q = (link_queue)malloc(sizeof(*q));
q -> front = q -> rear = new_node;
return q;
}
int queue_empty(link_queue q)
{
return q -> front == q -> rear;
}
int queue_en(link_queue q, datatype e)
{
/* 新建数据结点 */
link_node new_node = (link_node)malloc(sizeof(q_node));
/* 内存分配失败 */
if(!new_node)
return false;
new_node -> data = e;
q -> rear -> next = new_node;
q -> rear = new_node;
return true;
}
int queue_de(link_queue q, datatype *e)
{
/* 队列为空 */
if (q -> front == q -> rear)
return false;
*e = q -> front -> next -> data;
link_node temp = q -> front -> next;
q -> front -> next = temp -> next;
/* 防止丢失尾指针 */
if (temp == q.rear -> next)
q -> rear = q -> front;
free(temp);
temp = NULL;
return true;
}
void queue_clear(link_queue q)
{
/* 头结点 */
link_node head = q -> front -> next;
head -> next = NULL;
q -> front = q -> rear = head;
/* 第一个结点 */
link_node temp = head -> next;
while(temp)
{
link_node p = temp;
temp = p -> next;
free(p);
p = NULL;
}
}
void queue_destroy(link_queue q)
{
queue_clear(q);
free(q);
q = NULL;
}
int get_front(link_queue q, datatype *e)
{
/* 队为空 */
if (q -> front == q -> rear)
return false;
*e = q -> front -> next -> data;
link_node temp = q -> front -> next;
q -> front -> next = temp -> next;
free(temp);
temp = NULL;
return true;
}
int queue_len(link_queue q)
{
/* 头结点 */
link_node p = q -> front -> next;
/* 计数器 */
int count = 0;
while(p)
{
count += 1;
p = p -> next;
}
return count;
}
void queue_traverse(link_queue q, void(*visit)(link_queue q))
{
visit(q);
}
void visit(link_queue q)
{
/* 头结点 */
link_node p = q -> front -> next;
if(!p)
{
printf("队列为空");
}
while(p)
{
printf("%d ", p -> data);
p = p -> next;
}
printf("\n");
}
int main()
{
link_queue q = queue_init();
queue_en(q, 1);
queue_en(q, 2);
printf("length=%d\n", queue_len(q));
queue_en(q, 3);
printf("length=%d\n", queue_len(q));
queue_en(q, 4);
printf("length=%d\n", queue_len(q));
queue_en(q, 5);
printf("length=%d\n", queue_len(q));
queue_en(q, 6);
printf("length=%d\n", queue_len(q));
queue_traverse(q,visit);
datatype *e = (datatype *)malloc(sizeof(*e));
queue_de(q,e);
printf("queue_de(),e=%d length=%d\n", *e, queue_len(q));
queue_traverse(q, visit);
queue_clear(q);
queue_traverse(q, visit);
printf("length:%d\n", queue_len(q));
}
(2)循环队列
1.循环队列需要几个参数来确定
循环队列需要2个参数,front和rear
2.循环队列各个参数的含义
(1)队列初始化时,front和rear值都为零;
(2)当队列不为空时,front指向队列的第一个元素,rear指向队列最后一个元素的下一个位置;
(3)当队列为空时,front与rear的值相等,但不一定为零;
3.循环队列入队的伪算法
(1)把值存在rear所在的位置;
(2)rear=(rear+1)%maxsize ,其中maxsize代表数组的长度;
程序代码:
bool Enqueue(PQUEUE Q, int val)
{
if(FullQueue(Q))
return false;
else
{
Q->pBase[Q->rear]=val;
Q->rear=(Q->rear+1)%Q->maxsize;
return true;
}
}
4.循环队列出队的伪算法
(1)先保存出队的值;
(2)front=(front+1)%maxsize ,其中maxsize代表数组的长度;
程序代码:
bool Dequeue(PQUEUE Q, int *val)
{
if(EmptyQueue(Q))
{
return false;
}
else
{
*val=Q->pBase[Q->front];
Q->front=(Q->front+1)%Q->maxsize;
return true;
}
}
5.如何判断循环队列是否为空
if(front==rear)
队列空;
else
队列不空;
bool EmptyQueue(PQUEUE Q)
{
if(Q->front==Q->rear) //判断是否为空
return true;
else
return false;
}
6.如何判断循环队列是否为满
这个问题比较复杂,假设数组的存数空间为7,此时已经存放1,a,5,7,22,90六个元素了,如果在往数组中添加一个元素,则rear=front;此时,队列满与队列空的判断条件front=rear相同,这样的话我们就不能判断队列到底是空还是满了;
解决这个问题有两个办法:一是增加一个参数,用来记录数组中当前元素的个数;第二个办法是,少用一个存储空间,也就是数组的最后一个存数空间不用,当(rear+1)%maxsiz=front时,队列满;
bool FullQueue(PQUEUE Q)
{
if(Q->front==(Q->rear+1)%Q->maxsize) //判断循环链表是否满,留一个预留空间不用
return true;
else
return false;
}
附录:
queue.h文件代码:
#ifndef __QUEUE_H_
#define __QUEUE_H_
typedef struct queue
{
int *pBase;
int front; //指向队列第一个元素
int rear; //指向队列最后一个元素的下一个元素
int maxsize; //循环队列的最大存储空间
}QUEUE,*PQUEUE;
void CreateQueue(PQUEUE Q,int maxsize);
void TraverseQueue(PQUEUE Q);
bool FullQueue(PQUEUE Q);
bool EmptyQueue(PQUEUE Q);
bool Enqueue(PQUEUE Q, int val);
bool Dequeue(PQUEUE Q, int *val);
#endif
queue.c文件代码:
#include<stdio.h>
#include<stdlib.h>
#include"malloc.h"
#include"queue.h"
/***********************************************
Function: Create a empty stack;
************************************************/
void CreateQueue(PQUEUE Q,int maxsize)
{
Q->pBase=(int *)malloc(sizeof(int)*maxsize);
if(NULL==Q->pBase)
{
printf("Memory allocation failure");
exit(-1); //退出程序
}
Q->front=0; //初始化参数
Q->rear=0;
Q->maxsize=maxsize;
}
/***********************************************
Function: Print the stack element;
************************************************/
void TraverseQueue(PQUEUE Q)
{
int i=Q->front;
printf("队中的元素是:\n");
while(i%Q->maxsize!=Q->rear)
{
printf("%d ",Q->pBase[i]);
i++;
}
printf("\n");
}
bool FullQueue(PQUEUE Q)
{
if(Q->front==(Q->rear+1)%Q->maxsize) //判断循环链表是否满,留一个预留空间不用
return true;
else
return false;
}
bool EmptyQueue(PQUEUE Q)
{
if(Q->front==Q->rear) //判断是否为空
return true;
else
return false;
}
bool Enqueue(PQUEUE Q, int val)
{
if(FullQueue(Q))
return false;
else
{
Q->pBase[Q->rear]=val;
Q->rear=(Q->rear+1)%Q->maxsize;
return true;
}
}
bool Dequeue(PQUEUE Q, int *val)
{
if(EmptyQueue(Q))
{
return false;
}
else
{
*val=Q->pBase[Q->front];
Q->front=(Q->front+1)%Q->maxsize;
return true;
}
}
六.广义表和串
广义表
1.定义
广义表简称表,它是线性表的推广。一个广义表是n(n>=0)个元素的一个有限序列,当n=0时称为空表。在一个非空的广义表中,其元素可以是某一确定类型的对象,这种元素被称为单元素;也可以是由单元素构成的表,这种元素被称为子表或表元素。显然,广义表的定义是递归的,广义表是线性表的递归数据结构。
设ai为广义表的第i个元素,则广义表的一般表示与线性表相同。
(a1,a2,a3,a4......,an)
其中,n表示广义表的长度,即广义表中所含元素的个数,n>=0。
同线性表一样,也可以用一个标识符来命名一个广义表,例如:用LS命名上面的广义表,则为:LS(a1,a2,a3...an+1,an)。在广义表的讨论中,为了把单元素同表元素区别开来,一般用小写字母表示表元素,用大写字母表示表,例如:
A=() B=(e) C=(a,(b,c,d))
D=(A,B,C)=((),(e),(a,(b,c,d))) E=((a,(a,b),((a,b),c)))
其中,A是一个空表,不含任何元素,其长度为0;B是一个只含有单元素e的表,其长度为1;C中有两个元素,第1个元素是单元素a,第2个元素是表元素(a,b,c),C的长度为2;D中有3个元素,其中每个元素又都是一个表,D的长度为3;E中只含有一个元素,该元素是一个表,该表中含3个元素,其中后两个元素又都是表。
一个广义表的深度是指该表中括号嵌套的最大重数,在图像中则是值从树根节点到每个表元素结点所经过的结点个数的最大值。
广义表的长度:广义表内有多少个子表或者是原子 例如 C的长度为2
广义表的深度:所包括的括号层数(最大的同一边) 例如 D的深度为3
2.广义表的存储结构
广义表是一种递归的数据结构,因此很难为每个广义表分配固定大小的存储空间,所以其存储结构只好采用动态链接结构。
广义表中的结点类型可定义为:
public class GeneralizedNode { //假定用GeneralizedNode来表示广义表中的结点类
boolean tag; //结点标志域
Object data; //结点值域
GeneralizedNode sublist; //指向子表的引用域
public GeneralizedNode(boolean tag, Object data, GeneralizedNode sublist) {
this.tag = tag;
this.data = data;
this.sublist = sublist;//利用每个参数分别给相应成员赋值
}
}
当一个结点的标志域tag的取值为false时,表明它是一个单元素结点,此时结点值域data和后继链接域next有效,而子表的链接(表头指针)域sublist无效;相反,当tag域的取值为true时,则表明它是一个表元素(子表)结点,此时指向子表的链接域sublist和指向后继结点的链接域next有效,而结点的值域data无效。在广义表的链接存储结构中,通过表结点中的sublist域,保存其子表中第1个元素结点的引用,实现向下一层子表的链接;通过每个结点中的next域,保存其后继结点的引用,实现向同层次的后继结点的链接。
3.广义表类的定义
在广义表类的定义中,应包含数据成员和方法成员,数据成员一般是私有的,以体现类和对象的隐藏性,方法成员若被被提供作为外部调用的接口,则应是非私有的,若只提供给内部的方法所调用,则应是私有的。
广义表是线性表的递归结构,它的链接存储结构也是单链表的递归结构,所有对它进行的各种运算方法必然也是递归的。
当类中的一个方法需要递归实现时,必须另外定义一个驱动方法,该方法是一个非递归方法,方法体中只包含一条调用递归方法的语句。驱动方法的功能有两个:一是提供给外部的类和对象调用,所有它通常具有公有的访问属性;二是调用同名的递归方法,实现运算的功能,递归方法只需要提供给本类使用,不需要提供给外部使用,所有通常被定义为私有访问属性。
3.1求广义表的长度
在广义表 中,同一层次的每个结点是通过next域链接起来的,所有它是由next域链接起来的单链表。这样,求广义表的长度就是求单链表的长度,可以采用重复循环,从头到尾依次访问单链表中的每个结点,并进行已访问结点个数的统计,则能够求出单链表的长度,亦即广义表的长度。
private int length(GeneralizedNode glt) //求广义表长度的递归函数
{//求出表头指针glt所指向的广义表的长度,glt初始指向广义表的第1个结点
if(glt!=null) //若glt不为空,其长度等于1加上后继表的长度
{
return 1+length(glt.next);
}
else //若glt为空,其长度等于0
return 0;
}
3.2 求广义表的深度
广义表深度的递归定义是它等于所有子表中表的最大深度加1。若一个表为空或仅由单元素所组成,则深度为1,它是结束向下继续递归求深度的终止条件。设dep表示任一子表的深度,max表示所有子表中的最大深度,depth表示广义表的深度,则有depth=max+1
当一个表不包含任何子表时,其深度为1,所以在算法中max的初值应为0,返回max+1的值1就正好等于此时表的深度。
private int depth(GeneralizedNode glt) //求广义表深度的递归函数
{//求出表头指针glt所指向的广义表的深度,glt初始指向广义表的第1个结点
int max=0; //给max赋初值0
while(glt!=null) //遍历表中每一个结点
{
if(glt.tag==true)
{
int dep=depth(glt.sublist);//递归调用求出子表的深度
if(dep>max)
max=dep; //让max为同层求过子表深度的最大值
}
glt=glt.next; //使表头指针指向同一层的下一个结点
}
return max+1;
}
下面给出广义表类的具体定义:
public class GeneralizedList { //广义表类的定义
private GeneralizedNode head; //广义表结点引用域
private int i=0; //此变量i为建立广义表create方法所使用
public GeneralizedList() //广义表的构造方法,创建一个空的表结点
{ //表结点的tag域为true,其余为空值
head=new GeneralizedNode(true,null,null,null);
}
public int length() //求广义表长度的驱动方法
{
return length(head.sublist); //以表结点的子表引用域为参数调用递归函数
}
private int length(GeneralizedNode glt) //求广义表长度的递归函数
{//求出表头指针glt所指向的广义表的长度,glt初始指向广义表的第1个结点
if(glt!=null) //若glt不为空,其长度等于1加上后继表的长度
{
return 1+length(glt.next);
}
else //若glt为空,其长度等于0
return 0;
}
public int depth() //求广义表深度的驱动方法
{
return depth(head.sublist); //以表结点的子表引用域为参数调用递归函数
}
private int depth(GeneralizedNode glt) //求广义表深度的递归函数
{//求出表头指针glt所指向的广义表的深度,glt初始指向广义表的第1个结点
int max=0; //给max赋初值0
while(glt!=null) //遍历表中每一个结点
{
if(glt.tag==true)
{
int dep=depth(glt.sublist);//递归调用求出子表的深度
if(dep>max)
max=dep; //让max为同层求过子表深度的最大值
}
glt=glt.next; //使表头指针指向同一层的下一个结点
}
return max+1;
}
public Object find(Object obj) //从当前广义表中查找值为obj的结点
{
return find(head,obj); //此为调用递归函数的驱动方法
}
//以整个广义表结点的引用和obj作为参数
private Object find(GeneralizedNode glt,Object obj)
{ //从当前广义表中查找值为obj的递归方法,glt初始指向整个广义表的表结点
while(glt!=null) //处理广义表中的每个结点
{
if(glt.tag==false) //处理单元素结点
{
if(glt.data.equals(obj))
{
return glt.data; //查找成功返回结点值
}
else
glt=glt.next; //否则继续向后继结点查找
}
else //处理表元素结点
{
Object x=find(glt.sublist,obj);//在子表中查找,结果存入x中
if(x!=null)
{
return x; //在子表中查找成功返回结点值
}
else
{
glt=glt.next; //否则继续向表结点的后继结点查找
}
}
}
return null; //没有从表中找到应返回空值
}
public void output() //输出当前广义表的驱动方法
{
output(head); //以整个广义表结点的引用作为调用实参
}
private void output(GeneralizedNode glt)//输出广义表的递归函数
{
//以广义表的书写格式输出广义表glt,glt初始指向整个广义表的表结点
if(glt.tag==true) //对表结点进行处理
{
System.out.print("("); //先输出左括号,作为表的开始符号
if(glt.sublist==null) //若子表引用为空,则输出‘#’字符表示空表
{
System.out.print("#");
}
else //当一个子表输出结束后,应输出右括号作为终止符
output(glt.sublist);
System.out.print(")"); //对于单元素结点,输出该结点的值
if(glt.next!=null) //处理后继表
{
System.out.print(","); //先输出逗号分隔符
output(glt.next); //再递归输出后继表
}
}
}
public void create(String s) //根据字符串s中保存的广义表建立其存储结构
{
create(head,s); //此为调用同名递归函数的驱动方法
}
private void create(GeneralizedNode glt,String s)
{ //以整个广义表的引用和字符串s作为参数
//建立广义表存储结构的递归函数,glt初始指向整个广义表的表结点
char ch='\0';
//利用ch读取一个字符,循环结束后ch读取的只可能是左括号、英文字母或#字符
while((ch=s.charAt(i))==' ')
{
i++; //忽略掉空格
}
//若读取的为左括号则建立由glt.sublist所指向的子表并递归构造子表
if(ch=='(')
{
glt.sublist=new GeneralizedNode(true,null,null,null);//初始为表结点
i++;
create(glt.sublist,s); //递归建立子表
if(s.charAt(i)=='#') //若子表为空表,则修改子表域为空
{
while((ch=s.charAt(++i))==' ')//忽略空格
{
if(ch==')')
{
glt.sublist=null; //修改子表域为空
}
else
{
illegalChar(); //处理非法字符
}
}
}
//若读取的为字符元素,则修改初始建立的表结点为现在的单元素结点
else if(ch>='a' && ch<='z')
{
glt.tag=false; //修改为单元素结点
String str = String.valueOf(ch);
glt.data=str; //给数据域赋值
}
//若读取的为表示空的井字符,则返回到上面的递归建立子表的语句之后
else if(ch=='#') return;
//ch所读取的字符必为非法字符,应显示错误信息并退出运行
else illegalChar();
i++; //处理一个子表或元素后,使指示处理字符位置的i值增1
//空格被跳过,循环结束后,ch读取的字符必为逗号、右括号或分号
while((ch=s.charAt(i))==' ')
{
i++; //忽略掉空格
}
//若读取的为逗号则递归构造后继表
if(ch==',')
{
glt.next=new GeneralizedNode(true,null,null,null);//初始为表结构
i++; //使指示处理字符位置的i值增1,以便处理下一个子表或单元素
create(glt.next,s); //递归调用构造后继表
}
//若读取的为右括号或分号表明相应的子表或整个表处理完毕,则不做任何事情,自然结束
else if(ch==')'||ch==';');
//所读取的字符必为非法字符,应显示错误信息并退出运行
else illegalChar();
}
}
private void illegalChar() //在create算法中,调用此方法处理非法字符
{
System.out.println("广义表字符串中存在非法字符,终止运行!");
System.exit(1);
}
public void clear() //清除广义表中的所有元素,使之成为空表
{
head.sublist=head.next=null;
}
}
串
1.定义
字符串简称串,是一种特殊的线性表,它的数据元素仅由一个字符组成。
串(string)是由n(n>=0)个字符组成的有限序列。一般记作s="a1a2a3...an”,其中s为串名,双引号括起来的字符序列为串值,
双引号本身不属于串的内容,a1,a2,a3,....an为串值,其中的每个元素ai可以为字母,数字,或者其他的字符
串长是指串中字符的个数。例如 “123”第长度为3的数字字符串,而是一个整型常数。长度为0的串成为空串,记作“”。空格也是串的字符集合中的一个元素,由一个或多个空格组成的串成为空格串,如“ ”,其长度为串的空格的个数。
串中每个字符的顺序编号称为该字符在串中的位置。例如 字符C在字符串“structure”中的位置是5.当二个串的长度相等并且对应位置上的字符都相等,才称这二个串是相等的
串中任意连续字符组成的子序列称为该串的子串,而包含子串的串相应地称为主串。通常将子串在主串中首次出现的位置称为子串在主串中的位置。例如 串s1="is" , s2="This is a string",则s1为s2的子串,s2相对于s1为主串。s1在s2中出现了二次,其中首次出现所对应的主串位置是3,因此,称为s1在s2中的位置为3.
虽然串是由字符组成的,但串和字符是二个不同的概念。串是长度不确定的字符序列,而字符只是一个字符。即使是长度为1的串也与字符不同。例如,串“a” 和字符‘a’就是二个不同的概念,因为在存储时串的结尾通常加上串结束标志‘\0’
串结构体定义
#define MAXLEN 10//定义窜的最大长度
typedef struct
{
char vec[MAXLEN];
int len;//串的实际长度
} Str;//可用Str来定义该类型的结构体变量
在串尾存储一个不会在串中出现的特殊字符作为串的终结符,以此表示串的结尾。比如C语言中处理定长串的方法就是这样的,它是用‘\0’表示串的结束,
2.串的基本操作
(1)、求串长LenStr(s)
操作条件:串s存在。
操作结果:求出串s的长度。
用判断当前字符是有是‘\0’来确定串是否结束,若非‘\0’,则表示字符串长度的i增加1;若是‘\0’,则表示字符串结束,跳出循环,i即字符串的长度。
int LenStr(Str *r){
int i=0;
while(r->vec[i]!='\0'){
i++;
}
return i;
}
测试代码
void ShowStr(Str *r){
printf("\n\t\t该串值为: ");
if(r->vec[0]=='\0'){
printf("空串! \n");
}else{
puts(r->vec);//使用puts函数输出字符串,格式为 puts(字符串组名)
}
}
int main()
{
Str a;
Str *r=&a;
r->vec[0]='\0';
char choice;
int ch=1;
while(ch!=0){
printf("\n");
printf("\n\t\t 串子系统 *");
scanf("%c",&choice);
getchar();
if(choice=='1'){
printf("\n\t\t请输入一个字符串: ");
gets(r->vec);//使用get函数输入字符串 格式为 gets(字符串组名)
r->len=LenStr(r);
}else if(choice=='8'){
ShowStr(r);
int n=LenStr(r);
printf("串长度为:%d",n);
}
}
return 0;
}
(2)、串连接ConcatStr(s1,s2)
操作条件:串s1,s2存在。
操作结果:新串s1是串s1和串s2连接以后的新串,原串s2值不变,串s1的值则改变。
例子:设s1=“Micsosoft”,s2=“Office”。
操作结果是s1=“MicsosoftOffice”; s2=“Office”。
把两个串r1和r2首尾连接成一个新串r1,即:r1=r1+r2。
void ConcatStr(Str *r1,Str *r2){
int i;
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
if(r1->len+r2->len>MAXLEN){
printf("两个串太长,溢出! ");//连接后的串长超过串的最大长度
}else{
for( i=0;i<r2->len;i++){
r1->vec[r1->len+i]=r2->vec[i];//进行连接
}
r1->vec[r1->len+i]='\0';
r1->len=r1->len+r2->len;//修改连接后的新串的长度
}
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
}
测试代码
int main()
{
Str a,b;
Str *r=&a,*r1;
r->vec[0]='\0';
char choice;
int ch=1;
while(ch!=0){
printf("\n");
printf("\n\t\t 串子系统 *");
scanf("%c",&choice);
getchar();
if(choice=='1'){
printf("\n\t\t请输入一个字符串: ");
gets(r->vec);//使用get函数输入字符串 格式为 gets(字符串组名)
r->len=LenStr(r);
}else if(choice=='2'){
printf("\n\t\t请输入所要连接字符串: ");
r1=CreateStr(&b);
printf("\n\t\tr1为: ");
puts(r1->vec);
ConcatStr(r,r1);
printf("\n\t\t连接后的新串值为: ");
puts(r->vec);
int n=LenStr(r);
printf("新串长度为:%d",n);
}
else if(choice=='8'){
ShowStr(r);
int n=LenStr(r);
printf("串长度为:%d",n);
}
}
return 0;
}
(3)、求子串SubStr(s,i,len)
操作条件:串s存在。
操作结果:返回从串s的第i个字符开始的长度为len的子串。len=0得到的是空串。
例子:SubStr(“abcdefghi”,3,4)=“cdef”。
在给定的字符串r中从指定位置i开始连续取出j个字符构成子串r1。
void SubStr(Str *r,Str *r1,int i ,int j){
if(i+j-1>MAXLEN){
printf("子串越界! ");
}else{
for(int k=0;k<j;k++){
r1->vec[k]=r->vec[i+k-1];//从r中取出子串
}
r1->len=j;
r1->vec[r1->len]='\0';
}
printf("\n\t\t 取出字符为r1=%s ",r1->vec);
}
测试代码
else if(choice=='3'){
printf("\n\t\t请输入从第几个字符开始: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入取出的连续字符数: ");
scanf("%d",&j);
getchar();
SubStr(r,&a,i ,j);
}
(4)、串比较EqualStr(s1,s2)
操作条件:串s1,s2存在。
操作结果:若s1等于s2,返回值为0;若s1小于s2,返回值小于0; 若s1大于s2,返回值大于0。
两个串的长度相等且各对应位置上的字符都相同时,两个串才相等。
int EqualStr(Str *r1,Str *r2){
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
int i=0;
while(r1->vec[i]==r2->vec[i]&&r1->vec[i]!='\0'&&r2->vec[i]!='\0')
i++;
if(r1->vec[i]==r2->vec[i])
return 0;
else if(r1->vec[i]>r2->vec[i])
return 1;
else
return -1;
}
测试代码如下:
else if(choice=='7'){
printf("\n\t\t请输入第一个串: ");
gets(c.vec);
printf("\n\t\t请输入第二个串: ");
gets(d.vec);
int k=EqualStr(&c,&d);
if(k>0){
printf("\n\t\t第一个串大! \n");
}else if(k<0){
printf("\n\t\t第二个串大! \n");
}else {
printf("\n\t\t两个串一样大! \n");
}
}
(5)、子串查找IndexStr(s,t)
找子串t在主串s中首次出现的位置(也称模式匹配)
操作条件:串s,t存在。
操作结果:若t是s的子串,则返回在s中首次出现的位置,否则返回值为0。
例子:子串定位
IndexStr(“abcdebda”,”bc”)=2;
IndexStr(“abcdebda”,”ba”)=0;
int IndexStr(Str *r,Str *r1){
printf("\n\t\t r=%s r1=%s\n",r->vec,r1->vec);
int i,j,k;
for(i=0;r->vec[i];i++){
for(j=i,k=0;r->vec[j]==r1->vec[k];j++,k++){
if(!r1->vec[k+1]){
return i;
}
return -1;
}
}
}
测试代码如下:
else if(choice=='6'){
printf("\n\t\t请输入所要查找的字符串: ");
r1=CreateStr(&b);
i=IndexStr(r,r1);
if(i!=-1){
printf("\n\t\t第一次出现的位置是第%d个.\n ",i+1);
}else{
printf("\n\t\t该子串不在其中!");
}
(6)、串插入InsStr(s,t,i)
操作条件:串s,t存在。
操作结果:将串t插入到串s中的第i个字符前,s的串值发生改变。
在字符串r中的指定位置i插入子串r1。
Str *InsStr(Str *r,Str *r1,int i){
printf("\n\t\t r=%s r1=%s\n",r->vec,r1->vec);
if(i>r->len||r->len+r1->len>MAXLEN){
printf("不能插入!");
}else{
for(int k=r->len-1;k>=i;k--){
r->vec[r1->len+k]=r->vec[k];//后移空出的位置
}
for(int k=0;k<r1->len;k++){
r->vec[i+k]=r1->vec[k];//插入子串
}
r->len=r->len+r1->len;
r->vec[r->len]='\0';
}
printf("\n\t\t 插入后的新串 r=%s \n",r->vec);
return r;
}
测试代码如下:
else if(choice=='5'){
printf("\n\t\t请输入在第几个字符前插入: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入所要插入的字符串: ");
r1=CreateStr(&b);
Str *newStr=InsStr(r,r1,i-1);
printf("\n\t\t新串值为: ");
puts(newStr->vec);
}
(7)、串删除DelStr(s,i,len)
操作条件:串s存在。
操作结果:删除串s中第i个字符起长度为len的子串,s的值改变。
在给定的字符串r中删除从指定位置i开始连续的j个字符
void DelStr(Str *r,int i ,int j){
if(i+j-1>r->len){
printf("所要删除的字符串越界!");
}else{
for(int k=i+j;k<r->len;k++,i++){
r->vec[i]=r->vec[k];//将后面的字符串前移覆盖
}
r->len=r->len-j;
r->vec[r->len]='\0';
}
printf("\n\t\t 删除后的新串 r=%s \n",r->vec);
}
测试代码如下:
else if(choice=='4'){
printf("\n\t\t请输入从第几个字符开始: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入删除的连续字符数: ");
scanf("%d",&j);
getchar();
DelStr(r,i ,j);
}
串子系统完整代码
#include<iostream>
using namespace std;
#define MAXLEN 100//定义窜的最大长度
typedef struct
{
char vec[MAXLEN];
int len;//串的实际长度
} Str;//可用Str来定义该类型的结构体变量
int LenStr(Str *r){
int i=0;
while(r->vec[i]!='\0'){
i++;
}
return i;
}
Str *CreateStr(Str *r){
gets(r->vec);
r->len=LenStr(r);
return r;
}
void ShowStr(Str *r){
printf("\n\t\t该串值为: ");
if(r->vec[0]=='\0'){
printf("空串! \n");
}else{
puts(r->vec);//使用puts函数输出字符串,格式为 puts(字符串组名)
}
}
void ConcatStr(Str *r1,Str *r2){
int i;
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
if(r1->len+r2->len>MAXLEN){
printf("两个串太长,溢出! ");//连接后的串长超过串的最大长度
}else{
for( i=0;i<r2->len;i++){
r1->vec[r1->len+i]=r2->vec[i];//进行连接
}
r1->vec[r1->len+i]='\0';
r1->len=r1->len+r2->len;//修改连接后的新串的长度
}
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
}
void SubStr(Str *r,Str *r1,int i ,int j){
if(i+j-1>MAXLEN){
printf("子串越界! ");
}else{
for(int k=0;k<j;k++){
r1->vec[k]=r->vec[i+k-1];//从r中取出子串
}
r1->len=j;
r1->vec[r1->len]='\0';
}
printf("\n\t\t 取出字符为r1=%s ",r1->vec);
}
int EqualStr(Str *r1,Str *r2){
printf("\n\t\t r1=%s r2=%s\n",r1->vec,r2->vec);
int i=0;
while(r1->vec[i]==r2->vec[i]&&r1->vec[i]!='\0'&&r2->vec[i]!='\0')
i++;
if(r1->vec[i]==r2->vec[i])
return 0;
else if(r1->vec[i]>r2->vec[i])
return 1;
else
return -1;
}
Str *InsStr(Str *r,Str *r1,int i){
printf("\n\t\t r=%s r1=%s\n",r->vec,r1->vec);
if(i>r->len||r->len+r1->len>MAXLEN){
printf("不能插入!");
}else{
for(int k=r->len-1;k>=i;k--){
r->vec[r1->len+k]=r->vec[k];//后移空出的位置
}
for(int k=0;k<r1->len;k++){
r->vec[i+k]=r1->vec[k];//插入子串
}
r->len=r->len+r1->len;
r->vec[r->len]='\0';
}
printf("\n\t\t 插入后的新串 r=%s \n",r->vec);
return r;
}
void DelStr(Str *r,int i ,int j){
if(i+j-1>r->len){
printf("所要删除的字符串越界!");
}else{
for(int k=i+j;k<r->len;k++,i++){
r->vec[i]=r->vec[k];//将后面的字符串前移覆盖
}
r->len=r->len-j;
r->vec[r->len]='\0';
}
printf("\n\t\t 删除后的新串 r=%s \n",r->vec);
}
int IndexStr(Str *r,Str *r1){
printf("\n\t\t r=%s r1=%s\n",r->vec,r1->vec);
int i,j,k;
for(i=0;r->vec[i];i++){
for(j=i,k=0;r->vec[j]==r1->vec[k];j++,k++){
if(!r1->vec[k+1]){
return i;
}
return -1;
}
}
}
int main()
{
Str a,b,c,d;
Str *r=&a,*r1;
r->vec[0]='\0';
char choice,p;
int i,j, ch=1;
while(ch!=0){
printf("\n");
printf("\n\t\t 串子系统 *");
printf("\n\t\t************************************");
printf("\n\t\t* 1------输入字符串 *");
printf("\n\t\t* 2------连接字符串 *");
printf("\n\t\t* 3------取出子串 *");
printf("\n\t\t* 4------删除子串 *");
printf("\n\t\t* 5------插入子串 *");
printf("\n\t\t* 6------查找子串 *");
printf("\n\t\t* 7------比较串大小 *");
printf("\n\t\t* 8------显示字符串 *");
printf("\n\t\t* 0------返 回 *");
printf("\n\t\t************************************");
printf("\n\t\t请选择菜单号(0-8): *");
scanf("%c",&choice);
getchar();
if(choice=='1'){
printf("\n\t\t请输入一个字符串: ");
gets(r->vec);//使用get函数输入字符串 格式为 gets(字符串组名)
r->len=LenStr(r);
}else if(choice=='2'){
printf("\n\t\t请输入所要连接字符串: ");
r1=CreateStr(&b);
printf("\n\t\tr1为: ");
puts(r1->vec);
ConcatStr(r,r1);
printf("\n\t\t连接后的新串值为: ");
puts(r->vec);
int n=LenStr(r);
printf("新串长度为:%d",n);
}else if(choice=='3'){
printf("\n\t\t请输入从第几个字符开始: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入取出的连续字符数: ");
scanf("%d",&j);
getchar();
SubStr(r,&a,i ,j);
}else if(choice=='4'){
printf("\n\t\t请输入从第几个字符开始: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入删除的连续字符数: ");
scanf("%d",&j);
getchar();
DelStr(r,i ,j);
}else if(choice=='5'){
printf("\n\t\t请输入在第几个字符前插入: ");
scanf("%d",&i);
getchar();
printf("\n\t\t请输入所要插入的字符串: ");
r1=CreateStr(&b);
Str *newStr=InsStr(r,r1,i-1);
printf("\n\t\t新串值为: ");
puts(newStr->vec);
}else if(choice=='6'){
printf("\n\t\t请输入所要查找的字符串: ");
r1=CreateStr(&b);
i=IndexStr(r,r1);
if(i!=-1){
printf("\n\t\t第一次出现的位置是第%d个.\n ",i+1);
}else{
printf("\n\t\t该子串不在其中!");
}
}else if(choice=='7'){
printf("\n\t\t请输入第一个串: ");
gets(c.vec);
printf("\n\t\t请输入第二个串: ");
gets(d.vec);
int k=EqualStr(&c,&d);
if(k>0){
printf("\n\t\t第一个串大! \n");
}else if(k<0){
printf("\n\t\t第二个串大! \n");
}else {
printf("\n\t\t两个串一样大! \n");
}
}else if(choice=='8'){
ShowStr(r);
int n=LenStr(r);
printf("串长度为:%d",n);
}else if(choice=='0'){
break;
}else{
printf("\n\t\t请注意:输入有误! \n");
if(choice!='X'&&choice!='x'){
printf("\n\t\t按回车键继续,按任意键返回主菜单 \n");
p=getchar();
if(p!='\xA'){
getchar();
break;
}
}
}
}
return 0;
}
补:
KMP算法
KMP算法的核心思想是利用已经得到的部分匹配信息来进行后面的匹配过程。在匹配过程中指针 i 没有回溯。
某趟在si和tj匹配失败后,即当 S[i] <> T[j] 时,已经得到的结果:S[ i-j+1 ... i-1 ] == T[ 1 ... j-1 ]
如果模式串中有满足下述关系的子串存在:T[ 1 ... k-1 ] == T[ j-k+1 ... j-1 ]
则有 S[i-k+1..i-1] == T[1..k-1]
即:模式中的前k-1个字符与模式中tj字符前面的k-1个字符相等时,模式t就可以向右"滑动"至使tk和si对准,继续向右进行比较即可。
匹配过程如下图:
重点:模式中的next函数:当匹配过程中“失配”时,模式串“向右滑动的距离多远”,换句话说,当主串中的第i个字符与模式中的第j个字符“失配时”,主串中第i个字符(i指针不回溯)此时应与模式中哪个字符比较。这个字符定义为j的next位置,即i对应的主串字符应与next[j]对应的模式字符继续比较。
模式中的每一个tj都对应一个k值,这个k值仅依赖于模式T本身字符序列的构成,而与主串S无关。
例如下列串的next值情况如下:
利用next值进行匹配:
那么next值如何求得呢?
求 next 函数值的过程是一个递推过程,分析如下:
已知:next[1] = 0;
假设:next[j] = k;即
此时, next[j+1]=?有两种可能:
(1)若tk=tj,则有,
则:next[j+1] = k+1 = next[j] + 1
(2)若tk!=tj,则有
则需往前回朔,检查 tj = t?
这实际上也是一个匹配的过程,不同在于:主串和模式串是同一个串。
由于此时tk!=tj,相当于k指示的是模式,j指示的是主串,则此时应比较k的next值即next[k]对应的模式字符,设next[k]=k’, 即比较tk’与tj。若tk’ = tj,则next[j+1] = k’+1 = next[k’]+ 1;若tk’也不等于tj,则需再找tk’的next值,若设next[tk’] = k’’ ,则比较tk’’与tj,……,以此类推,直至tj与某个模式中某个字符匹配成功或者不存在任何k’(1<k’<j)满足
(1<k’<k<j),则next[j+1] = 1。
这样可得到next算法如下:
void get_next(SString &T, int &next[] )
{ // 求模式串T的next函数值并存入数组next。
i = 1; next[1] = 0; j = 0;
while (i < T[0])
{ if (j == 0 || T[i] == T[j])
{++i; ++j; next[i] = j; }
else j = next[j];
}
} // get_next
分析上面的代码:
1.当比较到主串第第i个字符与模式的第j个字符时,若si != tj而导致j退回到0,说明模式串的第一个字符就“失配”了,此时要从主串的第i+1个字符起,与模式的第1个字符开始重新比较,即next[i+1] = 1,所以j == 0 时,由于 i,j均自增了,则就是next[i] = j;
2.当比较到主串第第i个字符与模式的第j个字符时,当si !=tj 时,则应比较si与t next[j],因为设置了新j = next[j] ,则是比较si与新tj,若此时有si = tj,则next[i+1] = j+1,所以当i、j均自增后,应有next[i] = j。
3.当比较到主串第第i个字符与模式的第j个字符时,当si !=tj 时,则应比较si与t next[j],因为设置了新j‘ = next[j] ,则是比较si与新tj’,若此时新的tj‘ = 旧的tj,那么新的tj’ 也不会等于si,即需要继续寻找新的tj‘的next值--j'' = next[j']对应的字符tj'',即next[j'] = next[j''](前提:tj' = tj''),
所以根据上述的第3点,我们可以更加修正上面的next算法,当ti = tj'时,如果ti+1 != tj'+1,则next[i+1] = j'+1,否则若ti+1 = tj'+1,则next[i+1] = next[j'+1]。
kmp算法实现:
//求模式串T的next函数(修正方法)值并存入next数组
void getNextVal(SString T, int next[]){
next[1] = 0;
int i = 1, j = 0;
while(i < T[0]){
if(j == 0 || T[i] == T[j]){
i++; //继续比较后续字符
j++;
if(T[i] == T[j])//若除去if(T[i] == T[j]):next[i] = next[j];这2句,则得到的就是修正之前的next求解算法
next[i] = next[j];
else
next[i] = j;
}else{
j = next[j];//模式串向右滑动
}
}
}
//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。其中,T非空,1<=pos<=StrLength(S)。
int indexKMP(SString S, SString T, int pos, int next[]){
int i = pos, j = 1;
while(i <= S[0] && j <= T[0]){
if(j == 0 || S[i] == T[j]){
i++; //继续比较后续字符
j++;
}else{
j = next[j];//模式串向右滑动
}
}
if(j > T[0]){//如果j > len(T),说明模式串T与S中某子串完全匹配
return i - T[0];//因为i是已经自增过一次了,所以是i-len(T)而不是i-len(T)+1
}else
return 0;
}
void main(){
SString S ;
init(S, "ababcabcacbab");
printStr(S);
SString T;
init(T, "abcac");
printStr(T);
//int index = indexBF(S, T, 1);
//printf("index is %d\n", index);
int next[6] = {0};
getNextVal(T, next);
//打印next值
printf("next[]:");
for(int k = 1; k <= T[0]; k++)
printf("%d ", next[k]);
printf("\n");
int index = indexKMP(S, T, 1, next);
printf("index is %d\n", index);
}
运行结果
七.树
1.概念
树:由n(n>=0)个节点组成的有限集合,记作T Tree
空树:n=0,表示空树
2. 关键词
子树 根节点 叶子节点
孩子节点 父节点 子孙节点 祖先节点 兄弟节点(相对的)
总的结点数=所有的结点度数+1
结点的度:结点的子节点个数 度为0的结点为叶子节点
树的度:最大的结点的度
高度(深度):最大的层数 如上图 高度为4 a->c->h->i 取最大的路径 上面有多少个字母就有多少个层数
树的路径长度:二个结点之间的边数(要求最大)如上图 路径长度为 a->c->h->i 长度为3 有多少个(->) 就有多少个边数
3.二叉树
1.至多二棵子树 度最大为2 2.子树有顺序 ,不能换 度为0的结点的个数=度为2 的结点的个数+1;
度为0的结点个数是度为2结点个数+1
1 )满二叉树
叶子节点全在最下面一层,并且是满的 高度为h 拥有2^h-1个节点
2)完全二叉树
度为1的结点,只有左孩子 叶子节点全在最左边,且为连续 叶子节点只可能出现在最大的两层
高度为h 最多有拥有2^h-1个节点
完全二叉高度为 [log2n]+1 [log2n]表示取小于log2n的最大整数。例如,[log24] = 2,而 [log25] 结果也是 2。
二叉树的遍历
先序遍历(前序遍历) 根 左 右
中序遍历 左 根 右
后序遍历 左 右 根
层次遍历(借助队列) 左 根 右
3)树的链接存储结构和顺序存储
每个结点设置三个域:左指针域、值域、右指针域。
链接存储结点类型定义:
struct BTreeNode //结点类型定义{
ElemType data; //值域 struct BTreeNode* left; //左指针域 struct BTreeNode* right; //右指针域}
如下图为一个二叉树及其对应的链接存储结构
顺序存储结点类型定义: 根节点下标为0
typedef struct{
ElemType data//数据; int parent; //标记父节点} PTNode
typedef struct {
PTNode node[100]; int n;//用来统计节点数}PTree
如
4)二叉树的操作和运算
下面通过详细的程序展现二叉树的操作和运算,按上图二叉树作为例子,广义表表示为:A(B(C,D),E(,F(G)))
#include<stdio.h>
#include<stdlib.h>
#define QueueMaxSize 20 //定义队列数组长度
#define StackMaxSize 10 //定义栈数组长度
typedef char ElemType; //定义ElemType类型
struct BTreeNode //结点类型定义
{
ElemType data; //值域
struct BTreeNode* left; //左指针域
struct BTreeNode* right; //右指针域
};
//1、初始化二叉树
void InitBTree(struct BTreeNode** BT)
{
*BT = NULL; //把树根指针置空
}
//2、建立二叉树,采用广义表表示的输入法,如:A(B(C,D),E(,F(G)))
void CreateBTree(struct BTreeNode** BT, char* string)
{
struct BTreeNode* p;
struct BTreeNode* s[StackMaxSize]; //定义s数组作为存储根结点的指针的栈使用
int top = -1; //栈顶指针置为-1,表示空栈
int k; //k作为处理结点的标志,k=1处理左子树,k=2处理右子树
int i = 0; //用i扫描数组string中存储的二叉树广义表字符串,初值为0
*BT = NULL; //把树根指针置空,即从空树开始建立二叉树
while (string[i])
{
switch (string[i])
{
case ' ':break;
case '(':
{
if (top == StackMaxSize - 1)
{
printf("栈空间太小,需增加StackMaxSize!\n");
exit(1);
}
top++;
s[top] = p;
k = 1;
break;
}
case ')':
{
if (top == -1)
{
printf("二叉树广义表字符串错!\n");
exit(1);
}
top--;
break;
}
case ',':k = 2;break;
default:
{
p = malloc(sizeof(struct BTreeNode));
p->data = string[i];
p->left = p->right = NULL;
if (*BT == NULL)
*BT = p;
else
{
if (k == 1)
s[top]->left = p;
else
s[top]->right = p;
}
}
}//switch end
i++;
}//while end
}
//3、检查二叉树是否为空
int BTreeEmpty(struct BTreeNode* BT)
{
if (BT == NULL)
return 1;
else
return 0;
}
//4、求二叉树深度
int BTreeDepth(struct BTreeNode* BT)
{
if (BT == NULL)
return 0;
else
{
int dep1 = BTreeDepth(BT->left); //计算左子树深度
int dep2 = BTreeDepth(BT->right);//计算右子树深度
if (dep1 > dep2)
return dep1 + 1;
else
return dep2 + 1;
}
}
//5、从二叉树中查找值为x的结点,若存在则返回元素存储位置,否则返回空值(算法类似于前序遍历)
ElemType* FindBTree(struct BTreeNode* BT, ElemType x)
{
if (BT == NULL)
return NULL;
else
{
if (BT->data == x)
return &(BT->data);
else
{
ElemType* p;
if (p = FindBTree(BT->left, x))
return p;
if (p = FindBTree(BT->right, x))
return p;
return NULL;
}
}
}
//6、输出二叉树,可在前序遍历的基础上修改。采用广义表输出格式:A(B(C,D),E(,F(G)))
void PrintBTree(struct BTreeNode* BT)
{
if (BT != NULL)
{
printf("%c", BT->data); //输出根结点的值
if (BT->left != NULL || BT->right != NULL)
{
printf("(");
PrintBTree(BT->left); //输出左子树
if (BT->right != NULL)
printf(",");
PrintBTree(BT->right); //输出右子树
printf(")");
}
}
}
//7、清除二叉树,使之变为一棵空树,算法类似于后序递归遍历
void ClearBTree(struct BTreeNode** BT)
{
if (*BT != NULL)
{
ClearBTree(&((*BT)->left));//删除左子树
ClearBTree(&((*BT)->right));//删除右子树
free(*BT); //释放根结点
*BT = NULL; //置根指针为空
}
}
//8、前序遍历
void Preorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
printf("%c,", BT->data);
Preorder(BT->left);
Preorder(BT->right);
}
}
//9、中序遍历
void Inorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
Inorder(BT->left);
printf("%c,", BT->data);
Inorder(BT->right);
}
}
//10、后序遍历
void Postorder(struct BTreeNode* BT)
{
if (BT != NULL)
{
Postorder(BT->left);
Postorder(BT->right);
printf("%c,", BT->data);
}
}
//11、按层遍历
//按层遍历算法需要使用一个队列,开始时把整个树的根结点入队,然后每从队列中删除一个结点并输出该结点时,
//都把它的非空的左右孩子结点入队,当队列为空时算法结束。
//算法中,队列的最大长度不会超过二叉树中相邻两层的最大结点数,
//所以在提前在程序开始处定义最大队列长度QueueMaxSize大于队列的最大长度,就无需考虑队满溢出的事了
void Levelorder(struct BTreeNode* BT)
{
struct BTreeNode* p;
struct BTreeNode* q[QueueMaxSize];//定义队列所使用的数组空间,元素类型为指向结点的指针类型
int front = 0;
int rear = 0;
if (BT != NULL)//将树根指针入队
{
q[rear] = BT;
rear = (rear + 1) % QueueMaxSize;
}
while (front != rear)//当队列非空时执行循环
{
p = q[front];//保存队首元素
front = (front + 1) % QueueMaxSize;//删除队首元素,使队首指针指向队首元素
printf("%c,", p->data);//输出队首元素
if (p->left != NULL)//若结点存在左孩子,则左孩子结点指针入队
{
q[rear] = p->left;
rear = (rear + 1) % QueueMaxSize;
}
if (p->right != NULL)//若结点存在右孩子,则左孩子结点指针入队
{
q[rear] = p->right;
rear = (rear + 1) % QueueMaxSize;
}
}
}
//主函数
void main()
{
struct BTreeNode* bt;
char b[50];
ElemType x, *px;
InitBTree(&bt);
printf("输入二叉树广义表字符串:\n");
scanf("%s", b);
CreateBTree(&bt, b);
PrintBTree(bt);
printf("\n");
printf("前序:");
Preorder(bt);
printf("\n");
printf("中序:");
Inorder(bt);
printf("\n");
printf("后序:");
Postorder(bt);
printf("\n");
printf("按层:");
Levelorder(bt);
printf("\n");
printf("输入一个待查找的字符:\n");
scanf(" %c", &x); //格式串中的空格可以跳过任何空白符
px = FindBTree(bt, x);
if (px)
printf("查找成功:%c\n", *px);
else
printf("查找失败\n");
printf("二叉树的深度为:");
printf("%d\n", BTreeDepth(bt));
ClearBTree(&bt);
}
5)树的基本概念及存储结构
这里指度大于等于3的树,通常称为多元树或多叉树。
树的顺序存储适合满树的情况,否则将非常浪费存储空间。所以通常使用链接存储结构。
树的链接存储结构通常采用如下三种方式:
(1) 标准方式
在这种方式中,树中的每个结点除了包含有存储数据元素的值域外,还包含有k个指针域,用来分别指向k个孩子结点,或者说,用来分别链接k棵子树,其中k为树的度。结点的类型可定义为:
struct GTreeNode
{
ElemType data;//结点值域
struct GTreeNode* t[k];//结点指针域t[0]~t[k-1]
}
(2) 广义标准方式
广义标准方式是在标准方式的每个结点中增加一个指向其双亲结点的指针域。结点的类型可定义为:
struct PGTreeNode
{
ElemType data; //结点值域
struct PGTreeNode* t[k]; //结点指针域t[0]~t[k-1]
struct PGTreeNode* parent; //双亲指针域
};
(3)二叉树形式
这种表示首先将树转换为对应的二叉树形式,然后再采用二叉链表存储这棵二叉树。但无法表示任一结点中缺少前面孩子,又存在后面孩子那样的有序树。 一般情况下使用标准方式存储树。
6)树的操作和运算
树的操作和运算与二叉树类似,可以在熟练二叉树的操作和运算后进行比较。
下面用一个实例程序具体展现上图中树的操作和运算
#include<stdio.h>
#include<stdlib.h>
#define kk 3 //定义树的度
#define MS 10 //定义符号常量在建立树存储结构时指定栈空间的大小
#define MQ 10 //定义符号常量在树的按层遍历算法中指定队列空间的大小
typedef char ElemType;
struct GTreeNode //树的链接存储标准方式的结点类型定义
{
ElemType data; //结点值域
struct GTreeNode* t[kk]; //结点指针域t[0]~t[kk-1]
};
//1、建立树的存储结构,采用广义表的表示法,如:a(b(,e,f(,f)),c,d(g(k,,l),h,i))
//假定结点值仍为字符类型char,算法与二叉树类似
//设置两个栈,s栈用来存储指向根结点的指针,以便孩子结点向双亲结点链接之用,
//d栈用来存储待链接的孩子结点的序号,以便能正确地链接到双亲结点的指针域
//下面是根据广义表字符串string所给出的k叉树建立对应的存储结构
void CreateGTree(struct GTreeNode** GT, char* string)
{
struct GTreeNode* s[MS]; //MS常量为存储结点指针的栈数组长度
int d[MS];
int top = -1; //top作为两个栈的栈顶指针,初始值0表示栈空
struct GTreeNode* p; //定义p为指向树结点的指针
int i = 0, j; //用i指示扫描字符串数组a中的当前字符位置
*GT = NULL; //初始将树根指针置空
while (string[i])
{
switch(string[i])
{
case ' ':break;
case '(':
{
top++;
s[top] = p; //p指针进s栈,0进d栈,
d[top] = 0; //表明待扫描的孩子结点将被链接到s栈顶元素所指结点的第一个指针域
break;
}
case ')':top--;break; //让s和d退栈
case ',':d[top]++;break;//让待读入的孩子结点链接到s栈顶元素所指结点的下一个指针域
default:
{
p = malloc(sizeof(struct GTreeNode));
p->data = string[i];
for (j = 0; j < kk; j++) //kk表示树的深度
p->t[j] = NULL;
if (*GT == NULL) //使p结点成为树根结点(if)或链接到双亲结点对应的指针域(else)
*GT = p;
else
s[top]->t[d[top]] = p;
}
}//switch end;
i++; //准备处理下一个字符
}
}
//2、树的先根遍历
void PreRoot(struct GTreeNode* GT)//先根遍历一棵k叉树
{
int i;
if (GT != NULL)
{
printf("%c,", GT->data); //访问根结点
for (i = 0; i< kk ; i++)
PreRoot(GT->t[i]); //递归遍历每一个子树
}
}
//3、树的后根遍历
void PostRoot(struct GTreeNode* GT)
{
int i;
if (GT != NULL)
{
for (i = 0; i < kk; i++)
PostRoot(GT->t[i]); //递归遍历每一个子树
printf("%c,", GT->data); //访问根结点
}
}
//4、树的按层遍历
void LayerOrder(struct GTreeNode* GT)//按层遍历由GT指针所指向的k叉树
{
struct GTreeNode* p;
int i;
struct GTreeNode* q[MQ]; //定义一个队列用的数组q,MQ常量为队列数组长度
int front = 0, rear = 0; //定义队首指针和队尾指针,初始均置0表示空队
if (GT != NULL) //将树根指针进队
{
q[rear] = GT;
rear = (rear + 1) % MQ;
}
while (front != rear)//当队列非空时执行循环
{
p= q[front];
front = (front + 1) % MQ; //使队首指针指向队首元,素删除队首元素
printf("%c,",p->data); //输出队首元素所指结点的值
for (i = 0; i < kk; i++) //非空的孩子结点指针依次进队
if (p->t[i] != NULL)
{
q[rear] = p->t[i];
rear = (rear + 1) % MQ;
}
}
}
//5、从树中查找结点值
ElemType* FindGTree(struct GTreeNode* GT, ElemType x)
{
if (GT==NULL)
return NULL; //树空返回空指针
else
{
ElemType* p;
int i;
if (GT->data == x)
return &(GT->data); //查找成功返回结点值域的地址
for (i = 0; i < kk; i++) //向每棵子树继续查找,返回得到的值域地址
if (p = FindGTree(GT->t[i], x))
return p;
return NULL;//查找不成功返回空指针
}
}
//6、输出树,可在先根遍历的基础上修改。采用广义表输出格式:a(b(,e,f(,f)),c,d(g(k,,l),h,i))
void PrintGTree(struct GTreeNode* GT)
{
if (GT != NULL)
{
int i;
printf("%c", GT->data); //输出根结点的值
for (i = 0; i < kk; i++)//判断GT结点是否有孩子
if (GT->t[i] != NULL)
break;
if (i < kk) //有孩子时执行递归
{
printf("(");
PrintGTree(GT->t[0]); //输出第一棵树
for (i = 1; i < kk; i++)//输出其余子树
{
printf(",");
PrintGTree(GT->t[i]);
}
printf(")");
}
}
}
//7、求树深度
int GTreeDepth(struct GTreeNode* GT)
{
int i, max; //max用来保存已求过的子树中的最大深度
if (GT == NULL)
return 0;
max = 0;
for (i = 0; i < kk; i++)
{
int d = GTreeDepth(GT->t[i]); //计算一棵子树深度
if (d > max)
max = d;
}
return max + 1; //返回非空树的深度,它等于各子树的最大深度加1
}
//8、清除二叉树,使之变为一棵空树,算法类似于后序递归遍历
void ClearGTree(struct GTreeNode** GT)
{
if (*GT != NULL)
{
int i;
for (i = 0; i < kk; i++)
ClearGTree(&((*GT)->t[i]));//删除子树
free(*GT); //释放根结点
*GT = NULL; //置根指针为空
}
}
//主函数
void main()
{
char ch;
struct GTreeNode* gt = NULL;
char b[50]; //定义一个用于存放k叉树广义表的字符数组
printf("输入一棵%d叉树的广义表字符串:\n", kk);
scanf("%s", b);
CreateGTree(>, b);
printf("先根遍历结果:");
PreRoot(gt);
printf("\n");
printf("后根遍历结果:");
PostRoot(gt);
printf("\n");
printf("按层遍历结果:");
LayerOrder(gt);
printf("\n");
printf("按广义表形式输出的树为:");
PrintGTree(gt);
printf("\n");
printf("树的深度为:%d\n", GTreeDepth(gt));
printf("输入待查找的一个字符:");
scanf(" %c", &ch);//格式串中的空格可以跳过任何空白符
if (FindGTree(gt, ch))
printf("查找成功!\n");
else
printf("查找失败!\n");
ClearGTree(>);
}
4.线索二叉树
遍历二叉树是对非线性结构进行线性化操作,在得到的访问序列中,每个结点都只有一个直接前驱和一个直接后继。(除区头尾两个结点)
引入线索二叉树可以加快查找前驱与后继结点的速度,实质就是将二叉链表中的空指针改为指向前驱或后继的线索,线索化就是在遍历中修改空指针。
通常规定:对某一结点,若无左子树,将lchild指向前驱结点;若无右子树,将rchild指向后继结点。
还需要设置左右两个tag,用来标记当前结点是否有子树。
若ltag==1,lchild指向结点前驱(左边的孩子);若rtag==1,rchild指向结点后继 (右边的孩子)。
线索二叉树的存储结构如下:
typedef struct ThreadNode{
ElemType data; struct ThreadNode *lchild, *rchild; int ltag, rtag; }ThreadNode, *ThreadTree;
//BinaryThreadTree 线索二叉树
// 1.用户前序输入二叉树数据
// 2.对二叉树进行线索化 lchild ltag Data rtag rchild
// ltag/rtag 左右标记位 0:有孩子结点 link 1: 存储前驱或后继结点线索 Thread
// 3.中序迭代遍历输出二叉树数据
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
//线索存储标志位
//link(0): 指向左右孩子 | 表示当前结点的左指针(*lchild)或右指针(*rchild) 指向对应的左或右孩子
//Thread(1): 指向前驱后继线索 | 表示当前结点的左指针(*lchild)或右指针(*rchild) 指向对应的前驱或后继元素
typedef enum
{
Link, //link=0,
Thread //Thread=1
}PointerTag;
//BinaryThreadTree Node 线索二叉树结点结构
typedef struct BiThrNode
{
ElemType data;
struct BiThrNode *lchild, *rchild;
PointerTag ltag;
PointerTag rtag;
}BiThrNode, *BiThrTree; //*BiThrTree: 指向结点的指针为树
//全局变量 始终指向刚刚访问过的结点
BiThrTree pre; //前一个结点/头结点指针
//创建一颗二叉树,约定用户遵照前序遍历方式输入数据
void CreateBiThrTree(BiThrTree *T) //BiThrTree *T: T为指向树的指针 | 二级指针
{
ElemType c;
scanf("%c", &c);
if (' ' == c)
{
*T = NULL;
}
else
{
*T = (BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data = c;
(*T)->ltag = Link; //标志位赋初值,默认所有结点有左右孩子
(*T)->rtag = Link;
CreateBiThrTree(&(*T)->lchild); //递归法为左右子节点赋值
CreateBiThrTree(&(*T)->rchild);
}
}
//中序遍历线索化 | 改变无左或右孩子结点的tag标志位,使其指向前驱或后继(形成首尾相连的双向链表)
void InThreading(BiThrTree T)
{
if (T) //若不为空树
{
InThreading(T->lchild); //递归左孩子线索化
if (!T->lchild) //若该结点没有左孩子,设置Tag为Thread
{
T->ltag = Thread;
T->lchild = pre;
}
if (!pre->rchild) //*****//
{
pre->rtag = Thread;
pre->rchild = T;
}
pre = T;
InThreading(T->rchild); //递归右孩子线索化
}
}
//初始化二叉树T 开始时的头指针pre + 中序遍历线索化 + 收尾:头尾相连
//参数 *p: 指向树的头指针
//参数 T : 要操作的二叉树
void InOrderThreading(BiThrTree *p, BiThrTree T)
{
//*p = (BiThrTree)malloc(sizeof(BiThrTree));
*p = (BiThrNode *)malloc(sizeof(BiThrTree)); //头指针分配内存
(*p)->ltag = Link; //结点指针操作结点:赋值
(*p)->rtag = Thread;
(*p)->rchild = *p; //头指针右侧初始化指向自己
if (!T)
{
(*p)->lchild = *p; //空二叉树,指向自己
}
else
{
(*p)->lchild = T; //头指针左侧 指向要操作的对象
pre = *p; //初始化头指针pre
InThreading(T); //中序遍历线索化 后pre 变成最后一个结点T
pre->rchild = *p; //最后一个结点 指向 头指针
pre->rtag = Thread;
(*p)->rchild = pre; //头指针 指向 最后一个结点
}
}
//打印输出
void myvisit(char c)
{
printf("%c", c);
}
//中序遍历二叉树,迭代输出
//参数 T : 头指针
void InOrderTraverse(BiThrTree T)
{
BiThrTree p;
p = T->lchild; //从头指针位置迭代输出
while (p != T) //若为结点非空
{
while (p->ltag == Link) //输出最下层左结点数据
{
p = p->lchild;
}
myvisit(p->data);
while (p->rtag == Thread && p->rchild != T) //输出右结点下一个结点
{
p = p->rchild;
myvisit(p->data);
}
p = p->rchild; //迭代
}
}
int main()
{
BiThrTree P, T = NULL;
CreateBiThrTree(&T); //约定用户前序输入二叉树数据
InOrderThreading(&P, T); //线索化二叉树
printf("中序遍历输出结果为:");
InOrderTraverse(P); //中序遍历二叉树,迭代输出
printf("\n");
return 0;
}
八.图
1.图的定义
在图中的数据元素通常称为顶点,图G(Graph)是由顶点集合(vertex)及顶点之间的关系集合(称为边edge或弧arc)组成的一种数据结构。记为G=(V,E)。
2.图的结构
图的存储结构比较复杂,其复杂性主要表现在:
任意顶点之间可能存在联系,无法以数据元素在存储区中的物理位置来表示元素之间的关系。
图中顶点的度不一样,有的可能相差很大,若按度数最大的顶点设计结构,则会浪费很多存储单元,反之按每个顶点自己的度设计不同的结构,又会影响操作。
图的常用的存储结构有:邻接矩阵、邻接链表、十字链表、邻接多重表和边表。
3.术语
有向图:在一个图G中,任意两个顶点构成的偶对(vi,vj)∈E都是有序的,即两点相连形成的边都是有方向的,则称该图为有向图(Digraph)。
有向边(弧):如果顶点vi和vj间的边有方向,则称该边为有向边(或称为弧Arc)。用有序偶对表示:<vi,vj>。
线性表可以是空表,树可以是空树,但图必须得有顶点
弧头、弧尾:在无向图中,任意两个顶点之间的连线称为边,并且不区分首尾;在有向图中,任意两个顶点之间的连线称为弧,并且,有向图的弧需区分弧头和弧尾。例如:将顶点vi和vj之间的连线记为有序偶对<vi,vj >,其中顶点vi称为初始点(弧尾Tail),即弧的射出端,就是不带箭头的一端。顶点vj称为终端点(或弧头Head),即弧的射入端,就是带着箭头的一端。 有箭头一端的为弧头
无向图:在一个图G中,任意两个顶点构成的偶对(vi,vj)∈E都是无序的,即两点相连形成的边都是没有方向的,则称该图为无向图(Undigraph)。
无向边:如果顶点vi和vj间的边没有方向,则称该边为无向边(Edge)。用无序偶对表示:(vi,vj)。
简单图:一个图满足不存在重复的边和不存在顶点到自身的边,则称为简单图,数据结构里头讨论的是简单图。
多重图:概念与简单图相反,俩顶点之间边数多于一条,且允许通过一条边与自身关联
完全图:如果无向图中任意两个顶点间都存在边,则称之为无向完全图(Completed Graph)。在一个含有n个顶点的无向完全图中,边数为n(n-1)/2条。如果有向图中任意两个顶点间都存在方向互为相反的两条弧,则称之为有向完全图。在一个含有n个顶点的有向完全图中,边数为n(n-1)条
稠密图、稀疏图:当一个图接近完全图时,称之为稠密图(Dense Graph);相反地,当一个图中含有较少的边或弧时,则称之为稀疏图(Sparse Graph)。
权、网:在边或者弧上的数据信息称为边的权(Weight)。权值可以表示从一个顶点到另一个顶点的距离、耗费、时间或者价格等。带权的图称为网(Network)。
子图:若有两个图G1和G2,其中,G1=(V1,E1),G2=(V2,E2),且满足如下条件:V2⊆V1,E2⊆E1
即V2为V1的子集,E2为E1的子集,则称图G2为图G1的子图。
并非任何子集都能构成G的子集
邻接点和度:对于无向图,假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点;和顶点v 关联的边的数目定义为v的度。记为ID(v)。无向图不区分入度和出度。对于有向图,由于弧有方向性,则有入度和出度之分。顶点的出度(OutDegree)是以顶点v 为弧尾的弧的数目,记为OD(v);顶点的入度(InDegree)是以顶点v为弧头的弧的数目,记为ID(v);顶点的度记为TD(v),有TD(v)= OD(v)+ ID(v)。
在具有n个顶点e条边的无向图中,TD(v)=2*e 在具有n个顶点e条边的有向图中,ID(v)=OD(v)=e
路径、路径长度:顶点vi到顶点vj的路径(Path)是指从顶点vi到顶点vj之间所经历的顶点序列vi,vi,1…vi,m,vj,其中(vi,vi,1)(vi,m,vj)和(vi,j,vi,j+1)∈E,1≤j≤m-1都是图中的边。路径的长度是路径上的边或弧的数目
简单路径:顶点序列中顶点不重复出现的路径,称为简单路径
回路:若顶点序列中第一个顶点和最后一个顶点相同,则称该路径为回路或环(Cycle);
简单回路:若顶点序列中除第一个顶点和最后一个顶点相同外,其余顶点不重复,则称该回路为简单回路或者简单环。
连通图:无向图G中,如果从顶点vi到顶点vj有路径,则称顶点vi和vj是连通(不是连接)的。如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称图G为连通图(Connected Graph)。
连通分量:在无向图中,在满足连通条件时,尽可能多地包含原图中的顶点和这些顶点之间的边的连通子图(极大连通子图)称为该图的连通分量(Connected Component);连通图的连通分量是它本身,非连通图的连通分量可能为多个。
极大连通子图:尽可能多的包含顶点和边,且仍然连通
极小连通子图:添加一条边就为环(包含所有顶点,最少的边,但仍连通)
如果一个图由n个顶点,并且小于n-1条边,必为非连通图
强连通图:有向图G中,如果从vi到vj有路径,则称顶点vi和顶点vj是连通的;若图中任意两个顶点之间都存在两条互为反方向的路径,即从vi到vj及从vj到vi都有路径,则称此有向图为强连通图。
强连通分量:有向图中的极大连通子图称作该有向图的强连通分量。
生成树:连通图G的生成树,是包含G的全部n个顶点的一个极 小连通子图,该极小连通子图有(n-1)条边。如果在一棵生成树上添加一条边,必定构成一个环,因为这条边的出现使得它依附的那两个顶点之间有了第二条路径。一棵有n个顶点的生成树有且仅有(n-1)条边。如果一个图有n个顶点和小于(n-1)条边,则是非连通图。如果它多于(n-1)条边,则一定有环。
注意有(n-1)条边的图不一定是生成树。
生成森林:如果一个有向图有且仅有一个顶点的入度为0,其他顶点的入度均为1,则这个图是一棵有向树。当一个有向图有多个顶点的入度为0时,它的生成森林则由多棵有向树构成。这个生成森林含有图中全部的顶点和相应的弧。
4.存储结构
1)顺序存储
邻接矩阵(Adjacency Matrix)是用两个数组来表示图,一个数组是一维数组,存储图中顶点的信息,另一个数组是二维数组,即矩阵,存储顶点之间相邻的信息,也就是边(或弧)的信息,这是邻接矩阵名称的由来。
结点描述
typedef struct
{
VertexType vexs[MaxVertexNum]; //顶点
int edge[MaxVertexNum][MaxVertexNum]; //边
int vexnum; //图的当前顶点数
int arcnum; //图的弧数
//int kind; //种类} MGraph;
邻接矩阵表示法的特点
因为无向图的邻接矩阵具有对称性,一定是一个对称矩阵,所以可以采取压缩存储的方式只存储矩阵的上三角(或下三角)矩阵元素。
无向图(网)的邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi)。
有向图(网)的邻接矩阵的第i行非零元素(或非∞元素)的个数正好是第i个顶点的出度OD(vi)。
有向图(网)的邻接矩阵的第i列非零元素(或非∞元素)的个数正好是第i个顶点的入度ID(vi)。
图G邻接矩阵为A,An元素An[i][j]等于由顶点i到顶点j的长度为n的路径的数目
2)链式存储
邻接表(Adjacency List)是图的一种顺序存储与链式存储结合的存储方法
对于图G中的每个顶点Vi,将所有邻接于Vi的顶点Vj链成一个单链表,这个单链表就称为顶点Vi的邻接表
再将所有顶点的邻接表表头放到数组中,就构成了图的邻接表。
结点描述
typedef struct Arcnode //边表结点{
int adjvex;//vertexIndex //该弧所指向的顶点的位置
struct Arcnode *nextarc; //指向下一个边表结点
InfoType weight;//info //用于存储权值,对于非网图可以不需要//网的权值 }ArcNode;
typedef struct Vnode //顶点结点{
VertexType data; //顶点的信息
ArcNode *firstarc;//adj //指向第一个邻接点
}VNode,AdjList[maxVertexNum]; //取了俩别名 普通类型、数组类型
typedef struct ALGraph{
AdjList vertices;//顶点数组
int vexnum;//顶点数目
int arcnum;//边数目
int kind;//种类 }ALGraph;
邻接表(不唯一)存储法的特点
1.无向图/网
①第i 个链表中结点的数目为第i个顶点的度。
②所有链表中结点的数目的一半为图中边的数目。
③占用的存储(空间)单元数目为n+2e。(n为顶点数,e为边数)
2.有向图/网
①邻接表中,第i个链表中结点的数目为顶点i的出度。逆邻接表中,第i个链表中结点的数目为顶点i的入度。
②所有链表中结点的数目为图中弧的数目。
③占用的存储(空间)单元数为n+e。(n为顶点数,e为弧数)
注: 链式存储
十字链表(有向图/网)
data存放数据,firstin和firstout两个域分别指向以该顶点为弧头和弧尾的第一个弧结点
尾域tailvex和头域headvex分别指示弧尾和弧头这两个顶点在图中的位置,链域hlink指向弧头相同的下一个弧,链域tlink指向弧尾相同的下一条弧,info为相关信息(权重之类),这样弧头相同的弧在同一个链表上,弧尾相同的弧在同一个链表
邻接多重表(无向图/网)
data域存储数据,firstedge指示第一条依附于该顶点的边
mark为标识域,标记该条边是否搜索过,ivex和jvex为该边依附的两个顶点在图中的位置,ilink指向下一条依附于顶点ivex的边,jlink指向下一条依附于顶点jvex的边,info相关信息