1、数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机的符号集合。
2、数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。
3、数据项:一个数据元素可以由若干个数据项组成。数据项是数据不可分割的最小单位。
4、数据对象:是性质相同的数据元素的集合,是数据的子集。
5、数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
1、逻辑结构:是指数据对象中数据元素之间的相互关系。
(1)集合结构:集合结构中的元素除了同属于同一个集合外,它们之间没有其他关系。
(2)线性结构:线性结构中的数据元素之间是一对一的关系。
(3)树形结构:树形结构是数据元素之间存在一种一对多的层次关系。
(4)图形结构:图形结构的数据元素是多对多的关系。
2、物理结构:是指数据的逻辑结构在计算机中的存储形式。
(1)顺序存储结构;;是把数据元素存放在地址连续的存储单元中,其数据间的逻辑关系和物理关系是一致的。
(2)链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
1、数据类型:是指一组性质相同的值得集合及定义在此集合上的一些操作的总称。
2、在C语言中,按照取值的不同,数据类型可以分为两类:
(1)原子类型:是不可以再分解的基本类型,包括整型、实型、字符型等。
(2)结构类型:由若干类型组合而成,是可以再分解的。例如,整型数组是由若干整型数据组成的。
3、抽象是指抽取出事物的普遍性的本质。
4、抽象数据类型(ADT):是指一个数据模型及定义在该模型上的一组操作。
5、“抽象”的意义在于数据类型的属性抽象特性。抽象数据类型体现了数据程序设计中问题分解、抽象和信息隐藏的特性。
6、描述抽象数据类型的标准格式:
ADT 抽象数据类型名
Data
数据元素之间的逻辑关系的定义
Operation
操作1
初始条件
操作结果描述
操作2
……
操作n
……
endADT
1、算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
1、输入输出:算法具有零个或多个输入,算法至少有一个或多个输出。
2、有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每个步骤在可接受的时间内完成。
3、确定性:算法的每一步骤都具有确定的意义,不会出现二义性。
4、可行性:算法每一步骤都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
1、正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
2、可读性:算法的设计的另一目的是为了便于阅读,理解和交流。
3、健壮性:但数据数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结构。
4、时间效率高和存储量低:设计算法应尽量满足时间效率高和存储量的需求。
1、事后统计方法
2、事前分析估算方法
1、在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量及。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称为算法的渐进时间复杂度,简称为时间复杂度。其中f(n)问题规模n的某个函数。
2、大O记法:用大写O()来体现算法的时间复杂度的记法。
3、推导大O阶方法:
(1)用常数1取代运行时间中额所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且不是1,则去除与这个项相乘的常数。
执行次数函数 | 阶 | 非正式术语 |
---|---|---|
12 | O(1) | 常数阶 |
2n+3 | O(n) | 线性阶 |
3n2+2n+1 | O(n2) | 平方阶 |
5log2n+20 | O(logn) | 对数阶 |
2n+3nlog2n | O(nlogn) | nlogn阶 |
6n3+2n2+3n+4 | O(n3) | 立方阶 |
2n | O(2n) | 指数阶 |
常见的时间复杂度所耗费时间从小到大依次是:
O(1)
1、最坏情况运行时间是一种保证,那就是运行时间将不会在坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
2、平均运行时间是所有情况中最有意义的,,因为它是期望的运行时间。
3、一般在没有特殊说明的情况下,都是最坏时间复杂度。
1、算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的对魔,f(n)为语句关于n所占存储空间的函数。
2、通常,我们都是使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。当不用限定地使用“复杂度”时,通常是指时间复杂度。
1、线性表(List):零个或多个数据元素的有限序列。
2、数学语言定义:若线性表记为(a1,……,ai-1,ai,ai+1,……,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,3,……,n-1是ai有且仅有一个直接后继,当i=2,3,……,n是,ai有且仅有一个直接前驱。
3、空表:线性表元素的个数n(n>=0)定义为线性表的长度,当0时,成为空表
线性表的抽象数据类型定义如下:
ADT 线性表(List)
Data
线性表是数据对象集合为{
a1,a2,……,an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList(*L); 初始化操作,建立一个空的线性表L。
ListEmpty(L); 若线性表为空,返回true,否则返回false。
ClearList(*L); 将线性表清空。
GetElem(L,i,*e); 将线性表L中的第i个位置的元素值返回给e。
LocateElem(L,e); 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号表示成功;否则,返回0表示失败。
ListInsert(*L,i,e); 在线性表L中的第i个位置插入新元素e。
ListDelete(*L,i,*e); 删除线性表L中第个i个位置元素,并用e返回其值。
ListLength(L); 返回线性表L的元素个数。
endADT
实现两个线性表集合A和B的操作。
/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List *La,List *Lb)
{
int La_len,Lb_len,i;
ElemType e; /*声明与La和Lb相同的数据元素e*/
La_len = ListLength(La); /*求线性表的长度*/
Lb_len = ListLength(Lb);
for(i=1; i<Lb_len; i++)
{
GetElem(Lb, i, e); /*取出Lb中第i个数据元素赋值给e*/
if(!LoacteElem(La,e)) /*La中不存在和e相同数据元素*/
ListInsert(La, ++La_len, e); /*插入*/
}
}
1、顺序存储定义:线性表的顺序存储结构,指的是用一段连续的存储单元依次存储线性表的数据元素。
2、顺序存储方式:可以用C语言的一维数组来实现顺序存储结构。
线性表的顺序存储结构代码。
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef ine ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct
{
ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
3、数组长度与线性表长度的区别
(1)数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般不变的。
(2)线性表的长度是线性表中的元素个数,随着线性表插入和删除操作的进行,这个量是变化的。
(3)在任何时刻,线性表的长度应该小于等于数组的长度。
4、地址计算方法
(1)线性表的数是从1开始数的,可C语言中的数组确实从0开始第一个下标的,于是线性表的第i个元素是要存储在数组下标为-1的位置。
(2)存储器中每个存储单元都有自己的编号,这个编号称为地址。
(3)假设每个数据元素占用的是c个存储单元,那么线性表中的第i+1个数据元素的存储位置和第i个数据的存储位置满足下列关系
LOC(a(i+1))=LOC(ai)+c
所以对于第i个数据元素ai的存储位置可以有a1推算得出:
LOC(ai)=LOC(ai)+(i-1)*c
5、存取结构:分为随机存取和非随机存取(又称顺序存取)
(1)随机存取就是直接存取,可以通过下标直接访问的那种数据结构与存储结构位置无关,例如数组。
(2)非随机存取就是顺序存取,不能通过下标访问,只能按照存储顺序存取,与存储位置有关,例如链表。
(3)顺序存取就是存取第N个数据时,必须先访问前(N-1)个数据 (list),随机存取就是存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作 (array)。
(4)顺序表是顺序存储,随机存取的结构; 链表是随机存储,顺序存取的结构; 注意储存和存取的区别。
1、获得元素操作
#define OK 1
#define ERROR 0
#define TRUE 1
#define FLASE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表已存在,1<=i<=ListLength(L)*/
/*操作条件:用e返回L中第i个数据元素的值*/
Status GetElem(SqList L,int i, ElemType *e)
{
if(L.length==0 || i<1 || i>L.length)
return ERROR;
*e=L.data[i-1];
return OK;
}
2、插入操作
插入算法的思路:
(1)如果插入位置不合理,抛出异常;
(2)如果线性表长度大于数组长度,则抛出异常或动态增加容量;
(3)从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
(4)将要插入的元素填入位置i处;
(5)表长加1.
实现代码如下:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(Sqlist *L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE) /*顺序线性表已满*/
return ERROR;
if(i<1 || i>L->length+1) /*当i不在范围内时*/
return ERROR;
if(i<=L->length) /*若插入数据位置不在表尾*/
{
for(k=L->length-1;k>=i-1;k--) /*将要插入位置后的数据元素向后移动一位*/
L->data[k+1]=L->data[k];
}
L->data[i-1]=e; /*将新元素插入*/
L->length++;
return OK;
}
3、删除操作
删除算法的思路:
(1)如果删除位置不合理,抛出异常;
(2)取出删除元素;
(3)从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
(4)表长减1。
实现代码如下:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L中第i个位置的数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(Sqlist *L,int i,ElemType *e)
{
int k;
if(L->length==0) /*顺序线性表为空*/
return ERROR;
if(i<1 || i>L->length) /*删除位置不正确*/
return ERROR;
*e=L->data[i-1];
if(i<L->length) /*如果删除不是最后位置*/
{
for(k=i;k<L->length;k++) /*将删除位置后继元素前移*/
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
4、线性表顺序存储结构的优缺点
优点:
(1)无需为表中元素之间的逻辑关系而增加额外的存储空间;
(2)可以快速地存取表中任一位置的元素。
缺点:
(1)插入和删除操作需要移动大量元素;
(2)当线性表长度变化较大时,难以确定存储空间的容量;
(3)造成存储空间的“碎片”。
1、数据域,指针域,结点
为了表示每个数据元素ai与其直接连接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
2、单链表
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,……,an)的链式存储结构,因此此链表的每个结点中只包含一个指针域,所以叫单链表。线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
3、头指针
链表中第一个结点的存储位置叫做头指针。
4、头结点
单链表的第一个结点前附设一个结点,称为头结点。头结点可以不存储任何信息,也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。
头指针
(1)头指针是指指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
(2)头指针具有表示作用,所有常用头指针冠以链表的名字。
(3)无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
头结点
(1)头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)。
(2)有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。
(3)头结点不一定是链表必须要素。
6、若线性表为空表,则头结点的指针域为“空”
存储示意图表示单链表
若带有头结点的单链表
空链表
7、单链表中,C语言中可用结构指针来描述
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /*定义LinkList*/
结点由存放数据元素的数据域存放后继结点地址的指针域组成。
单链表实现获取第i个元素的数据的操作GetElem,单链表的结构中没有定义表长,不能事先知道要循环多少次,其主要核心思想是“工作指针后移”。
获得链表第i个数据的算法思路:
1、声明一个结点p指向链表的第一个结点,初始化j从1开始;
2、当j
3、若到链表末尾p为空,则说明第i个元素不存在;
4、否则查找成功,返回结点p的数据。
实现代码算法如下:
/*初始条件:顺序线性表L已存在*,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p; /*声明一结点p*/
p=L->next; /*让p指向链表L的第一个结点*/
j=1; /*j为计数器*/
while(p&&j<i) /*p不为空或者计数器j还没有等于i时,循环继续*/
{
p=p->next; /*让p指向下一个结点*/
++j;
}
if(!p||j>i)
return ERROR; /*第i个元素不存在*/
*e=p->data; /*取第i个元素的数据*/
return OK;
}
1、单链表的插入
单链表第i个数据插入结点的算法思路:
(1)声明一结点p指向链表的第一个结点,初始化j从1开始;
(2)当j
(3)若到链表末尾p为空,则说明第i个元素不存在;
(4)否则查找成功,在系统中生成一个空结点s;
(5)将数据元素e赋值给s->data;
(6)单链表的插入标准语句s->next=p->next, p->next=s;
(7)返回成功。
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,长度加1*/
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p&&j<i) /*寻找第i个结点*/
{
p=p->next;
++j;
}
if(!p||j>i)
return ERROR; /*第i个元素不存在*/
s=(LinkList)malloc(sizeof(Node)); /*生成新结点*/
s->data=e;
s->next=p->next; /*将p的后继结点赋值给s的后继*/
p->next=s; /*将s赋值给p的后继*/
return OK;
}
2、单链表的删除
单链表第i个数据删除结点的算法思路:
(1)声明一结点p指向链表第一个结点,初始化j从1开始;
(2)当j
(3)若到链表末尾p为空,则说明第i个元素不存在;
(4)否则查找成功,将欲删除的结点p->next赋值给q;
(5)单链表的删除标准语句p->next=q->next;
(6)将q结点中的数据赋值给e,作为返回;
(7)释放q结点;
(8)返回成功。
实现代码算法如下:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next&&j<i) /*遍历寻找第i个元素*/
{
p=p->next;
++j;
}
if(!(p->next)||j>i)
return ERROR; /*第i个元素不存在*/
q=p->next;
p->next=q->next; /*将q的后继赋值给p的后继*/
*e=q->data; /*将q结点中的数据给e*/
free(q); /*让系统回收此结点,释放内存*/
return OK;
}
单链表整表创建的算法思路:
1、声明一结点p和计数器变量i;
2、初始化一空链表L;
3、让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4、循环:
(1)生成一新结点赋值给p;
(2)随机生成一数字赋值给p的数据域p->data;
(3)将p插入到头结点与前一新结点之间。
头插法:
/*随机产生n个元素的值,建立带有头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L,int n)
{
LinkList p;
int i;
srand(time(0)); /*初始化随机数种子*/
*L=(LinkList)malloc(sizeof(Node));
(*L)->next=NULL; /*先建立一个带头结点的单链表*/
for(i=0;i<n;i++)
{
p=(LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data=rand()%100+1;
p->next=(*L)->next;
(*L)->next=p; /*插入到表头*/
}
}
尾插法:
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time((0));
*L=(LinkList)malloc(sizeof(Node));
r=*L; /*r为指向尾部的结点*/
for(i=0;i<n;i++)
{
p=(Node *)malloc(sizeof(Node)); /*生成新结点*/
p->data=rand()%100+1; /*随机生成100以内的数字*/
r->next=p; /*将表尾终端结点的指针指向新结点*/
r=p; /*将当前的新结点定义为表尾终端结点*/
}
r->next=NULL; /*表示当前链表结束*/
}
单链表整表删除的算法思路如下:
1、声明一结点p和q;
2、将第一个结点赋值给p;
3、循环:
(1)将下一个结点赋值给q;
(2)释放p;
(3)将q赋值给p。
实现代码算法如下:
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next; /*p指向第一个结点*/
while(p) /*没到表尾*/
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL; /*头结点指针域为空*/
return OK;
}
1、若线性表需要频繁查找,很少进行插入操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
2、当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不用考虑存储空间问题。而如果事先知道线性表的大致长度,这种用户顺序存储结构效率会高很多。
1、静态链表(游标实现法),用数组来代替指针描述的链表叫做静态链表。首先我们让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每一个下标都对应一个data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。
/*线性表的静态链表存储结构*/
#define MAXSIZE 1000 /*假设链表的最大长度1000*/
typedef struct
{
ElemType data;
int cur; /*游标,为0是表示无指向*/
} Component,StaticLinkList(MAXSIZE);
2、对数组第一个和最后一个元素作为特殊元素处理,不存数据。通常把未使用的数组称为备用链表。而数组第一元素,即下标为0的元素的cur就存放备用链表中的第一个结点的下标;而数组最后一个元素的cur则存放第一个有数值的元素的下标,单链表中的头结点作用,当整个链表为空时,则为0。
此时图示相当于初始化的数组状态,见下面代码:
/*将一维数组space中各分量链成一备用链表*/
/*space[0].cur 为头指针,"0"表示空指针*/
Status InitList(StaticLinkList space)
{
int i;
for(i=0;i<MAXSIZE-1;i++)
space[i].cur=i+1;
space[MAXSIZE-1].cur=0; /*目前静态链表为空,最后一个元素的cur为0*/
return OK;
}
3、静态链表的插入操作
静态链表中药解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要是时申请,无用时释放。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点为待插入的新结点。
/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
int i=spacep[0].cur; /*当前数据第一元素的cur存的值,就是要返回的第一个备用空闲的下标*/
if(space[0].cur)
space[0].cur=space[i].cur; /*由于要拿出一个分量来使用了,所以我们就得把它的下一个分量用来做备用*/
return i;
}
Status LinkInsert(StaticLinkList L,int i,ElemType e)
{
int j,k,l;
k=MAX_SIZE-1; /*注意k首先是最后一个元素的下标*/
if(i<1||i>ListLength(L)+1)
return ERROR;
j=Malloc_SSL(L); /*获得空闲分量的下标*/
if(j)
{
L[j].data=e; /*将数据赋值给此分量的data*/
for(l=1;l<i-1;l++) /*找到第i个元素之前的位置*/
k=L[k].cur;
L[j].cur=L[k].cur /*把第i个元素之前的cur赋值给新元素的cur*/
L[k].cur=j; /*把新元素的下标赋值给第i个元素之前元素的cur*/
return OK;
}
return ERROR;
}
4、静态链表的删除操作
/*删除在L中第i个数据元素e*/
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i<1||i>ListLength(L))
return ERROR;
k=MAX_SIZE-1;
for(j=1;j<=i-1;j++)
k=L[k].cur;
j=L[k].cur;
L[k].cur=L[j].cur;
Free_SSL(L,j);
return OK;
}
/*将下标为k的空闲结点回收到备用链表*/
void Free_SSL(StaticLinkList spcace,int k)
{
space[k].cur=space[0].cur; /*把第一个元素cur值赋给要删除的分量cur*/
space[0].cur=k; /*把要删除的分量下标赋值给第一个元素的cur*/
}
5、静态链表的长度
/*初始条件:静态链表L已存在。操作结果:返回L中数据元素个数*/
int ListLength(StaticLinkList L)
{
int j=0;
int i=L(MAXSIZE-1).cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
1、将单链表中终端结点的指针端由空指针改为指向头指针,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
2、循环链表带头结点的空链表
3、对于非空的循环链表
4、循环链表和单单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。
5、将两个循环链表合并成一个表时,下面两个循环链表的尾指针分别是rearA和rearB。
p=rearA->next; /*保存A表的头结点*/
rearA->next=rearB->next->next; /*将本是指向B表的第一个结点(不是头结点,赋值给rearA->next)*/
rearB->next=p; /*将原A表的头结点赋值给rearB->next*/
free(p); /*释放p*/
1、双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; /*直接前驱指针*/
struct DulNode *next; /*直接后继指针*/
}DulNode,*DuLinkList;
1、栈是限定仅在表尾进行插入和删除操作的线性表。
2、允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
3、栈的插入操作,叫作进栈,也称压栈、入栈。
4、栈的删除操作,叫作出栈,也有的叫作弹栈。
ADT 栈(stack)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitStack(*S):初始化操作,建立一个空栈S。
DestroyStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,返回true,否则返回false。
GetTop(*S,e):若栈存在且非空,用e返回S的栈顶元素。
Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT
1、栈的顺序存储结构
typedef int SElemType;
typedef struct
{
SElemType data[MAXSIZE];
int top; /*用于栈顶指针*/
}SqlStack;
若现在有一个栈,StackSize是5,则栈普通情况、空栈和栈满的情况:
2、栈的顺序存储结构——进栈操作
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *s, SElemType e)
{
if(S->top == MAXSIZE-1) /*栈满*/
{
return ERROR;
}
S->top++; /*栈顶指针加1*/
S->data[S->top]=e; /*将新插入元素赋值给栈顶元素*/
return OK;
}
3、栈的顺序存储结构——出栈操作
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR*/
Status Pop(SqStack *S, SElemType *e)
{
if(S->top == -1)
{
return ERROR;
}
*e=S->data[S->top]; /*将要删除的栈顶元素赋值给e*/
S->top--; /*栈顶指针加一*/
return OK;
}
1、数组有两个端点,两个栈有两个栈底,让第一个栈的栈底为数组的始端,即下标为处,另一个栈为栈的末端,即下标数组长度n-1处。这样两个栈如果增加元素,就是两端点向中间延伸。
两栈共享空间的结构的代码:
/*两栈共享空间结构*/
typedef struct
{
SElemType data[MAXSIZE];
int top1; /*栈1栈顶指针*/
int top2; /*栈2栈顶指针*/
}SqDoubleStack;
对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要判断是栈1还是栈2的栈号参数stackNumber。插入元素的代码如下:
/*插入元素e为新的栈顶元素*/
Status Push(SqDouble *S, SElemType e, int stackNumber)
{
if(S->top1+1==S->top2) /*栈已满,不能再push新元素了*/
return ERROR;
if(StackNumber==1) /*栈1有元素进栈*/
S->data[++S->top1]=e; /*若栈1则先top1+1后给数组元素赋值*/
else if (StackNumber==2) /*栈2有元素进栈*/
S->data[--S->top2]=e; /*若栈2则先top2-1后给数组元素赋值*/
return OK;
}
对于;两栈共享空间的pop方法,参数就只判断栈1栈2的参数stackNumber,代码如下:
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR*/
Status Pop (SqDoubleStack *S, SElemType *e, int stackNumber)
{
if(stackNumber==1)
{
if(S->top1==-1)
return ERROR; /*说明栈1已经是空栈,溢出*/
*e=S->data[S->top1--]; /*将栈1的栈顶元素出栈*/
}
else if (stackNumber==2)
{
if(S->top2==MAXSIZE)
return ERROR; /*说明栈2已经是空栈,溢出*/
*e=S->data[S->top2++]; /*将栈2的栈顶元素出栈*/
}
return OK;
}
1、栈的链式存储结构,简称为链栈。
对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。
链栈的结构代码如下:
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
2、栈的链式存储结构——进栈操作
对于链栈的进栈push操作,假设元素值为e的新结点是s,top为栈顶指针。
/*插入元素e为新的栈顶元素*/
Status Push(LinkStack *S, SElemType e)
{
LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top;
S->top=s;
S->count++;
return OK;
}
3、栈的链式存储结构——出栈操作
至于链栈的出栈pop操作,也是简单的三句操作。假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可。
/*若栈不空,则删除S的栈顶元素,用e返回其值*/
Status Pop(LinkStack *S, SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /*将栈顶元素赋值给p*/
S->top=S->top->next; /*使得栈顶指针下移一位,指向后一结点*/
free(p); /*释放结点p*/
S->count--;
return OK;
}
4、如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,它的变化在可控范围内,建议使用顺序栈会更好一些。
1、斐波那契数列实现
打印出前40位斐波那契数列数
(1)常规的迭代实现
int main()
{
int i;
int a[40];
a[0]=0;
a[1]=1;
printf("%d ",a[0]);
printf("%d ",a[1]);
for(i=2;i<40;i++)
{
a[i] = a[i-1] + a[i-2];
printf("%d ",a[i]);
}
return 0;
}
(2)递归实现
/*斐波那契的递归函数*/
int Fbi(int i)
{
if(i<2)
return i == 0 ? 0 : 1;
return Fbi(i-1) + Fbi(i-2);
}
int main()
{
int i;
for(i=0; i<40; i++)
{
printf("%d ", Fbi(i));
}
return 0;
}
2、递归定义
(1)把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。
(2)每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。
(3)递归和迭代的区别是:迭代使用的是循环结构,递归使用的是选择结构。
1、后缀(逆波兰)表示法定义
一种不需要括号的后缀表达式,对于“9+(3-1)×3+10÷2”,用后缀表示法应该是“9 3 1 - 3 * + 10 2 / +”,这样的表达式称为后缀表达式,叫后缀的原因在于所有的符号都是在要运算数字的后面出现。
2、后缀表达式的计算结果
规则:从左到右遍历表达式的每个数字和符号,遇到的是数字就进栈,遇到的是符号,就将处于栈顶的两个数字出栈,进行运算,运行结果进栈,一直到最终获得结果。
3、中缀表达式转后缀表达式
(1)标准四则运算表达式叫做中缀表达式。
(2)中缀表达式“9+(3-1)×3+10÷2”转化为后缀表达式“9 3 1 - 3 * + 10 2 / +”。
(3)规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
1、队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
2、队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
3、队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,则返回true,否则返回false。
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数
endADT
1、队列顺序存储的不足
假设一个队列有n个元素,则顺序存储队列需建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。
入队列操作是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)。
队列元素的出列是在队头,即下标0的位置,队列中所有的元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时时间复杂度为O(n)。
如果不去限制队列元素必须存储在数组的前n个单元这一条件,出队的性能就大大增加。队头不需要一定在下表为0的位置。
为了避免当只有一个元素是,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
假设是长度为5的数组,初始状态,front与rear指针均指向下标为0的位置。然后入队a1,a2,a3,a4,front指针依然指向下标为0的位置,而rear指针指向下标为4的位置。
出队a1,a2,则front指针指向下标为2的位置,rear不变。再入对a5,此时front指针不变,rear指针移动到数组之外。
假设这个队列的总个数不超过5个,但目前如果接着入队的话,因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我们的队列在下标为0和1的地方还是空闲的,这种现象叫做“假溢出”。
2、循环队列定义
所以解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列这种头尾相接的顺序存储结构称为循环队列。
接着入队a6,将它放置与下标0处,rear指针指向下标为1处,若再入队a7,则rear指针就与front指针重合,同时指向下标为2的位置。
空队列时,front等于rear,现在当队列满时,也是front等于rear。判断队列是空还是满:
(1)设置一个标志变量
(2)当队列满时,修改其条件,保留一个元素空间。队列满时,数组中还存有一个空闲单元。
由于rear可能比front大,也可能比front小,所以尽管它们相差一个位置是就是满的情况,但也可能是相差整整一圈。所以队列的最大尺寸为QueueSize,那么队列满的条件是(rear+1)%QueueSize == front。
另外,当rear>front时,此时队列的长度为rear-front。但当rear
循环队列的顺序存储结构代码如下:
typedef int QElemType;/*QElemType类型根据实际情况而定,这里假设为int*/
/*循环队列的顺序存储结构*/
typedef struct
{
QElemType data[MAXSIZE];
int front; /*头指针*/
int rear; /*尾指针,若队列不空,指向队列尾元素的下一位置*/
}SqQueue;
循环队列的初始化代码如下:
/*初始化一个空队列*/
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
循环队列求队列长度代码如下:
/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
循环队列的入队操作代码如下:
/*若队列未满,则插入元素e为Q新的队尾元素*/
Status EnQueue(SqQueue *Q, QElemType e)
{
if((Q->rear+1)%MAXSIZE == Q->front) /*队列满的判断*/
return ERROR;
Q->data[Q->rear]=e; /*将元素e赋值给队尾*/
Q->rear=(Q->rear+1)%MAXSIZE; /*rear指针向后移一位置,若到最后则转到数组头部*/
return OK
}
循环队列的出队操作代码如下:
/*若队列不空,则删除Q中队头元素,用e返回其值*/
Status DeQueue(SqQueue *Q, QElemType *e)
{
if(Q->front == Q->rear) /*队列空的判断*/
return ERROR;
*e=Q->data[Q->front]; /*将队头元素赋值给e*/
Q->front=(Q->front+1)%MAXSIZE; /*front指针向后移一位置,若到最后则转到数组头部*/
return OK
}
1、队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
空队列时,front和rear都指向头结点。
链队列的结构为:
typedef int QElemType; /*QElemType类型根据实际情况而定,这里假设为int*/
typedef struct QNode /*结点结构*/
{
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct /*队列的链表结构*/
{
QueuePtr front,rear; /*队头、队尾指针*/
}LinkQueue;
2、队列的链式存储结构——入队操作
入队操作是,其实就是在链表尾部插入结点。
/*插入元素e为新的队尾元素*/
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!s) /*存储分配失败*/
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s; /*把拥有元素e新结点s赋值给原队尾结点的后继*/
Q->rear=s; /*把当前的s设置为队尾结点,rear指向s*/
return OK;
}
3、队列的链式存储结构——出队操作
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点。
/*若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next; /*将欲删除的队头结点暂存给p*/
*e=p->data; /*将欲删除的队头结点的值赋给e*/
Q->front->next=p->next; /*将原队头结点后继p->next赋值给头结点后继*/
if(Q->rear==p) /*若队头是队尾,则删除后将rear指向头结点*/
Q->rear=Q->front;
free(p);
return OK;
}
4、在可以确定队列长度最大值的情况下,建议用循环队列,如果无法预估队列的长度是,则用链队列。
1、串是由零个或多个字符组成的有限序列,又名叫字符串。串中的字符数目称为串的长度。零个字符的串称为空串,它的长度为零,可以直接用“ “” ”表示,也可以用希腊字母“Φ”表示。
2、空格串是只包含空格的串,空格串是有内容有长度的,而且可以不止一个空格。
3、子串与主串,串中任意个数连续字符组成的字符序列称为该串的子串,相应的,包含子串的串称为主串。
4、子串在主串中的位置就是子串的第一个字符在主串中的序号。
1、C语言中要比较两个串是否相等,必须是它们串的长度以及它们各自对应位置的字符都相等时,才算是相等。
2、给定两个串:s=“a1a2a3……an”,t=“b1b2b3……bm”
(1)例如当s=“hap”, t=“happy”,就有s (2)例如当s=“happen”,t=“happy”,因为两串的前4个字母相同,而两串的第5个字母,字母e的ASCII码是101,而字母y的ascii码是121,显然e (3)例如当s=“abc”,t="bc"时,s 1、串的顺序存储结构 (2)既然是定长数组,就存在一个预定义的最大串长度,一般可以将实际的串长度值保存在数组的0下表位置,有的也会定义存储在数组的最后一个下标位置。但也有些编程语言不想这么干,它规定在串值后面加一个不计入串长度的结束标记字符,比如“\0”来表示串值的终结,这个时候要知道此时的串长度,就需要遍历计算一下。 (3)串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由存储区,叫做“堆”。这个堆可有C语言的动态分配函数malloc()和free()来管理。 2、串的链式存储结构 对于串的链式存储结构,与线性表是相似的,一个结点可以存放多个字符,最后一个结点若是未被占满时,可以用“#”或其他非串值字符补全。 子串的定位操作通常称作串的模式匹配。 假设从"S"=“goodgoogle"中,找到"T”="google"这个子串的位置。 1、主串S第一位开始,S与T前三个字母都匹配成功,但S第四个字母是d而T的是g。第一位匹配失败。 2、主串S第二位开始,主串S首字母是o,要匹配的T首字母是g,匹配失败。 3、主串S第三位开始,主串的首字母是o,要匹配的T的首字母是g,匹配失败。 4、主串S第四位开始,主串S首字母是d,要匹配的T首字母是g,匹配失败。 5、主串S第五位开始,S与T,6个字母全匹配,匹配成功。 数组实现模式匹配的算法Index,假设主串S和要匹配的子串T的长度在S[0]和T[0]中,实现代码如下: 1、KMP模式匹配算法原理 问题是由模式串决定的,不是由目标串决定的 。KMP匹配算法在每次匹配失败后模式串移动的距离不一定是 1,某些情况下一次可移动多个位置,这样可以大大减少重复比较。在KMP匹配算法中,定义了一个next数组进行记录串应该返回的地方。next数组的长度为子串的长度,next数组表示若当前位不匹配我们该从第几位开始重新比较。 2、next数组值的推导 前缀:除了最后一个字符外,字符串的全部头部组合。 后缀:除了第一个字符外,字符串的全部尾部组合。 前缀必须要从头开始算,后缀要从最后一个数开始算,中间截一段相同字符串是不行的。“ababa” 前缀[a,ab,aba,abab],后缀[a,ba,aba,baba],最长公共部分为[aba]。 KMP算法中next数组保存当前字符之前的字符的最长公共前后缀+1。 (1)T=“abcdex” 当j=1时,next[1] = 0; 当j=3时,j由1到j-1的串是“ab”,“a”与“b”不等,next[3] = 1; (2)T=“abcabx” 当j=1时,next[1] = 0; 当j=6时,j由1到j-1的串是“abcab”,由于前缀字符“ab”和后缀字符“ab”相等,所以,next[6] = 3; (3)T=“ababaaaba” 当j=1时,next[1] = 0; 当j=6时,j由1到j-1的串是“ababa”,由于前缀字符“aba”和后缀字符“aba”相等,所以,next[6] = 4; (4)T=“aaaaaaaab” 当j=1时,next[1] = 0; 当j=9时,j由1到j-1的串是“aaaaaaaa”,由于前缀字符“aaaaaaa”和后缀字符“aaaaaaa”相等,所以,next[9] = 8; 3、KMP模式匹配算法实现 4、KMP模式匹配算法改进 5、nextval数组值推导 1、T=“ababaaaba” 当j=1时,nextval[1]=0; 当j=2时,因第二个字符“b”的next值是1,而第一位就是“a”,它们不相等,所以nextval[2]=next[2]=1,维持原值。 当j=3时,因为第三个字符“a”的next值为1,所以与第一位的“a”比较得知它们相等,所以nextval[3]=nextval[1]=0; 2、T=“aaaaaaaab” 当j=1时,nextval[1]=0; 当j=2时,next值为1,第二个字符与第一个字符相等,所以nextval[2]=nextval[1]=0; 当j=9时,next值为8,第九个字符“b”与第八个字符“a”不相等,所以nextval[9]=“8”; 总结改进过的KMP算法,它是在计算出next值得同时,如果a为字符与它next指向的b位字符相等,则该a为的nextval就指向b为的nextval值,如果不等,则该a位nextval值就是它自己a位得next的值。 树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一棵非空树中: (1)有且仅有一个特定的称为根的(Root)结点; (2)当n>0时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 子树T1和子树T2就是跟结点A的子树。当然,D、G、H、I组成的树又是B结点的子树,E、J组成的树是C为结点的子树。 对于树的定义还需要强调两点: n>0时,根结点是唯一的,不可能存在多个根结点,别和现实中的大树混在一起,现实中的树有很多根须,那是真实的树,数据结构中的树是只能有一个根结点。 m>0时,子树的个数没有限制,但它们一定是互不相交的。 1、结点分类 树的结点包含一个数据元素及其若干指向其子树的分支。 结点拥有的子树数称为结点的度(Degree)。 度为0的结点称为叶结点(Leaf)或终端结点。 度不为0的结点称为非终端结点或分支结点。 除根结点之外,分支结点也称为内部结点。 树的度是树内各结点的度的最大值。 2、结点间关系 结点的子树的根称为该结点的孩子(Child),相应的,该结点称为孩子的双亲(Parent)。 同一个双亲的孩子之间互称兄弟(Sibling)。 结点的祖先是从根到该结点所经分支上所有结点。 以某结点为根的子树中的任一结点都称为该结点的子孙。 对H来说,D、B、A都是它的祖先,B的子孙有D、G、H、I。 3、树的其他相关概念 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第l层,则其字树的根就在l+1层。其双亲在同一层的结点互为堂兄弟。 树中结点的最大层次称为树的深度(Depth)或高度。 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。 森林(Forest)是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。 线性表与数的结构的比较 线性结构 第一个数据元素:无前驱 最后一个数据元素:无后继 中间元素:一个前驱一个后继 树结构 根结点:无双亲,唯一 叶结点:无孩子,可以多个 中间结点:一个双亲多个孩子 1、双亲表示法 除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。 假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁外,还知道它的双亲在哪里。 其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。 由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,所有的结点都存有它双亲的位置。 这样的存储结构很容易找到结点的双亲,时间复杂度为O(1),但如果要知道结点的孩子需要遍历整个结构才行。 增加一个结点左边孩子的域,这样就可以容易得到结点的孩子。如果没有孩子的结点,这个域就设置为-1。 对于有0或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。甚至是两个孩子,知道了长子是谁,另一个当然就是次子了。 增加一个右兄弟域来体现兄弟关系。也就是说,每一个结点如果存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。 如果结点的孩子很多,超过了2个。还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。 存储结构的设计是一个非常灵活的过程。一个存储结构过程设计的是否合理,取决于基于该存储结构的运算是否适合,是否方便、时间复杂度好不好等。 2、多重链表表示法 由于树中每个结点可能有多个子树,可以考虑用多重链表,及每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。树的每个结点的度,也就是它的孩子的个数是不同的,所以可以设计两种方案来解决。 方案1 第一种是指针域的个数就等于树的度。树的度是树各个结点度的最大值。 其中data是数据域。child1到childd是指针域,用来指向该结点的孩子结点。 这种方法对于树中各结点的度相差很大时,显然是浪费空间的,因为有很多的结点,它的指针域都是空的。 方案2 第二种是每个结点指针域的个数等于该结点的度,专门去一个位置来存储结点指针域的个数。 其中data是数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点各个孩子的结点。 这种方法克服了浪费空间的缺点,对于空间利用率是很高了,但由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间的损耗。 3、孩子表示法 为了要遍历整棵树,把每个结点放到一个顺序存储结构的数组中是合理的,但每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子建立一个单链表体现他们的关系。 这就是孩子表示法。具体方法是:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针有组成一个线性表,采用顺序存储结构,存放进一个一维数组中。 为此,设计两种结点结构,一个是孩子链表的孩子结点。 其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。 另一个表头数组的表头结点。 其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。 这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的对头结点的数组循环即可。 但是,要知道某个结点的双亲是谁,比较麻烦,需要整棵树遍历才行。这种情况下可以把双亲表示法和孩子表示法综合一下。 我们把这种方法成为双亲孩子表示法,应该算是孩子表示的改进。 4、孩子兄弟表示法 任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,,分别指向该结点的第一个孩子和此结点的右兄弟。 其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。 这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过firstchild找到此结点的长子,然后通过长子结点的rightchild找到它的二弟,接着一直下去,直到找到具体的孩子。 这个表示法的最大好处是它把一棵复杂的树,变成了一棵二叉树。 对于在某个阶段都是两种结果的情形,比如开和关、0和1、真和假、上和下、对与错、正面与反面等,都适合用树状结构来建模,而这种树是一种很特殊的树,叫做二叉树。 二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。 1、二叉树特点 二叉树的特点有 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。 左子树和右子树是有顺序的,次序不能颠倒。 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。树1和树2是同一棵树,但它们却是不同的二叉树。 二叉树具有的五种基本形态 空二叉树。 只有一个根结点。 根结点只有左子树。 根结点只有右子树。 根结点既有左子树又有右子树。 如果是由三个结点的树,有几种形态?如果是由三个结点的二叉树,又由于几种形态? 若只从形态上考虑,三个结点的树只有两种情况,有两层的树1和有三层的后四种的任意一种。 但对于二叉树来说,由于要区分左右,所以就演变成五种形态,树2、树3、树4和树5分别代表不同的二叉树。 2、特殊二叉树 1、斜树 所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树交右斜树,这两者统称为斜树。 斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。 2、满二叉树 在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。 满二叉树的特点有: (1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。 (2)非叶子结点的度一定是2。 (3)正在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。 3、完全二叉树 对一棵具有n个结点的二叉树按层序编号,如果编号为(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。 满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。 完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点,是一一对应的。 树1中,因为5结点没有左子树,却有右子树,那就使得按层序编号的第10个编号空档了。 树2中,由于3结点没有子树,使得6、7编号的位置空档了。 完全二叉树的特点 (1)叶子结点只能出现在最下两层。 (2)最下层的叶子一定集中在左部连续位置。 (3)倒数二层,若有叶子结点,一定都在右部连续位置。 (4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。 (5)同样结点数的二叉树,完全二叉树的深度最小。 判断某二叉树是否是完全二叉树的方法,就是给每个结点按照满二叉树的接否逐层顺序编号,如果编号出现空档,就说明不是完全二叉树,否则就是。 性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)。n 性质2:深度为k的二叉树至多有2k-1个结点(k>=1)。 性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。 终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,设n1为度是1的结点数,则树T结点总数n=n0+n1+n2。 性质4:具有n个结点的完全二叉树的深度为[log2n]+1([x]表示不大于x的最大整数)。 性质5:如果对一棵有n个结点的完全二叉树(其深度为[log2n] + 1)的结点按层序编号(从第1层到第[log2n] + 1层。每层从左到右),对任一结点i(1<=i<=n)有: (1)如果i=1,则结点i是完全二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。 (2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。 (3)如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。 第一条,i=1时就是根结点。i>1时,比如结点7,它的双亲就是[7/2]=3,结点9,它的双亲就是[9/2]=4。 第二条,比如结点6,因为2×6=12超过了结点总数10,所以结点6无左孩子,它是叶子结点。同样,而结点5,因为2×5=10正好是结点总数10,所以它的左孩子是结点10。 第三条,比如结点5,因为2×5+1=11,大于结点总数10,所以它无右孩子,而结点3,因为2×3+1=7小于10,所以它的右孩子是结点7。 1、二叉树的顺序存储结构 二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等等。 完全二叉树的顺序存储 将这棵二叉树存入到数组中,相应的下标对应其同样的位置。 对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的结点设置为"^"而已。 考虑一种极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配2k-1个存储空间单元,这显然是对存储空间的浪费。 2、二叉链表 二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,我们称这样的链表叫做二叉链表。 其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。 1、二叉树遍历原理 二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。 2、二叉树遍历方法 如果限制了从左到右的习惯方式,那么主要就分为四种: (1)前序遍历 规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,在前序遍历右子树。 (2)中序遍历 规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 (3)后序遍历 规则是若树为空,则空操作返回,否则从从左到右先叶子后结点方式遍历访问左右子树,最后是访问根结点。 (4)层序遍历 规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。 3、前序遍历算法 4、中序遍历算法 5、后序遍历算法 6、推导遍历结果 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。 已知前序遍历序列和后序遍历序列,是不能确定一棵二叉树的。 为了能让每个结点确定是否有左右孩子,对它进行扩展,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#”。称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。 扩展二叉树的前序遍历序列AB#D##C##,中序遍历序列#B#D#A#C#,后序遍历序列为###DB##CA。 生成一棵二叉树,假设二叉树的结点均为一个字符,吧前序遍历序列AB#D##C##用键盘挨个输入。实现算法如下: 1、线索二叉树原理 指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Threaded Binary Tree)。 对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化。 ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。 rtag为0时指向该结点的右孩子,为1时指向该结点的后继。 2、线索二叉树结构实现 线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。 中序遍历线索化的递归函数代码如下: 遍历的代码如下: 如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。 1、树转换为二叉树 将树转换为二叉树的步骤如下 加线。在所有兄弟结点之间加一条连线。 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。 2、森林转换为二叉树 森林是由如若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理方法来操作。步骤如下: 把每一棵树转换为二叉树 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。 3、二叉树转换为树 二叉树转换为树是树转换为二叉树的逆过程。步骤如下: 加线。左孩子的n个右孩子结点都作为此结点的孩子,将这些右孩子结点用线连接起来。 去线。删除原二叉树中所有结点与其右孩子结点的连线。 层次调整。使之结构层次分明。 4、二叉树转换为森林 判断一棵二叉树能够转换成一棵树还是森林,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树,那么如果是转换成森林,步骤如下: 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。 再将每棵分离后的二叉树转换成树即可。 5、树与森林的遍历 树的遍历分为两种方式: 一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。 另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。 先根遍历序列为ABEFCDG,后根遍历序列为EFBCGDA。 森林的遍历也分为两种方式: 前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成森林。 后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树剩余树构成的森林。 前序遍历序列为ABCDEFGHIJ,后序遍历序列为BCDAFEJHIG。 森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。 1、赫夫曼树 2、赫夫曼树定义与原理 其中A表示不及格,B表示及格,C表示中等,D表示良好,E表示优秀。每个叶子的分支线上的数字就是五级分制的成绩所占比例数。 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。 二叉树a中,根结点到结点D的路径长度就是4,二叉树b中根结点到结点D的路径长度为2。 树的路径长度就是从树根到每一结点的路径长度之和。 二叉树a的路径长度就为1+1+2+2+3+3+4+4=20。二叉树b的路径长度就为1+2+3+3+2+1+2+2=16。 结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。带权路径长度WPL最小的二叉树称作赫夫曼树或最优二叉树。 二叉树a的WPL=5×1+15×2+40×3+30×4+10×4=315, 构造赫夫曼树的步骤: 先把有权值得叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40。 取头两个最小权值的结点作为一个新结点N1的两个子结点,注意相对较小的是左孩子。新结点的权值为两个叶子权值的和5+10=15。 将N1替换A与E,插入有序序列中,保持从小到大排列。即:N115,B15,D30,C40。 重复步骤2。 将N2替换N1与B,插入有序序列中,保持从小到大排列。即:N230,D30,C40。 重复步骤2 。 将N3替换N2与D,插入有序序列中,保持从小到大排列。即:C40,N360。 重复步骤2。 3、赫夫曼编码 若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称作前缀编码。 当我们接收到1001010010101001000111100,由约定好的赫夫曼树可知,1001得到第一个字母是B,接下来01意味着第二个字符是A。 规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。 在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都有可能相关。 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 线性表中的数据叫元素,树中的元素叫结点,在图中数据元素叫顶点(Vertex)。 线性表中可以没有数据元素,称为空表。树中可以没有结点,称为空树。同样,在图结构中不允许没有结点。在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空。 线性表中,相邻的数据元素之间具有线性关系。树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都有可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。 1、各种图定义 无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。 如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。连接顶点A与D的边,可以表示称无序对(A,D),也可以写成(D,A)。 对于图7-2-2中的无向图G1来说,G1=(V1,{E1}),其中顶点集合V1={A,B,C,D};边集合E1={ (A,B),(B,C),(C,D),(D,A),(A,C)} 有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc)。用有序偶 如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,表示弧,注意不能写成 对于图7-2-3中的有向图G2来说,G2=(V2,{E2}),其中顶点集合V2={A,B,C,D};弧集合E2={,, 无向边用小括号“()”表示,而有向边则是用尖括号“<>”表示。 在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。数据结构中讨论的都是简单图。图7-2-4中的两个图就不是简单图。 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有(n×(n-1))/2条边。 在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称改图为有向完全图。含有n个顶点的有向完全图有n×(n-1)条边。 对于具有n个顶点和e条边数的图,无向图0<=e<=(n×(n-1))/2,有向图0<=e<=n×(n-1)。 有很少边或弧的图称为稀疏图,反之称为稠密图。 与图的边或弧相关的数叫做权。带权的图通常称为网。 假设有两个图G=(V,{E})和G’=(V’,{E’}),如果V’是V的子集且E’是E的子集,则称G’为G的子图。 2、图的顶点与边间关系 对于无向图G=(V,{E}),如果边(v,v’)∈E,则称顶点v和v’互为邻接点,即v和v’相邻接。 边(v,v’)依附于顶点v和v’,或者说(v,v’)与顶点v和v’相关联。 顶点v的度适合v相关联的边的数目。即为TD(v)。 对于有向图G=(V,{E}),如果弧 以顶点v为头的弧的数目称为v的入度,记为ID(v);以顶点v为尾的数目称为v的出度,记为OD(v);顶点v的度TD(v)=ID(v)+OD(v)。 路径的长度是路径上的边或弧的数目。 第一个顶点到最后一个顶点相同的路径称为回路或环。 序列中顶点不重复出现的路径称为简单路径。 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。 左侧的环因第一个结点和最后一个顶点都是B,且C,D,A,没有重复出现,因此是一个简单环。 右侧的环,由于顶点C的重复,它就不是简单环了。 3、连通图相关术语· 在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。如果对于图中任意两个顶点vi、vj∈E,vi和vj是都是连通的。则称G是连通图。 图1中,顶点A与顶点E或F无路径,不能算是连通图。图2中,顶点A、B、C、D相互都是连通的,所以它是连通图。 无向图中的极大连通子图称为连通分量。它强调: 要是子图; 子图要是连通的 连通子图含有极大顶点数 就有极大顶点数的连通子图包含依附与这些顶点的所有边。 图1是一个无向非连通图。但是它有两个连通,即分量图2和图3。而图4,尽管是图1的子图,但是它却不满足连通子图的极大顶点数,因此它不是图1的无向图的连通分量。 在有向图G中,如果对于每一对vi,vj∈V、vi≠vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图。 有向图中的极大强连通子图称作有向图的强连通分量。 图1不是强连通图,因为顶点A到顶点D存在路径,而D到A不存在路径。图2是强连通图,而且图2是图1的极大强连通子图,即它是强连通分量。 一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。 图1是一个普通图不是生成树,图2和图3,就满足n个顶点n-1条边且连通的定义,都是一棵生成树。 如果一个图有n个顶点和小于n-1条边,则是非连通图。如果它多于n-1条边,必定构成一个环。 不过有n-1条边并不一定是生成树,比如图4。 如果一个有向图恰好有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。入度为0其实就相当于树中的根结点,其余顶点入度为1就是说树的非根结点的双亲只有一个。 一个有向图的生成森林有若干棵有向树组成1,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。 图1是一棵有向图,去掉一些弧后,它可以分解为两棵有向树,如图2和图3,这两根就是图1有向图的生成森林。 4、图的定义与术语总结 图按照有无方向分为无向图和有向图,无向图由顶点和边构成,有向图有顶点和弧构成。弧有弧尾和弧头之分。 图按照边或弧的多少分为稀疏图和稠密图。如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。 图中顶点之间有邻接点,依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。 图上的边或弧上带权则称为网。 图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向图则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。 无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。 1、邻接矩阵 图的邻接矩阵存储方式是用两个数组来表示图。一个以为数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。 对称矩阵:n阶矩阵的元满足aij=aji(0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元素与左下角相对应的元全都是相等的。 顶点v1的入度为1,正好是第v1列的各数之和。顶点v1的出度为2,即第v1行的各数之和。 判断顶点vi到j是否存在弧,只需要查找矩阵中arc[i][j]是否为1即可。 要求vi的所有邻接点就是将矩阵i行元素扫描一遍,查找arc[i][j]为1的顶点。 每条边上带有权的图叫做网。 图的邻接矩阵存储的结构,代码如下: 无向网图的创建代码: 2、邻接表 数组与链表相结合的存储方法称为邻接表。 邻接表的处理方法 1、图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数组元素还需要存储指向第一个邻接点的指针,以便利于查找该顶点的边信息。 2、图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。 顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此结点的第一个邻接点。 边表有节点adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。 若是有向图,为了便于确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个连接为vi为弧头的表。 对于带权值的网图,可以在边表结点中在增加一个weight的数据域,存储权值信息即可。 无向图的邻接表创建代码如下: 3、十字链表 把邻接表与逆邻接表结合起来,就是十字链表。 顶点表结点结构 firstin表示入边表头指针,指向该顶点的入边表中的第一个结点,firstout表示出边表头指针,指向该顶点的出边表的第一个结点。 边表结点结构 tailvex是指弧起点在顶点表的下标,headvex是指终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还增加一个weight域来存储权值。 十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度。 4、邻接多重表 边表结点结构 ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。 邻接多重表与邻接表的差别,是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,若要删除(v0,v2)这条边,只需要将右图的⑥⑨的链接指向改为^即可。 5、边集数组 边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这边数组每一个数据元素由下一条边的起点下标(begin)、终点下标(end)和(weight)组成。 定义的边数组结构 begin是存储起点下标,end是存储终点下标,weight是存储权值。 从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。 1、深度优先遍历 深度优先遍历,也有称为深度优先搜索,简称DFS。 深度优先遍历其实就是一个递归过程,就像是一棵树的前序遍历。 邻接矩阵,代码如下: 邻接表,代码如下: 2、广度优先遍历 广度优先遍历,又称为广度优先搜索,简称BFS。 如果说图的深度遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历。 邻接矩阵,代码如下: 邻接表,代码如下: 把构造连通网的最小代价生成树称为最小生成树。 1、普利姆(Prim)算法 2、克鲁斯卡尔(Kruskal)算法 对比这两个算法,克鲁斯卡尔算法主要针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法主要对于稠密图,即使边数非常多的情况会更好一些。 在网图和非网图中,最短路径的含义是不同的。由于非1网图没有边上的权值,所谓的最短路径,其实就是指两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。就地图来说,距离就是两顶点间的权值之和。而非网图完全可以理解为所有的边的权值都为1的网。 1、迪杰斯特拉(Dijkstra)算法 2、弗洛伊德(Floyd)算法 求最短路径的显示代码: 如果你面临需要所以顶点至所有顶点的最短路径问题时,弗洛伊德(Floyd)算法应该是不错的选择。 1、拓补排序介绍 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network)。AOV网中的弧表示活动之间存在的某种制约关系。AOV网中不能存在回路。 设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,……,vn满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前,则我们称这样的顶点序列为一个拓扑序列。 拓扑排序,其实就是对一个有向图构造拓扑序列的过程。构造时会有两个结果,如果此网的全部顶点全部被输出,则说明它是不存在环(回路)的AOV网;如果输顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。 2、拓扑排序算法 对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或AOV网中不存在入度为0的顶点为止。 由于拓扑排序的过程中,需要删除顶点,显然用邻接表会更加方便,因此我们需要为AOV网建立一个邻接表。考虑到算法过程中始终要查找入度为0的顶点,我们在原来顶点表结点结构中,增加一个入度域in,其中in就是入度的数字。 在拓扑排序算法中,涉及的结构代码如下。 拓扑排序主要是为解决一个工程是否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。我们如果要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。 在表示工程的带权有向图中,用顶点表示时间,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网。我们称之为AOE网(Activity On Edge Network),我们把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE网中只有一个源点一个汇点。 如有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始。只有在进入某顶点的个活动都已经结束,该顶点所代表的事件才能发生。 尽管AOE网与AOV网都是用来对工程进行建模的,它们还是有很大的不同,主要体现在AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示持续的时间,AOE网是要建立在活动之间的制约关系没有矛盾的基础之上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动问题。 我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径交关键路径,在关键路径上的活动叫关键活动。显然就图7-9-3的AOE图而言,“开始->发动机完成->部件集中到位->组装完成”就是关键路径,路径长度为5.5。 1、关键路径算法原理 只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动建的路径为关键路径,如果不等,则就不是。 为此我们需要定义如下几个参数。 1、事件的最早发生时间etv(earliest time of vertex):即顶点Vk的最早发生时间。 2、事件的最晚发生时间ltv(latest time of vertex):即顶点Vk的最晚发生时间,也就是每个顶点对应事件最晚需要开始的时间,超出此时间将会延误整个工期。 3、活动的最早开始时间ete(earliest time edge):即弧Vk的最早发生时间。 4、活动的最晚开工时间lte(latest time edge):即弧Vk的最晚发生时间,也就是不推迟工期的最晚开工时间。 我们是由1和2可以求得3和4,然后再根据ete[k]是否与lte[k]相等来判断ak是否为关键活动。 2、关键路径算法 与拓扑排序邻接表结构不同地方在于,这里弧链表增加了weight域,用来存储弧的权值。 求事件的最早发生时间etv过程,就是我们从头至尾找出拓扑序列的过程,因此,在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算etv和拓扑序列列表。为此,我们首先在程序开始处声明几个全局变量。 其中stack2是用来存储拓扑序列,以便后面求关键路径时使用。 下面是改进过的求拓扑序列的算法。 图的存储结构,其中比较重要的是邻接矩阵和邻接表,它们分别代表着边集是用数组还是链表的方式存储。十字链表是邻接矩阵的一种升级,而邻接多重表则是邻接表的升级。边集数组更多考虑的是对边的关注。用什么存储结构需要具体问题具体分析,通常稠密图,或读存数据较多,结构修改较少的图,用邻接矩阵要更合适,反之则应该考虑邻接表。 图的遍历分为深度和广度两种,各有优缺点。 图的应用一共谈了三种应用:最小生成树、最短路径和有向无环图的应用。 最小生成树,两种算法:普拉姆算法和克鲁斯卡尔算法。普里姆算法像是走一步看一步的思维方式,逐步生成最小生成树。而克鲁斯卡尔算法则更有全局意识,直接从图中最短权值的边入手,找寻最后的答案。 最短路径的现实应用非常多,两种算法。迪杰斯特拉(Dijkstra)算法更强调单源顶点查找路径的方式,比较符合我们正常的思路,容易理解原理,但算法代码相对复杂。而弗洛伊德 (Fbyd)算法则完全抛开了单点的局限思维方式,巧妙地应用矩阵的变换,用最清爽的代码实现了多顶点间最短路径求解的方案,原理理解有难度,但算法编写很简洁。 有向无环图时常应用于工程规划中,对于整个工程或系统来说,我们一方面关心的是工程能否顺利进行的问题,通过拓扑排序的方式,我们可以有效地分析出一个有向图是否存在环,如果不存在,那它的拓扑序列是什么?另一方面关心的是整个工程完成所必须的最短时间问题,利用求关键路径的算法,可以得到最短完成工程的工期以及关键的活动有哪些。 查找表是由同一类型的数据元素(或记录)构成的集合。 关键字是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素。也可以标识一个记录的某个数据项(字段),我们称为关键码。 若此关键字可以唯一的标识一个记录,则称此关键字为主关键字。对不同的记录,其主关键字均不相同。主关键字所在的数据项称为主关键码。 对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字。次关键字也可以理解为是不以唯一标识一个数据元素(或记录)的关键字,它对应的数据项就是次关键码。 查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。 查找表按照操作方式来分有两大种:静态查找表和动态查找表。 静态查找表:只作查找操作的查找表,它的主要操作有: (1) 查询某个“特定的”数据元素是否在查找表中。 (2) 检索某个“特定的”数据元素和各种属性。 动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。动态查找表的操作就是两个: (1) 查找时插入数据元素。 (2) 查找时删除数据元素。 针对线性表进行查找操作就是静态查找表。 顺序查找又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或者最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果知道最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。 1、顺序查找算法 顺序查找的算法实现 2、顺序表查找优化 1、折半查找 折半查找技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常是从小到大有序),线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键词,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。 折半查找的时间复杂度为O(logn),不过折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法已经比较好了,但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,不建议使用。 2、插值查找 插值查找是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式(key-a[low])/(a[high]-a[low]),时间复杂度是O(logn),但对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。 3、斐波那契查找 斐波那契查找,它是利用了黄金分割原理来实现的。 斐波那契查找算法的核心在于: (1) 当key=a[mid]时,查找就成功;5.3 串的抽象数据类型
ADT 串(String)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
StrAssign(T,*chars):生成一个其值等于字符串常量chars的串T。
StrCopy(T,S):串S存在,由串S复制的串T。
ClearString(S):串S存在,将串清空。
StringEmpty(S):若串S为空,返回true,否则返回false。
StringLength(S):返回串S的元素个数,即串的长度。
StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0。
Concat(T,S1,S2):用T返回有S1和S2连接而成的新串。
SubString(Sub,S,pos,len):串S存在,1<=pos<=StrLength(S)且0<=len<=StringLength(S)-pos+1,用Sub返回串S的第pos个字符起长度为len的子串。
Index(S,T,pos):串S和T存在,T是非空串,1<=pos<=StrLength(S)。若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0。
Replace(S,T,V):串S和T和V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
StrInsert(S,pos,T):串S和T存在,1<=pos<=StrLength(S)+1。在串S的第pos个字符之前插入串T。
StrDelete(S,pos,len):串S存在,1<=pos<=StrLength(S)-len+1。从串S中删除第pos个字符起长度为len的子串。
endADT
/*T为非空串。若主串S中第pos个字符之后存在与T相等的子串,则返回第一个这样的子串在S中的位置,否则返回0*/
int Index(String S,String T,int pos)
{
int n,m,i;
String sub;
if(pos>0)
{
n = StrLength(S); /*得到主串S的长度*/
m = StrLength(T); /*得到子串T的长度*/
i = pos;
while(i<n-m+1)
{
SubString(sub,S,i,m); /*取主串第i个位置,长度与T相等给sub*/
if(StrCompare(sub,T) != 0) /*如果两串不相等*/
++i;
else /*如果两串相等*/
return i; /*则返回i值*/
}
}
return 0; /*若无子串与T相等,返回0*/
}
5.4 串的存储结构
(1)串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个的定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。5.5 朴素的模式匹配算法
/*返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0*/
/*T非空,1<=pos<=StrLength(S)*/
int Index(String S, String T, int pos)
{
int i = pos; /*i用于主串S中当前位置的下标,若pos不为1,则从pos位置开始匹配*/
int j = 1; /*j用于子串T中当前位置下标值*/
while(i<=S[0] && j<=T[0]) /*若i小于S长度且j小于T的长度时循环*/
{
if(S[i] == T[j]) /*两字母相等则继续*/
{
++i;
++j;
}
else /*指针后退重新开始匹配*/
{
i = i-j+2; /*i退回上次匹配首位的下一位*/
j = 1; /*j退回到子串T的首位*/
}
}
if(j>T[0])
return i-T[0];
else
return 0;
}
5.6 KMP模式匹配算法
/*通过计算返回子串T的next数组*/
void get_next( String T, int *next ) {
int i,j;
i = 1;
j = 0;
next[1] = 0;
while( i<T[0] ) /*T[0]表示串T的长度*/
{
if( j==0 || T[i]==T[j] ) /*T[i]表示后缀的单个字符,T[j]表示前缀的单个字符*/
{
++i;
++j;
next[i] = j;
}
else
j = next[j]; /*若字符不相同,则j值回溯*/
}
}
/*返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数返回值为0*/
/*T非空,1<=pos<=StrLength(S)*/
int Index_KMP( String S, String S, int pos ) {
int i = pos; /*i用于子串S当前位置的下标值,若pos不为1,则从pos位置开始匹配*/
int j = 1; /*j用于子串T中当前位置的下标*/
int next[255]; /*定义一next数组*/
get_next(T,next); /*对串T作分析,得到next数组*/
while( i<=S[0] && j<=T[0] )
{
if ( j==0 || S[i]==T[j] ) {
++i;
++j;
}
else {
j = next[j];
}
}
if ( j>T[0] )
return i-T[0];
else
return 0;
}
/*求模式串T的next函数修正值并存入数组nextval*/
void get_nextval( String T, int *nextval ) {
int i,j;
i = 1;
j = 0;
nextval[1] = 0;
while( i<T[0] ) /*T[0]表示串T的长度*/
{
if( j==0 || T[i]==T[j] ) /*T[i]表示后缀的单个字符,T[j]表示前缀的单个字符*/
{
++i;
++j;
if ( T[i]!=T[j] ) /*若当前字符与前缀字符不同*/
nextval[i] = j; /*则当前的j为nextval子i位置的值*/
else
nextval[i] = nextval[j]; /*如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值*/
}
else
j = nextval[j]; /*若字符不相同,则j值回溯*/
}
}
第6章 树
6.1 树的定义
6.2 树的抽象数据类型
ADT 树(Tree)
Data
树是由一根结点和若干子树构成,树中结点具有相同数据类型及层次关系。
Operation
InitTree(*T):构造空树T。
DestroyTree(*T):销毁树T。
CreateTree(*T,definitation):按defination中给出树的定义来构造树。
ClearTree(*T):若树T存在,则将树T清为空树。
TreeEmpty(T):若T为空树,返回true,否则返回false。
TreeDepth(T):返回T的深度。
Root(T):返回树的根结点。
Value(T,cur_e):cur_e是树T中的一个结点,返回此结点的值。
Assign(T,cur_e,value):给树T的结点cur_e赋值为value。
Parent(T,cur_e):若cur_e是树T的非根结点,则返回它的双亲,否则返回空。
LeftChild(T,cur_e):若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空。
RightSibling(T,cur_e):若cur_e有右兄弟,则返回它的右兄弟,否则返回空。
InsertChild(*T,*p,i,c):其中p指向树T的某个结点,i为所指结点p的度街上1,非空树c与T不相交,操作结果为插入c为树T中p指向结点的第i棵子树。
DeleteChild(*T,*p,i):其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i棵子树。
endADT;
6.3 树的存储结构
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType; /*树结点的数据类型,目前暂定为整型*/
typedef struct PINode /*结点结构*/
{
TElemType data; /*结点数据*/
int parent; /*双亲位置*/
}PINnode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE]; /*结点数组*/
int r,n; /*根的位置和结点数*/
}PTree;
/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 100
typedef struct CTNode /*孩子结点*/
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct /*表头结构*/
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct /*树结构*/
{
CTBox nodes[MAX_TREE_SIZE]; /*结点数组*/
int r, n; /*根的位置和结点数*/
}CTree;
/*树的孩子兄弟表示法结构定义*/
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild, *rightsib;
}CSNode,*CSTree;
如果要找某个结点的双亲,完全可以增加一个parent指针域来解决快速查找双亲问题。6.4 二叉树的定义
6.5 二叉树的性质
6.6 二叉树的存储结构
所以顺序存储结构一般只用于完全二叉树。/*二叉树的二叉链表结点结构定义*/
typedef struct BiTNode /*结点结构*/
{
TElemType data; /*结点数据*/
struct BiTNode *lchild, *rchild; /*左右孩子指针*/
}BiTNode,*BiTree;
6.7 遍历二叉树
/*二叉树的前序遍历递归算法*/
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return 0;
printf("%c",T->data); /*显示结点数据,可以更改为其他对结点操作*/
PreOrderTraverse(T->lchild); /*再先序遍历左子树*/
PreOrderTraverse(T->rchild); /*最后先序遍历右子树*/
}
/*二叉树的中序遍历递归算法*/
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return 0;
InOrderTraverse(T->lchild); /*中序遍历左子树*/
printf("%c",T->data); /*显示结点数据,可以更改为其他对结点操作*/
InOrderTraverse(T->rchild); /*最后中序遍历右子树*/
}
/*二叉树的后序遍历递归算法*/
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return 0;
PostOrderTraverse(T->lchild); /*先后序遍历左子树*/
PostOrderTraverse(T->rchild); /*再后序遍历右子树*/
printf("%c",T->data); /*显示结点数据,可以更改为其他对结点操作*/
}
6.8 二叉树的建立
/*按前序输入二叉树中结点的值(一个字符)*/
/*#表示空树,构造二叉链表表示二叉树T。*/
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c", &ch);
if(ch=="#")
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /*生成根结点*/
CreateBiTree(&(*T)->lchild); /*构造左子树*/
CreateBiTree(&(*T)->rchild); /*构造右子树*/
}
}
6.9 线索二叉树
typedef enum {
Link,Thread} PointerTag; /*Link==0表示指向左右孩子指针,Thread==1表示指向前驱或后继的线索*/
typedef struct BiThrNode
{
TElemType data; /*结点数据*/
struct BiThrNode *lchild, *rchild /*左右孩子指针*/
PointerTag LTag;
PointerTag RTag; /*左右标志*/
}BiThrNode, *BiThrTree;
BiThrTree pre; /*全局变量,始终指向刚刚访问过的结点*/
/*中序遍历进行中序线索化*/
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); /*递归左子树线索化*/
if(!p->lchild) /*没有左孩子*/
{
p->LTag=Thread; /*前驱线索*/
p->lchild=pre; /*左孩子指针指向前驱*/
}
if(!pre->rchild) /*前驱没有右孩子*/
{
pre->RTag=Thread; /*后继线索*/
pre->rchild=p; /*前驱右孩子指针指向后继(当前结点p)*/
}
pre=p; /*保持pre指向p的前驱*/
InThreading(p->rchild); /*递归右子树线索化*/
}
}
/*T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。中序遍历二叉线索链表表示的二叉树T*/
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild; /*p指向根结点*/
while(p != T) /*空树或遍历结束时,p==T*/
{
while(p->LTag==Link) /*当LTag==0时循环到中序序列第一个结点*/
p = p->lchild;
printf("%c",p->data); /*显示结点数据,可以更改为其他对结点操作*/
while(p->RTag==Thread && p->rchild!=T)
{
p = p->rchild;
printf("%c",p->data);
}
p = p->rchild; /*p进至其右子树根*/
}
return OK;
}
6.10 树、森林与二叉树的转换
6.11 赫夫曼树及其应用
二叉树b的WPL=5×3+15×3+40×2+30×2+10×2=220。
第7章 图
7.1 图的定义
7.2 图的抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。
Operation
CreateGraph(*G,V,VR):按照顶点集V和弧集VR的定义构造图G。
DestroyGraph(*G):图G存在则销毁。
LocateVex(G,u):若图G中存在顶点u,则返回图中的位置。
GetVex(G,v):返回图G中顶点v的值。
PutVex(G,v,value):将图G顶点v赋值value。
FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点G中无邻接顶点返回空。
NextAdjVex(G,v,*w):返回顶点v的相对于顶点w的下一个相邻结点,若w是v的最后一个邻接点则返回“空”。
InsertVex(*G,v):在图G中增添新顶点v。
DeleteVex(*G,v):删除图G中顶点v及相关的弧。
InsertArc(*G,v,w):在图G中增添弧<v,w>,若G是无向图,还需要增添对称弧<w,v>。
DeleteArc(*G,v,w):在图G中删除弧<v,w>,若G是无向图,还需要删除对称弧<w,v>。
DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程中对每个顶点调用。
HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程中对每个顶点调用。
endADT
7.3 图的存储结构
typedef char VertexType /*顶点类型应由用户定义*/
typedef int EdgeType /*边上的权值类型应由用户定义*/
#define MAXVEX 100 /*最大顶点数,应由用户定义*/
#define INFINITY 65535 /*用65535来代表∞*/
typedef struct
{
VertexType vexs[MAXVEX]; /*顶点表*/
EdgeType arc[MAXVEX][MAXVEX] /*邻接矩阵,可看作边表*/
int numVertexes, numEdges; /*图中当前的顶点数和边数*/
}MGragh;
void CreateMGraph(MGraph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); /*输入顶点和边*/
for( i=0; i<G->numVertexes; i++ ) /*读入顶点信息,建立顶点表*/
scanf(&G->vexs[i]);
for( i=0; i<G->numVertexes; i++ )
for(j=0; i<G->numVertexes; j++ )
G->arc[i][j]=INFINITY; /*邻接矩阵的初始化*/
for( k=0;k<G->numEdges;k++ ) /*读入numEdges条边,建立邻接矩阵*/
{
printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
scanf("%d,%d,%d", &i,&j,&w); /*输入边(vi,vj)上的权w*/
G->arc[i][j]=w;
G->arc[j][i]=G->arc[i][j]; /*因为是无向图,矩阵对称*/
}
}
typedef char VertexType; /*顶点类型应由用户定义*/
typedef int EdgeType; /*边上的权值类型应由用户定义*/
typedef struct EdgeNode /*边表结点*/
{
int adjvex; /*邻接点域,存储该顶点对应的下标*/
EdgeType weight; /*用于存储权值,对于非网图可以不需要*/
struct EdgeNode *next; /*链域,指向下一个邻接点*/
}EdgeNode;
typedef struct VertexNode /*顶点表结点*/
{
VertexType data; /*顶点域,存储顶点信息*/
EdgeNode *firstedge; /*边表头指针*/
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /*图中当前顶点数和边数*/
}GraphAdjList;
/*建立图的邻接表结构*/
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVertexes,&G->numEdges); /输入顶点数和边数*/
for(i=0; i<G->numVertexes; i++) /*读入顶点信息,建立顶点表*/
{
scanf(&G->adjList[i].data); /*输入顶点信息*/
G->adjList[i].firstedge=NULL; /*将边表值为空表*/
}
for(k=0;k<G->numEdges;k++) /*建立边表*/
{
printf("输入边(vi,vj)上的顶点序号:\n");
scanf("%d,%d",&i,&j); /*输入边(vi,vj)上的顶点序号*/
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /*向内存申请空间,生成边表结点*/
e->adjvex=j; /*邻接序号为j*/
e->next=G->adjList[i].firstedge; /*将e指针指向当前顶点的结点*/
G->adjList[i].firstedge=e; /*将当前顶点的指针指向e*/
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /*向内存申请空间,生成边表结点*/
e->adjvex=i; /*邻接序号为i*/
e->next=G->adjList[j].firstedge; /*将e指针指向当前顶点指向的结点*/
G->adjList[j].firstedge=e; /*将当前顶点的指针指向e*/
}
}
7.4 图的遍历
typedef int Boolean; /*Boolean是布尔类型,其值是TRUE或FALSE*/
Boolean visited[MAX]; /*访问标志的数组*/
/*邻接矩阵的深度优先递归算法*/
void DFS (MGraph G, int i)
{
int j;
visited[i] = TRUE;
printf("%c", G.vexs[i]); /*打印顶点,也可以其他操作*/
for(j=0; j<G.numVertexes; j++)
if(G.arc[i][j] == 1 && !visited[j])
DFS(G, j); /*对未访问的邻接顶点递归调用*/
}
/*邻接矩阵的深度遍历操作*/
void DFSTraverse (MGraph G)
{
int i;
for(i=0; i<G.numVertexes; i++)
visited[i] = FALSE; /*初始所有顶点状态都是未访问过状态*/
for(i=0; i<G.numVertexes; i++)
if(!visited[i]) /*对未访问过的顶点调用DFS,若是连通图,只会执行一次*/
DFS(G, i);
}
/*邻接表的深度优先遍历算法*/
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
printf("%c ",GL->adjList[i].data); /*打印顶点也可其他操作*/
p = GL->adjList[i].firstedge;
while(p)
{
if(!visited[p->adjvex])
DFS(GL, p->adjvex); /*对未访问的邻接顶点递归调用*/
p = p->next;
}
}
/*邻接表的深度遍历操作*/
void DFSTraverse(GraphAdjList GL)
{
int i;
for(i=0; i<GL->numVertexes; i++)
visited[i] = FALSE; /*初始所有顶点状态都是未访问状态*/
for(i=0; i<GL->numVertexes; i++)
if(!visited[i]) /*对未访问过的顶点调用DFS,若是连通图,只会执行一次*/
DFS[G, i];
}
/*邻接矩阵的广度优先遍历算法*/
void BFSTraverse(MGraph G)
{
int i,j;
Queue Q;
for(i=0; i<G.numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q); /*初始化辅助用的队列*/
for(i=0; i<G.numVertexes; i++) /*对每一个顶点做循环*/
{
if(!visited[i]) /*若是未访问过就处理*/
{
visited[i] = TRUE; /*设置当前顶点被访问过*/
printf("%c ", G.vexs[i]); /*打印顶点,也可以进行其他操作*/
EnQueue(&Q,i); /*将此顶点入队列*/
while(!QueueEmpty(Q)) /*若当前队列不为空*/
{
DeQueue(&Q,&i); /*将队中元素出队列,赋值给i*/
for(j=0; j<G.numVertexes; j++)
{
/*判断其他顶点若与当前顶点存在边且未访问过*/
if(G.arc[i][j] == 1 && !visited[j])
{
visited[j] = TRUE; /*将找到的此顶点标记为已访问*/
printf("%c ", G.vexs[j]); /*打印顶点*/
EnQueue(&Q,j); /*将找到的此顶点入队列*/
}
}
}
}
}
}
/*邻接表的广度优先遍历算法*/
void BFSTraverse(GraphAdjList GL)
{
int i
EdgeNode *p;
Queue Q;
for(i=0; i<GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i=0; i<GL->numVertexes; i++)
{
if(!visited[i])
{
visited[i] = TRUE;
printf("%c ", GL->adjList[i].data); /*打印顶点,也可以进行其他操作*/
EnQueue(&Q,i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p = GL->adjList[i].firstedge; /*找到当前顶点边表链表头指针*/
while(p)
{
if(!visited[p->adjvex]) /*若此顶点未被访问*/
{
visited[p->adjvex] = TRUE;
printf("%c ",GL->adjList[p->adjvex].data);
EnQueue(&Q,p->adjvex); /*将此顶点入队列*/
}
p = p->next; /*指针指向下一个邻接点*/
}
}
}
}
}
7.5 最小生成树
/*Prim算法生成最小生成树*/
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /*保存相关顶点下标*/
int lowcost[MAXVEX]; /*保存相关顶点间边的权值*/
lowcost[0]=0; /*初始化第一个权值为0,即V0加入生成树。lowcost的值为0,在这里就是此下标的顶点已经加入生成树*/
adjvex[0]=0; /*初始化第一个顶点下标为0*/
for(i=1; i<G.numVertexes; i++) /*循环除下标为0外的全部顶点*/
{
lowcost[i]=G.arc[0][i]; /*将v0顶点与之有边的权值存入数组*/
adjvex[i]=0; /*初始化都为v0的下标*/
}
for(i=1; i<G.numVertexes; i++)
{
min=INFINITY; /*初始化最小权值为∞,通常设置为不可能的大数字如32767,65535等*/
j=1;k=0;
while(j<G.numVertexes) /*循环全部顶点*/
{
if(lowcost[j]!=0 && lowcost[j]<min)
{
/*如果权值不为0且权值小于min*/
min=lowcost[j]; /*则让当前权值成为最小值*/
k=j; /*将当前最小值的下标存入k*/
}
j++;
}
printf("(%d,%d)",adjvex[k],k); /*打印当前顶点边中权值最小边*/
lowcost[k]=0; /*将当前顶点的权值设置为0,表示此顶点已经完成任务*/
for(j=1; j<G.numVertexes; j++) /*循环所有顶点*/
{
if(lowcost[j]!=0 && G.arc[k][j]<lowcost[j])
{
/*若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值*/
lowcost[j]=G.arc[k][j]; /*将较小权值存入lowcost*/
adjvex[j]=k; /*将下标为k的顶点存入adjvex*/
}
}
}
}
/*对边集数组Edge结构的定义*/
typedef struct
{
int begin;
int end;
int weight;
}Edge;
/*kruskal算法生成最小生成树*/
void MiniSpanTree_Kruskal(MGraph G) /*生成最小生成树*/
{
int i, n, m;
Edge edges[MAXEDGE]; /*定义边集数组*/
int parent[MAXVEX]; /*定义一数组用来判断边与边是否形成环路*/
/*此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码*/
for(i=0; i<G.numVertexes; i++)
parent[i]=0; /*初始化数组值为0*/
for(i=0; i<G.numEdges; i++) /*循环每一条边*/
{
n=Find(parent,edges[i].begin);
m=Find(parent,edges[i].end);
if(n!=m) /*假设n与m不等,说明此边没有与现有生成树形成环路*/
{
parent[n]=m; /*将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中*/
printf("(%d,%d) %d", edges[i].begin,edges[i].end,edges[i].weight);
}
}
}
int Find(int *parent, int f) /*查找连线顶点的尾部下标*/
{
while(parent[f]>0)
f=parent[f];
return f;
}
7.6 最短路径
#define MAXVEX 9;
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; /*用于存储最短路径下标的数组*/
typedef int ShortPathTable[MAXVEX]; /*用于存储到各点最短路径的权值和*/
/*Dijkstra算法,求有向网G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v]*/
/*P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和*/
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *p, ShortPathTable *D)
{
int v,w,k,min;
int final[MAXVEX]; /*final[w]=1表示求得顶点v0至vw的最短路径*/
for(v=0; v<G.numVertexes; v++) /*初始化数据*/
{
final[v]=0; /*全部顶点初始化为未知最短路径状态*/
(*D)[v]=G.matirx[v0][v]; /*将与v0点有连线的顶点加上权值*/
(*P)[v]=0; /*初始化路径数组P为0*/
}
(*D)[v0]=0; /*v0至v0路径为0*/
final[v0]=1; /*v0至v0不需要求路径*/
/*开始主循环,每次求得v0到v顶点的最短路径*/
for(v=1; v<G.numVertexes; v++)
{
min=INFINITY; /*当前所知离v0顶点的最近距离*/
for(w=0; w<G.numVertexes; w++) /*寻找离v0点最近的顶点*/
{
if(!final[w] && (*D)[w]<min)
{
k=w;
min=(*D)[w]; /*w顶点离v0顶点更近*/
}
}
final[k]=1; /*将目前找到的最近的顶点置为1*/
for(w=0; w<G.numVertexes; w++) /*修正当前最短路径及距离*/
{
/*如果经过v顶点的路径比现在这条路径的长短短的话*/
if(!final[w] && (min+G.matirx[k][w]<(*D)[w]))
{
/*说明找到了更短的路径,修改D[w]和P[w]*/
(*D)[w] = min + G.matirx[k][w]; /*修改当前路径长度*/
(*p)[w]=k;
}
}
}
}
typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/*Floyd算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w]*/
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D)
{
int v,w,k;
for(v=0; v<G.numVertexes; ++v) /*初始化D与P*/
{
for(w=0; w<G.numVertexes; ++w)
{
(*D)[v][w]=G.matirx[v][w]; /*D[v][w]值即为对应点间的权值*/
(*P)[v][w]=w; /*初始化P*/
}
}
for(k=0; k<G.numVertexes; ++k)
{
for(v=0; v<G.numVertexes; ++v)
{
for(w=0; w<G.numVertexes; ++w)
{
if((*D)[v][w]>(*D)[v][k]+(*D)[k][w])
{
/*如果经过下标为k顶点路径比原两点间路径更短*/
/*将当前两点间权值设为更小的一个*/
(*D)[v][w]=(*D)[v][k]+(*D)[k][w];
(*P)[v][w]=(*P)[v][k]; /*路径设置经过下标为k的顶点*/
}
}
}
}
}
for(v=0; v<G.numVertexes; ++v)
{
for(w=v+1; w<G.numVertexes; w++)
{
printf("v%d-v%d weight: %d ", v,w,D[V][W]);
k=P[v][w]; /*获得第一个路径顶点下标*/
printf(" path: %d", v); /*打印源点*/
while(k!=w)
{
printf(" -> %d",k); /*打印路径顶点*/
k=P[k][w]; /*获得下一个路径顶点下标*/
}
printf(" -> %d\n",w); /*打印终点*/
}
printf("\n");
}
7.7 拓扑排序
typedef struct EdgeNode /*边表结点*/
{
int adjvex; /*邻接点域,存储该顶点对应的下标*/
int weight; /*用于存储权值,对于非网图可以不需要*/
struct EdgeNode *next; /*链域,指向下一个邻接点*/
}EdgeNode;
typedef struct VertexNode /*顶点表结点*/
{
int in; /*顶点入度*/
int data; /*顶点域,存储顶点信息*/
EdgeNode *firstedge; /*边表头指针*/
}VertexNode,AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /*图中当前顶点数和边数*/
}graphAdjList,*GraphAdjList;
/*拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路则返回ERROR*/
Statue TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; /*用于栈指针下标*/
int count=0; /*用于统计输出顶点的个数*/
int *stack; /*建栈存储入度为0的顶点*/
stack=(int *)malloc(GL->numVertexes * sizeof(int));
for(i=0;i<GL->numVertexes;i++)
if(GL->adjList[i].in==0)
stack[++top]=i; /*将入度为0的顶点入栈*/
while(top!=0)
{
gettop=stack[top--]; /*出栈*/
printf("%d -> ",GL->adjList[gettop].data); /*打印此顶点*/
count++; /*统计输出顶点数*/
for(e=GL->adjList[gettop].firstedge;e;e=e->next)
{
/*对此顶点弧表遍历*/
k=e->adjvex;
if(!(--GL->adjList[k].in)) /*将k号顶点的邻接点的入度减1*/
stack[++top]=k; /*若为0则入栈,以便于下次循环输出*/
}
}
if(count<GL->numVertexes) /*如果count小于顶点数,说明存在环*/
return ERROR;
else
return OK;
}
7.8 关键路径
int *etv,*ltv; /*事件最早发生时间和最迟发生时间数组*/
int *stack2; /*用于存储拓扑序列的栈*/
int top2; /*用于stack2的指针*/
/*拓扑排序,用于关键路径计算*/
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; /*用于栈指针下标*/
int count=0; /*用于统计输出顶点的个数*/
int *stack; /*建栈将入度为0的顶点入栈*/
stack=(int *)malloc(GL->numVertexes * sizeof(int));
for(i=0;i<GL->numVetexes;i++)
if(0==GL->adjList[i].in)
stack[++top]=i;
top2=0; /*初始化为0*/
etv=(int *)mal1oc(GL->numVertexes*sizeof (int));/*事件最早发生时间*/
for (i=0; i<GL->numvertexes; i++)
etv[i]=0;/*初始化为0*/
stack2=(int *)malloc(GL->numVertexes*sizeof(int));/*初始化*/
while (top!=0)
{
gettop=stack[top--];
count++;
stack2[++top2]=gettop;/*将弹出的顶点序号压入拓扑序列的栈*/
for(e=GL->adjList[gettop].firstedge;e;e=e->next)
{
k-e->adjvex;
if(!(--GL->adjList[k] .in))
stack[++top]=k;
if((etv[gettop]+e->weight)>etv[k])/*求各顶点事件最早发生时间值*/
etv[k]=etv[gettop]+e->weight;
}
}
if (count<GL->numvertexes)
return ERROR;
else
return OK;
}
/*求关键路径,GL为有向图,输出GL的各项关键活动*/
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i,gettop,k,j;
int ete,lte; /*声明活动最早发生时间和最迟发生时间变量*/
TopologicalSort(GL); /*求拓扑序列,计算数组etv和stack2的值*/
ltv=(int *)malloc(GL->numVertexes*sizeof(int)) /*事件最晚发生时间*/
for(i=0;i<GL->numVertexes;i++)
ltv[i]=etv[GL->numvertexes-1]; /*初始化ltv*/
while(top2!=0)
{
gettop=stack2[top2--]; /*将拓扑序列出栈,后进先出*/
for(e=GL->adjList[gettop].firstedge;e;e=e->next)
{
/*求各顶点时间的最迟发生时间ltv的值*/
k=e->adjvex;
if(ltv[k]-e->weight<ltv[gettop]) /*求各顶点事件最晚发生时间ltv*/
ltv[gettop]=ltv[k]-e->weight;
}
}
for(j=0;j<GL->numVertexes;j++) /*求ete,lte和关键活动*/
{
for(e=GL->adjList[j].firstedge;e;e=e->next)
{
k=e->adjvex;
ete=etv[j]; /*活动最早发生时间*/
lte=ltv[k]-e->weight; /*活动最迟发生时间*/
if(ete==lte)
printf("
7.9 总结回顾
第8章 查找
8.1 查找概论
8.2 顺序表查找
/*顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字*/
int Sequential_Search(int *a,int n,int key)
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
return i;
}
return 0;
}
/*有哨兵顺序查找*/
int Sequential_Search2(int *a,int n,int key)
{
int i;
a[0]=key; /*设置a[0]为关键字值,我们称之为“哨兵”*/
i=n; /*循环从数组尾部开始*/
while(a[i]!=key)
{
i--;
}
return i; /*返回0则说明查找失败*/
}
8.3 有序表查找
/*折半查找*/
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; /*定义最低下标为记录首位*/
high=n; /*定义最高下标为记录末位*/
while(low<=high)
{
mid=(low+high)/2; /*折半*/
if(key<a[mid]) /*若查找值比中值小*/
high=mid-1; /*最高下标调整到中位下标小一位*/
else if(key>a[mid]) /*若查找值比中值大*/
low=mid+1; /*最低下标调整到中位下标大一位*/
else
return mid; /*若相等说明mid即为查找到的位置*/
}
return 0;
}
/*插值查找*/
int Interpolation_Search(int *a,int n,int key)
{
int low,high,mid;
low=1;
high=n;
while(low<=high)
{
mid=low+ (high-low) * (key-a[low]) / (a[high]-a[low]); /*插值*/
if(key<a[mid])
high=mid-1;
else if(key>a[mid])
low=mid+1;
else
return mid;
}
return 0;
}
/*斐波那契查找*/
int Fibonacci_Search(int *a,int n,int key)
{
int low,high,mid,i,k;
low=1; /*定义最低下标为记录首位*/
high=n; /*定义最高下标为记录末位*/
k=0;
while(n>F[k]-1) /*计算n位于斐波那契数列的位置*/
k++;
for(i=n;i<F[k]-1;i++) /*将不满的数值补全*/
a[i]=a[n];
while(low<high)
{
mid=low+F[k-1]-1; /*计算当前分隔的下标*/
if(key<a[mid]) /*若查找记录小于当前分隔记录*/
{
high=mid-1; /*最高下标调整到分隔下标mid-1处*/
k=k-1; /*斐波那契数列下标减一位*/
}
else if(key>a[mid]) /*若查找记录大于当前分隔记录*/
{
low=mid+1; /*最低下标调整到分隔下标mid+1处*/
k=k-2; /*斐波那契数列下标减两位*/
}
else
{
if(mid<=n)
return mid; /*若相等则说明mid即为查找到的位置*/
else
return n; /*若mid>n说明是补全数值,返回n*/
}
}
return 0;
}