[知识框架] 线性结构: 线性表 栈 队列 逻辑结构 非线性结构: 树 图 集合 数据结构 存储结构 (物理结构) (三要素) 数据的运算 绪 论 算法定义 五个特征 五个特征: 有穷性 确定性 可行性 输入 输出 时间复杂度 效率的度量 空间复杂度 1.1.2数据结构三要素:存储结构\数据结构\数据运算 1.逻辑结构 集合 结构中的数据元素除了"同属于一个集合"的关系外,别无其他关系 线性结构 结构中的数据元素之间存在一对一的关系 树形结构 结构中的数据元素之间存在一对多的关系 图状或网状结构 结构上的数据 数据的逻辑结构 线性结构 非线性结构 受限线性表 线性表推广 集合 树形结构 图状结构 一般线性表 栈和队列 串 数组 广义表 一般树 二叉树 有向图 无向图 2.数据的存储结构 存储结构是指数据结构在计算机中的表示(又称为映像),也称为物理结构.包括了数据元素的表示和关系的表示. 数据的存储结构是逻辑结构用于计算机语言的实现,它依赖于计算机语言. 数据的存储结构主要有:顺序存储\链式存储\索引存储和散列存储. 1)顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元里. 元素之间的关系由存储单元的邻接关系来体现.其优点是可以实现随机存取,每个元素占用最少的 存储空间,缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片. 2)链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针表示 元素之间的逻辑关系,其优点是不出现碎片现象,充分利用所有存储单元,缺点是每个元素因存储指针 而占用额外的存储空间,并且只能实现顺序存取. 3)索引存储:在存储元素信息的同事,还建立附加的索引表.索引表中的每一项称为索引项, 索引项的一般形式是:(关键字,地址).其优点是检索速度快.缺点是增加了附加的索引表, 会占用较多的存储空间,另外,在增加和删除数据时要修改索引表,因而会花费较多的时间. 4)散列存储:根据元素的关键字直接计算处该元素的存储地址,又称为Hash存储. 其优点是检索,增加和删除节点的操作都很快,缺点是如果散列函数不好,可能出现元素存储单元的 冲突,而解决冲突会增加时间和空间开销. 3.数据的运算 施加在数据上的运算包括运算的定义和实现.运算的定义是针对逻辑结构,指出运算的功能; 运算的实现是针对存储结构的,指出运算的具体操作步骤. 算法的五个重要特性: 1.有穷性 2.确定性 3.可行性 4.输入 5.输出 设计一个好的算法应该考虑达到以下目标: 1.正确性 2.可读性 3.健壮性(鲁棒性) 4.效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间 这两者都与问题的规模有关. 算法效率的度量 1.时间复杂度 2.空间复杂度 [线性表] 线 顺序存储 顺序表 性 表 链式存储 单链表 双链表 循环链表 静态链表 定义:线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列. 其中n为表长,当n=0时,该线性表是一个空表. 若用L命名线性表,则其一般表示如下: L = {a1,a2,a3..an} 除表头元素外,每个元素有且仅有一个直接前驱. 除表尾元素外,每个元素有且仅有一个直接后继. 线性表特点: 1.表中元素个数有限 2.表中元素具有逻辑上的顺序性,在序列中各元素排序有其先后次序. 3.表中元素都有数据元素,每个元素都是单个元素 4.表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间. 5.表中元素具有抽象性.即仅讨论元素间的逻辑关系,不考虑元素究竟表示什么内容. [注]线性表是一种逻辑结构,表示元素之间一对一的相邻关系. 顺序表和链表是指存储结构,两者属于不同层面的概念.注意不要混淆 线性表的基本操作: 主要操作: InitList(&):初始化表 Length(L): LocateElem(L,e): GetElem(L,i): ListInsert(&L,i,e): ListDelete(&L,i,e): PrintList(L) Empty(L) DestoryList(&L) 线性表的顺序表示: 顺序表的定义: 线性表的顺序存储又称为顺序表.它是用一组地址连续的存储单元,一次存储线性表的数据元素, 从而使得逻辑上相邻的两个元素在物理位置上也相邻. 第一个元素存储子啊线性表的起始位置,第i个元素的存储位置后面紧接存储的是第i+1个元素. 因此,顺序表的特点是表中元素的逻辑顺序与其物理顺序相同. 假定线性表的元素类型为ElemType,线性表的顺序存储类型描述为: #define MaxSize 50 //定义线性表的最大长度 typedef struct{ ElemType data[MaxSize]; //顺序表的元素 int length; //顺序表的当前长度 }sqList; //顺序表的类型定义 动态存储语句分配内存空间 #define InitSize 100 //表长度的初始定义 typedef struct{ ElemType *data; //指示动态分配数组的指针 int MaxSize,length; //数组的最大容量和当前个数 }seqList; //动态分配数组顺序表的类型定义 C++的初始动态分配语句为: L.data = new ElemType[InitSize]; 顺序表上基本操作的实现: (1)插入操作 bool ListInsert(sqList &L, int i, Elemtype e){ //实现将元素e插入到顺序表L中的第i个位置 //判断i的范围是否有效 if (i < 1 || i > L.length + 1){ return false; } //当前存储空间已满,不能插入 if (L.length >= MaxSize){ return false; } //将第i个元素及之后的元素后移 for (int j = L.length; j >= i; j--){ L.data[j] = L.data[j-1]; } //在位置i处放入e L.data[i-1] = e; //线性表长度加1 L.length++; return true; } (2)删除操作 bool ListDelete(SqList &L, int i, Elemtype &e){ //判断i的范围是否有效 if (i < 1 || i > L.length){ return false; } //将被删除的元素赋值给e e = L.data[i-1]; //将第i个位置之后的元素前移 for (int j = i; j < L.length; j++){ L.data[j-1] = L.data[j]; } //线性表长度减1 L.length--; return true; } (3)按值查找(顺序查找) 在顺序表L中查找第一个元素值等于e的元素,并返回其位序. int LocateElem(SqList L, ElemType e){ //实现查找顺序表中值为e的元素,如果查找成功,返回元素位序,否则返回0 int i; for (i = 0; i < L.length; i++){ if (L.data[i] == e){ //下标为i的元素值等于e,返回其位序i+1 return i+1; } } return 0; } 线性表的链式表示 链式存储线性表时,不需要使用地址连续的存储单元,即它不要求逻辑上相邻的两个元素在物理位置 上也相邻,它是通过"链"建立起数据元素之间的逻辑关系.因此,对于线性表的插入,删除不需要 移动元素,而只需要修改指针. 单链表的定义 单链表结点结构:data->next data为数据域,存放数据元素 next为指针域,存放其后继结点的地址 单链表中结点类型的描述如下: typedef struct LNode{ //定义单链表结点类型 ElemType data; //数据域 struct LNode *next; //指针域 }LNode,*LinkList; 利用单链表可以解决顺序表需要大量的连续存储空间的缺点,但是单链表附加指针域,也存在浪费 存储空间的缺点.由于单链表的元素是离散地分布在存储空间中,所有单链表是非随机存取的存储 结构,即不能直接找到表中某个特定的结点.查找某个特定的结点时,需要从表头开始遍历,依次查找. 通过用头指针来标识一个单链表.如单链表L,头指针为"NULL"时则表示一个空表. 此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点. 头结点的数据域可以不设任何信息,也可以记录表长等相关信息.头结点的指针域指向线性表 的第一个元素结点. 头结点和头指针的区分:不管带不带头结点,头结点始终指向链表的第一个结点, 而头结点是带头结点链表中的第一个结点,结点内通常不存储信息. 引入头结点后,可以带来两个优点: 1.由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和表的其他 位置上的操作一致,无须进行特殊处理. 2.无论链表是否为空,其头结点是指向头结点的非空指针(空表中头结点的指针域为空), 因此空表和非空表的处理也就统一了. 单链表上基本操作的实现 1.采用头插法建立单链表 该方法:从一个空表开始,生成新结点,并将读取到的数据存取到新结点的数据域中,然后将 新结点插入到当前链表的表头,即头结点之后. 头插法建立单链表的算法如下: LinkList createList1(LinkList &L){ //从表尾到表头逆向建立单链表L,每次均在头结点之后插入元素 LNode *s; int x; //创建头结点 L = (LinkList)malloc(sizeof(LNode)); //初始为空链表 L -> next = NULL; //输入结点的值 scanf("%d",&x); //输入9999表示结束 while(x != 9999){ s = (LNode*)malloc(sizeof(LNode)); s -> data = x; s -> next = L -> next; //将新节点插入表中,L为头结点 L -> next = s; //while结束 scanf("%d",&x); } return L; } 2.采用尾插法建立单链表 尾插法建立单链表的算法如下: LinkList createList2(LinkList &L){ //从表头到表位正向建立单链表L,每次均在表尾插入元素 int x; L = (LinkList)malloc(sizeof(LNode)); //r为表尾指针 LNode *s,*r=L; //输入结点的值 scanf("%d",&x); //输入9999表示结束 while (x != 9999){ s = (LNode*)malloc(sizeof(LNode)); s -> data = x; r -> next = s; r = s; scanf("%d", &x); } //尾结点指针置空 r -> next = NULL; return L; } 3.按序号查找结点值 在单链表中从第一个结点出发,顺着指针next域逐个往下搜索,直到找到第i个结点为止, 否则返回最后一个结点指针域NULL. 按序号查找结点值的算法如下: LNode *GetElem(LinkList L, int i){ //取出单链表L(带头结点)中第i个位置的结点指针 //计数,初始化为1 int j = 1; //头结点指针赋给p LNode *p = L->next; if (i == 0){ //若i等于0,则返回头结点 return L; } if (i < 1){ //若i无效,则返回NULL return NULL; } //从第i个结点开始找,查找第i个结点 while (p && j < i){ p = p -> next; j++; } //返回第i个结点的指针,如果i大于表长 //p=NULL 直接返回p即可 return p; } 4.按值查找表结点 从单链表第一个结点开始,由前往后依次比较表中各结点数据域的值. 若某结点数据域的值等于给定值e,则返回该结点的指针; 若整个单链表没有这样的结点,则返回NULL 按值查找结点的算法如下: LNode *LocateElem(LinkList L, ElemType e){ //查找单链表L(带头结点)中数据域等于e的结点指针,否则返回NULL LNode *p = L -> next; //从第1个结点开始查找data域为e的结点 while (p != NULL && p -> data != e){ p = p -> next; } //找到后返回该结点指针,否则返回NULL return p; } 5.插入结点操作 插入操作是将值为x的新结点插入到单链表的第i个位置上. 先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在插入新结点. p = GetElem(L, i-1); //查找插入位置的前驱结点 s -> next = p->next; p -> next = s; 后插法: //将*s结点插入到*p之前的主要代码片段/ //修改指针域,不能颠倒 s -> next = p -> next; p -> next = s; //交换数据域部分 temp = p -> data; p -> data = s -> data; s -> data = temp; //删除结点操作 删除操作是将单链表的第i个结点删除.先检查删除位置的合法性,然后查找表中第i-1个结点. 即被删操作的前驱结点,再将其删除. p = GetElem(L,i-1); //查找删除位置的前驱结点 q = p -> next; //令q指向被删除结点 p -> next = q -> next; //将*q结点从链中"断开" free(q); //释放结点的存储空间 双链表 单链表结点中只有一个指向其后继的指针.这使得单链表只能从头结点依次顺序地向后遍历. 若要访问某个结点的前驱结点(插入,删除操作时),只能从头开始遍历,访问后继结点的时间 复杂度为O(1),访问前驱结点的时间复杂度为O(n). 双链表中结点类型的描述如下: typedef struct DNode{//定义双链表结点类型 //数据域 ElemType data; //前驱和后继指针 struct DNode *prior,*next; }DNode,*DLinklist; 1.双链表的插入操作 在双链表中p所指的结点*s; 插入操作的代码片段如下: //将结点*s插入到结点*p之后 s -> next = p -> next; p -> next -> prior = s; s -> prior = p; p -> next = s; 2.双链表的删除操作 删除双链表中结点*p的后继结点*q: 删除操作的代码片段如下: p-> next = q -> next; q -> next -> prior = p; //释放结点空间 free(q); 循环链表 1.循环单链表 循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是指向头结点, 从而整个链表形成一个闭环. 在循环单链表中,表尾元素*r的next区域指向L,故表中没有指针域为NULL的结点,因此. 循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针. 循环单链表的插入,删除算法与单链表的几乎一样,所不同的是如果操作是在表位进行,则执行的操作 不相同.以让单链表继续保持循环的性质.因此,在任何一个位置上的插入和删除操作都是等价的, 无须判断是否是表尾. 2.循环双链表 与循环单链表不同的是,在循环双链表中,头结点的prior指针还要指向表尾结点. 在循环双链表L中,某结点*p为尾结点时,p->next==L;当循环双链表为空表时,器头结点 的prior域和next域都等于L. 静态链表 静态链表是借助数组来描述线性表的链式存储结构.结点也有数据域data和指针域next. 这里的指针是结点的相对地址(数组下标),又称为游标. 和顺序表一样,静态链表也要预先分配一块连续的内存空间. 静态链表结构类型的描述如下: #define MaxSize 50 //静态链表的最大长度 typedef struct{ //静态链表结构类型的定义 ElemType data; //存储数据元素 int next; //下一个元素的数组下标 }SLinkList[MaxSize]; 第三章 栈和队列 顺序栈 栈 链栈 共享栈 操作受限 循环队列 队列 链式队列 双端队列 线 性 表 推广 一维数组 数组 多维数组:压缩存储,稀疏矩阵 栈 定义:只允许在一端进行插入或删除操作的线性表. 首先,栈是一种线性表,但是限定这种线性表只能在某一段进行插入和删除操作 栈顶:线性表允许进行插入和删除的那一端. 栈底:固定的,不允许进行插入和删除的另一端 空栈:不包含任何元素的空表 栈的一个操作特性可以概括为:后进先出 2.栈的基本操作 InitStack(&S):初始化一个空栈 StackEmpty(S):判断一个栈是否为空 Push(&S,x):进栈,若栈S未满,将x加入使之称为新栈顶 Pop:出栈,若栈S非空,弹出栈顶元素,并用x返回 GetTop(S,&x):读栈顶元素,若栈S非空,用x返回栈顶元素 ClearStack(&S):销毁栈,并释放栈S占用的存储空间. 3.栈的顺序存储结构 1.顺序栈的实现 栈的顺序存储称为顺序栈,它是利用一组地址的存储单元存放自栈底到栈顶的数据元素, 同时蝮蛇一个指针(top)指示当前栈顶的位置. 栈的顺序存储类型可描述为: #define MaxSize 50 //定义栈中元素的最大个数 typedef struct{ Elemtype data[MaxSize]; //存放栈中的元素 int top; //栈顶指针 }SqStack; 栈顶指针:S.top 初始时设置S.top = -1 栈顶元素:S.data[S.top] 出栈操作:栈非空时,先去栈顶元素值,再将栈顶指针减1 栈空条件:S.top=-1; 栈满条件:S.top==MaxSize-1 栈长:S.top+1 由于顺序栈的入栈操作受数组上界的约束,当对栈的最大使用空间估计不足,有可能发生栈上溢. 此时应及时向用户报告信息,以便及时处理,避免出错. 2.顺序栈的基本操作 (1)初始化 void InitStack(&S){ //初始化栈顶指针 s.top = -1; } (2)判断栈是否为空 bool StackEmpty(S){ //栈空 if (s.top==-1) return true; else //非空 return false; } (3)进栈 bool Push(SqStack &S, ElemType x){ //栈满 if (S.top == MaxSize-1){ return false; } //指针先加上1,再入栈 S.data[++S.top] = x; return true; } (4)出栈 bool Pop(SqStack &S, ElemType &x){ //栈空,返回错误 if ( S.top == -1){ return false; } //先出栈,指针再减1 x = S.data[S.top--]; return true; } (5)读栈顶元素 bool GetTop(SqStack S, ElemType &x){ //栈空,报错 if (S.top == -1){ return false; } //x记录栈顶元素 x = S.data[S.top]; return true; } 3.共享栈 利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数据空间,将两个栈的栈底分别 设置在共享空间的两端,两个栈顶向共享空间的中间延伸. 两个栈的栈顶指针都指向栈顶元素,top0= -1时0号栈为空,top1 = MaxSize时1号栈为空; 仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满.当0号栈进栈时top0先加1再赋值, 1号栈进栈时top1先减1再赋值;出栈时刚刚好相反. 共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时 才发生上溢.其存取数据的时间复杂度均为O(1),所以对存取效率没有什么影响. 栈的链式存储结构 采用链式存储的栈被称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在 栈满上溢的情况.通常采用单链表实现,并规定所有操作都是在单链表的表头进行的. 这里规定链栈没有头结点,Lhead指向栈顶. 栈的链式存储类型描述为: typedef struct Linknode{ Elemtype data; //数据域 struct Linknode *next; //指针域 } *LiStack //栈类型定义 三.队列 定义:队列也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一个端进行删除. 向队列中插入元素称为入队或进队;删除元素称为出队或离队; 其操作特性是先进先出,故又称为先进先出的线性表. 队头(Front):允许删除的一端,又称为队首 队尾(Rear):允许插入的一端 空队列:不含任何元素的空表 2.队列常见的基本操作 InitQuene(&Q):初始化队列,构造一个空队列Q QueueEmpty(Q):判队列为空,若队列Q为空,返回ture,否则返回false EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾 DeQueue(&Q,&x):出队,若队列Q非空,删除队头,并用x返回 GetHead(Q,&Q):读队头元素,若队列Q非空,则将队头元素赋值给x 队列的顺序存储类型可描述为: 队列的顺序存储的实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针front 和rear分别指示队头和队尾元素的位置.设队头指针指向队头元素,队尾指针指向队尾元素的下一个位置. 队列的顺序存储类型可描述为: #define MaxSize 50 //定义队列中元素的最大个数 typedef struct{ ElemType data[MaxSize]; //存放队列中元素的最大个数 int front,rear; //队首和队尾元素 }SqQueue; 初始状态 (队空条件):Q.front == Q.rear == 0 进队操作:队不满时,先送值到队尾元素,再将队尾指针加1 出队操作:队不空时,先取对头元素值,再将队头指针加1 2.循环队列 初始时: 队首指针进1: Q.front = Q.rear = 0 队尾指针进1: Q.rear = (Q.front+1)%MaxSize 队列长度:(Q.rear + MaxSize - Q.front)%MaxSize 判断队列是空还是满的情况,有三种处理方式: 1)牺牲一个单元来区分队空还是队满,入队时少用一个队列单元. 约定以"队头指针在队尾指针的下一个位置作为队满的标志" 队满条件:(Q.rear+1)%MaxSize == Q.front 队空条件:Q.front == Q.rear 队列中元素的个数:(Q.rear-Q.front + MaxSize)%MaxSize 2)类型中增设表示元素个数的数据成员: 3)类型中增设tag数据成员,以区别是队满还是队空. tag等于0的情况下,若因删除导致Q.front==Q.rear则为空 tag等于1的情况下,若因插入导致Q.front==Q.rear则为队满 3.循环队列的操作 (1)初始化 void IniQueue(&Q){ //初始化队首队尾指针 Q.rear = Q.front = 0; } (2)判断队空 bool isEmpty(Q){ //队空条件 if (Q.rear == Q.front) return true; else return false; } (3)入队 bool EnQueue(SqQueue &Q, ElemType x){ //队满 if ((Q.rear+1)%MaxSize == front) return false; Q.data[Q.rear] = x; //队尾指针加1取模 Q.rear = (Q.rear+1)%MaxSize; return true; } (4)出队 void DeQueue(SqQueue &Q,ElemType &x){ //队空,报错 if ((Q.rear+1)%MaxSize == Q.front) return false; x = Q.data[Q.front]; //队头指针加1取模 Q.front = (Q.front+)%MaxSize; return true; } 队列的链式存储结构 1.队列的链式存储 队列的链式存储类型可描述为: typedef struct{ //链式队列结点 ElemType data; struct LinkNode *next; }LinkNode; typedef struct{ //链式队列 //队列的队头和队尾指针 LinkNode *front,*rear; }LinkQueue; 当Q.front == NULL 且 Q.rear == NULL时,链式队列为空 链式队列基本操作: (1)初始化 void InitQuene(LinkQueue &Q){ //建立头结点 Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode)); //初始为空 Q.front -> next = NULL; } (2)判断队空 bool IsEmpty(LinkQueue){ if (Q.front == Q.rear) return true; else return false; } (3)入队 void EnQueue(LinkQueue &Q, ElemType x){ s = (LinkNode*)malloc(sizeof(LinkNode)); //创建新结点,插入到链尾 s -> data = x; s -> next = NULL; Q.rear -> next = s; Q.rear = s; } (4)出队 bool DeQueue(LinkQueue &Q, ElemType &x){ //判断队空 if (Q.front == Q.rear) return false; p = Q.front -> next; x = p -> data; Q.front -> next = p -> next; //若原队列中只有一个结点,删除后变为空 if (Q.rear == p) Q.rear = Q.front; free(p); return true; } 双端队列 双端队列是指运行两端都可以进行入队和出队操作的队列. 其元素的逻辑结构,仍是线性结构,将队列的两端分别称为前端和后端,两端都可以入队和出队. 栈在递归中的应用 递归的精髓在于能否将原始问题转化为属性相同但规模较小的问题. 必须注意递归模型不能是循环定义的,其必须要满足两个条件: 1.递归表达式(递归体) 2.边界条件(递归出口) 特殊矩阵的压缩存储 矩阵在计算机图形学\工程计算中占有举足轻重的地位. 在数据结构中考虑的是如何用最小的内存空间来存储同样一组数据. 数组的定义: 数组是有n(n>1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素, 每个元素受n个线性关系的约束,每个元素在n个线性关系中的序号称为该元素在数组中的下标, 并称该数组为n维数组. 数组与线性表的关系:数组是线性表的推广.一维数组可以看成是一个线性表; 二维数组可以看做是线性表的线性表,依次类推.数组一旦被定义,它的维度和维界就不在改变. 因此,处理结构的初始化和销毁之外,数组只会有存储和修改元素的操作. 数组的存储结构 大多数计算机语言都提供数组数据类型,逻辑意义上的数组可以采用计算机语言的数组数据类型进行 存储,一个数组的所有元素在内存中占用一段连续的存储空间. 矩阵的压缩存储 压缩存储:指的是多个值相同的元素只分配一个存储空间,对零元素不分配存储空间. 其目的是为了节省存储空间. 特殊矩阵:指具有许多相同矩阵元素或零元素,并且这些相同矩阵元素或零元素的分布有一定规律性的 矩阵.常见的特殊矩阵有对称矩阵,上(下)三角矩阵,对角矩阵. 特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的值 相同的多个矩阵元素压缩到一个存储空间中. 树和二叉树 概念:定义,存储结构 三种遍历 操作 线索二叉树 二叉树 树 排序二叉树 平衡二叉树 形 应用 结 哈夫曼 构 概念:定义,存储结构 树和森林 与二叉树的转换 操作 遍历 应用:并查集 树的基本概念 树的定义:树是N(N>=0)个结点的有限集合,N=0时,为空树,这是一种特殊情况. 在任意一棵非空树中应该满足: (1)有且仅有一个特定的称为根的结点 (2)当N>1时,其余结点可分为m(m>0)个互不相同的有限集合T1,T2..Tm,其中每个集合 本身又是以棵树,并且称为根结点的子树 显然树的定义是递归的,是一种递归的数据结构.树作为一种逻辑结构,同事也是一种分层结构. 具有以下两个特点: a.树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点. b.树中所有结点可以有零个或多个后继结点. 树适合于表示具有层次结构的数据.树中的某个结点(除根结点外)最多只和上一层的一个结点 (即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中有n-1条边. 而树中每个结点与其下一层的零个或多个结点(即其子女结点)有直接关系. 基本术语 (1) A (2) B C D (3) E F G H I J (4)K L M 1.考虑结点K 根A到结点K的唯一路径上的任意结点,称为结点K的祖先结点. 如果结点B是结点K的祖先结点,而结点K是结点B的子孙结点.路径上最接近结点K的结点 称为K的双亲结点,而K为结点E的孩子结点.根A是树中唯一没有双亲的结点.有相同双亲的结点 称为兄弟结点,如结点K和结点L有相同的双亲结点E,即K和L为兄弟结点. 2.树中一个结点的子结点个数称为该结点的度,树中结点的最大度数称为树的度. 如结点B的度为2,结点D的度为3,树的度为3. 3.度大于0的结点称为分支结点(又称为非终端结点);度为0(没有子女结点)的结点称为叶子结点 (又称终端结点).在分支结点中,每个结点的分支数就是该结点的度. 4.结点的深度,高度和层次 结点的层次:从树根开始定义,根结点为第i层,它的子结点为第2层,依次类推. 结点的深度:从根结点开始自顶而下逐层累加的 结点的高度:从叶结点开始自定向上逐层累加的 树的高度(又称深度)是树中结点的最大层数. 5.有序树和无序树 树中结点的子树从左到右是有次序的,不能交换,这样的树叫做有序树. 有序树中,一个结点其子结点按左往右顺序出现是有关联的.反之则称为无序树. 若将子结点位置互换,则变成一颗不同的树. 6.路径和路径长度 树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过 的边的个数.结点A和结点K的路径长度为3,中间经过结点B和结点E. (注意:由于树的分支是有向的,即从双亲结点指向孩子结点,所以树中的路径是从上向下的, 同一双亲结点的两个孩子之间不存在路径) 7.森林 森林m(m>=0)棵互补相交的树的集合.森林的概念与树的概念十分相近,因为只要把树的根结点删去 就成了树.反之,只要给n棵独立的树加上一个结点,并把这n棵树作为该结点的子树,则森林就变成 了树. 树的性质 树具有如下最基本的性质: 1)树中的结点数等于所有结点的度树+1 2)度为m的树中第i层上至多有m的(i-1)次方个结点(i>=1) 3)高度为h的m叉树至多有(m的h次方-1)/(m-1)个结点 4)具有n个结点的m叉树的最小高度为[logm(n(m-1)+1)] 二叉树的概念 二叉树的定义及其主要特性 定义:二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点), 并且,二叉树的子树有左右之分,其次数不能任意颠倒. 与树相似,二叉树也以递归的形式定义. 二叉树是n(n>=0)的有限集合: 1.n=0,为空的二叉树 2.n>0,由一个根结点和两个互补相交的被称为根的左子树和右子树.左子树和右子树又分别是一棵二叉树. 二叉树是有序树,若将其中左右子树颠倒,就成为另一棵不同的二叉树.即使树中结点只有一棵字数, 也要区分它是左子树还是右子树. 二叉树与度为2的有序树的区别: 1.度为2的有序树至少有3个结点,二叉树可以为空 2.度为2的有序树的孩子结点的左右次序是相对于另一个孩子结点而言的,如果某个结点只有一个 孩子结点,这个孩子结点就无须区分其左右次序,二叉树无论其孩子数是否为2,均需确定其左右次序, 也就是说二叉树的结点次序不是相对于另一个结点而言的,而是确定的. 几个特殊的二叉树 1)满二叉树: 一棵高度为h,并且含有2的h次方-1个结点的二叉树称为满二叉树. 即树中的每一层都含有最多的结点.满二叉树的叶子结点都集成在二叉树的最下一层,并且除了 叶子结点之外的每个结点度数为2. 可以对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起来,自上而下,自左向右. 这样每个结点对应于一个编号.对于编号为i的结点,如果有双亲,其双亲为|L/2|,如果有左孩子, 则左孩子为2i;如果有右孩子,则右孩子为2i+1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (满二叉树) 2)完全二叉树: 设一个高度为h,有n个结点的二叉树,当且仅当其每一个结点都与高度为h的满二叉树中编号为 1~n的结点一一对应时,称为完全二叉树. 完全二叉树的特点如下: 1.若i< |n/2|,则结点i为分支结点,否则为叶子结点. 2.叶子结点只可能在层次最大的两层上出现.对于最大层次中的叶子结点,都依次排列在该层 最左边的位置上. 3.如果有度为1的结点,只可能有一个,且该结点只有左孩子而无右孩子(重要特征) 4.按层序编号后,一旦出现某结点(其编号为i)为叶子结点或只有左孩子,则编号大于i的结点均为 叶子结点. 5.若n为奇数,则每个分支结点都有左子女和右子女; 若n为偶数,则编号最大的分支结点(编号为n/2)只有左子女,没有右子女,其余分支结点.左右结点 左右子女都有. 3)二叉排序树: 一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树; 左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于 根结点的关键字.左子树和右子树又各是一棵二叉排序树. 4)平衡二叉树 树上任一结点的左子树和右子树的深度之差不超过1. 3.二叉树的性质 1)非空二叉树上叶子结点数等于度为2的结点数加1,即N0=N2+1 (N0+N1+N2 = N1+2N2+1 ==> N0 = N2 + 1) 2)非空二叉树上第k层至多有2的k-1次方(k>=1) 3)高度为H的二叉树至多有2的H次方-1个结点(H>1) 4)按从上到下,从左到右的顺序依次编号1,2,3,4..N,则有以下关系: a.当i>1时,结点i的双亲编号为|i/2| 即当i为偶数时,其双亲结点的编号为i/2,它是双亲结点的左孩子 当i为奇数时,其双亲结点的编号为(i-1)/2,它是双亲结点的右孩子 b.当2i<=N时,结点i的左孩子编号为2i,否则无左孩子 c.当2i+1<=N时,结点i的右孩子编号为2i+1,否则无右孩子 d.结点i所在层次(深度)为|log2(i)|+1 5)具有N个(N>0)结点的完全二叉树的高度为|log2(N+1)|或者|log2(N)|+1 二叉树的存储结构 1.顺序存储结构 依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较适合,树中结点的序号可以唯一地反映出 结点之间的逻辑关系,这样即能最大可能地节省存储空间,又可以利用数组的元素的下标值确定 结点在二叉树中的位置,以及结点之间的关系. 区别于树的顺序存储结构与二叉树的顺序存储结构. 在树的顺序存储结构中,数组下标即代表了结点的编号,也指示了树中各结点之间的关系. 而在二叉树的顺序存储结构中,数组下标即代表了结点的编号,也指示了树中各结点之间的关系, 这种关系借助完全二叉树的性质反应.当然,二叉树属于树,因此二叉树都可以用树的存储结构存储, 但树却不都能用二叉树的存储结构来存储. 2.链式存储结构 由于顺序存储对于空间利用率极低,因此一般二叉树都采用链式存储结构. 链式结构是指用一个链表来存储一棵二叉树,二叉树中每一个结点用链表的一个链结点来存储. 在二叉树中,结点结构通常包含若干个数据域和指针域. 二叉链表至少包含三个域:数据域data,左指针域lchild和右指针域rchild. 二叉树的链式存储结构描述如下: typedef struct BiTNode{ //数据区域 ElemType data; //左\右孩子指针 struct BiTNode *lchild,*rchild; }BiTNode,*BiTree; 二叉树的遍历和线索树 所谓二叉树的遍历,是指按某条搜索路径访问树中的每个结点,使得每个结点均被访问一次, 而且仅被访问一次.由二叉树的递归定义可知,遍历一棵二叉树便要决定对根结点N,左子树L 和右子树R的访问顺序.按照先遍历左子树再遍历右子树的原则. 常见的遍历次序有:先序(NLR),中序(LNR),后序(LRN)三种遍历算法. 其中,序指的是根结点在何时被访问. 1.先序排列(NLR) 操作过程: 如果二叉树为空,什么也不做.否则: (1)访问根结点 (2)先序遍历 左子树 (3)先序遍历 右子树 对应的递归算法如下: void PreOrder(BiTree T){ if (T != NULL){ //访问根结点 visit(T); //递归遍历左子树 PreOrder(T->lchild); //递归遍历右子树 PreOrder(T->rchild); } } 2.中序遍历(LNR) 操作过程: 如果二叉树为空,什么也不做.否则: (1)中序遍历 左子树 (2)访问根结点 (3)中序遍历 右子树 对应的递归算法如下: void InOrder(BiTree T){ if (T != NULL){ //递归遍历左子树 InOrder(T->lchild); //访问根结点 visit(T); //递归遍历右子树 InOrder(T->rchild); } } 2.后序遍历(LNR) 操作过程: 如果二叉树为空,什么也不做.否则: (1)后序遍历 左子树 (2)后序遍历 右子树 (3)访问根结点 对应的递归算法如下: void PostOrder(BiTree T){ if (T != NULL){ //递归遍历左子树 PostOrder(T->lchild); //递归遍历右子树 PostOrder(T->rchild); //访问根结点 visit(T); } } [注]三种遍历算法中递归遍历左,右子树的顺序都是固定的,.只是访问根结点的顺序不同. 不管采用哪种遍历算法,每个结点都访问一次且仅访问一次.故时间复杂度都是O(n). 在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏的情况下,二叉树是有n个结点' 且深度为n的单支树,遍历算法的空间复杂度为O(n). 4.递归算法和非递归算法的转换 可以借助栈,将二叉树的递归遍历算法转换为非递归算法. 例如:中序遍历的非递归算法 /*思路: 先扫描(并未访问)根结点的所有左结点并将它们一一进栈. 然后出栈一个结点*p(显然结点*p没有左孩子结点或者左孩子结点均已访问过),则访问它. 然后扫描该结点的右孩子结点,将其进栈. 再扫描该右孩子结点的所有左结点并一一进栈. 如此继续,直到栈为空为止.*/ 中序遍历的非递归算法如下: void InOrder2(BiTree T){ //初始化栈 InitStack(S); //p是遍历指针 BiTree p = T; //栈不空或p不空时循环 while(p || !IsEmpty(S)){ //根指针进栈,遍历左子树 if (p){ //每遇到非空二叉树先向左走 Push(S, p); p = p->lchild; } else {//根指针退栈,访问根结点,遍历右子树 //退栈,访问根结点 Pop(S,p); visit(p); //再向右子树 p = p->rchild; } } } 5.层次遍历 要进行层次遍历需要借助一个队列. 先将二叉树根结点入队,然后出队,访问该结点. 如果它有左子树,则将左子树根结点入队 如果它有右子树,则将右子树根结点入队. 然后出队,对出队结点访问. 如此反复,直到队列为空. 二叉树的层次遍历算法如下: void LevelOrder(BiTree T){ //初始化辅助队列 InitQuene(Q); BiTree P; //将根结点入队 EnQueue(Q,T); //队列不空,进入循环 while (!IsEmpty(Q)){ DeQueue(Q,p); visit(p); if (p->lchild != NULL) EnQueue(Q, p->lchild); if (p->rchild != NULL) EnQueue(Q, P->rchild); } } [注]遍历是二叉树各种操作的基础,可以在遍历的过程中对结点进行各种操作. 如对于一棵已知树求结点的双亲,求结点的孩子结点,求二叉树的深度,求二叉树的叶子结点个数 判断两棵树是否相同等.所有这些操作都建立在二叉树遍历的基础上.因此必须掌握二叉树的各种 遍历过程,并能灵活运行以解决各种问题. 6.由遍历序列构造二叉树 由二叉树的先序序列和中序序列可以唯一地确定一棵二叉树. 在先序遍历序列中,第一个结点一定是二叉树的根结点. 而在中序遍历中,根结点必然将中序序列分割成两个子序列. 前一个子序列就是根结点的左子树的中序序列. 后一个子序列就是根结点的右子树的中序序列. 在先序序列中,左子序列的第一个结点是左子树,右子序列的第一个结点是右子树的根结点. 如此递归地进行下去,便能唯一地确定这棵二叉树. 同理,二叉树的层序序列和中序序列可以唯一地确定一棵二叉树. 二叉树的后序序列和后序序列也可以唯一地确定一棵二叉树 线索二叉树 线索二叉树的基本概念 遍历二叉树就是以一定的规则将二叉树中的结点排列成一个线性序列,从而得到二叉树结点的各种 遍历序列.其实质就是对一个非线性结构进行线性化操作,使在这个访问序列中每一个结点(除第一个 和最后一个)都有一个直接前驱和直接后继. 通过观察,我们发现在二叉链表表示的二叉树中存在大量的空指针,若利用这些空链域存放指向 直接前驱或后继的指针,则可以更方便地运用某些二叉树操作算法. 引入线索二叉树是为了加快查找结点前驱和后继的速度. 在有N个结点的二叉树中,有N+1个空指针 (因为每个叶结点有2个空指针,而每一个度为1的结点有一个空指针,总的空指针,而每一个度 为1的结点有一个空指针,总的空指针为2N0+N1,又有N0=N2+1 所以,总的空指针为N0+N1+N2+1=N+1) 在二叉树线索化时,通常规定:若无左子树,令lchild指向其前驱结点; 若无右子树,令rchild指向其后继结点.还需要增加两个标志域所指对象是指向左(右)子结点 还是直接前驱(后继). |ltag| |lchild| |data| |rchild| |rtag| { 0 lchild域 指示结点的左孩子 ltag { 1 lchild域 指示结点的前驱 { 0 lchild域 指示结点的左孩子 ltag { 1 lchild域 指示结点的前驱 线索二叉树的存储结构描述如下: typedef struct ThreadNode{ ElemType data; //数据元素 struct ThreadNode *lchild,*child; //左右孩子指针 int ltag, rtag; //左右线索标志 }ThreadNode,*ThreadTree; 以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的 指针,叫做线索.加上线索的二叉树称为线索二叉树.对二叉树以某种次序遍历使其变为线索二叉树 的过程叫做线索化. 2.线索二叉树的构造 对二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左右指针 域是否为空,若为空,将它们改为前驱结点或后继结点的线索. 通过中序遍历对二叉树线索化的递归算法如下: void InThread(ThreadTree &p, ThreadTree &pre){ //中序遍历对二叉树线索化的递归算法 if (p != NULL){ //递归,线索化左子树 InThread(p -> lchild, pre); //左子树为空,建立前驱线索 if (p -> lchild == NULL){ p -> lchild = pre; p -> ltag = 1; } if (pre != NULL && pre -> rchild == NULL){ //建立前驱结点的后继线索 pre -> rchild = p; pre -> rtag = 1; } //标记当前结点成为刚刚访问过的结点 pre = p; //递归,线索化右子树 InThread(p->rchild, pre); }//if(p != NULL) } 通过中序遍历建立中序线索二叉树的主过程算法如下: void CreateInThread(ThreadTree T){ ThreadTree pre = NULL; if (T != NULL){ InThread(T, pre); //非空二叉树,线索化 pre -> rchild = NULL; //线索化二叉树 pre -> rtag = 1; //处理遍历的最后一个结点 } } 线索二叉树的遍历 中序线索化二叉树主要是为了访问运算服务的,这种遍历不再需要借助栈.因为它的结点中隐含了 线索二叉树的前驱和后继信息.利用线索二叉树,可以实现二叉树遍历的非递归算法. 不含头结点的线索二叉树的遍历算法如下: 1)求中序线索二叉树中中序序列下的第一个结点: ThreadNode *Firstnode(ThreadNode *p){ while(p->ltag==0){ //最左下结点(不一定是叶结点) p = p -> lchild; } return p; } 2)求中序线索二叉树中结点p在中序序列下的后继结点: ThreadNode *Nextnode(ThreadNode *p){ if (p->rtag==0){ return Firstnode(p->rchild); }else { //rtag == 1直接返回后继线索 return p -> rchild; } } 3)不含头结点的中序线索二叉树的中序遍历的算法 void Inorder(ThreadNode *T){ for (ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p)){ visit(p); } } 树\森林 树的存储结构 树的存储结构有多种,既可以采用顺序存储结构,也可以采用链式存储结构. 但无论采用何种存储方式,都要求能够唯一地反映出树中各结点之间的逻辑关系. 1.双亲表示法 双亲表示发的存储结构描述如下: #define MAX_TREE_SIZE 100 //树中最多结点树 typedef struct PTNode{ //树的结点定义 ElemType data; //数据元素 int parent; //双亲位置域 }PTNode; typedef struct PTree{ //树的类型定义 PTNode nodes[MAX_TREE_SIZE]; //双亲表示 int n; //结点数 }PTree; 该存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲 结点,但是求结点的孩子时却需要遍历整个结构. 2.孩子表示法 孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,则N个结点就有 N个孩子链表(叶子结点的孩子链表为空表) 对于这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历N个结点中孩子 链表指针域所指向的N个孩子链表. 3.孩子兄弟表示法 孩子兄弟表示法又称为二叉树表示法,即以二叉链表作为树的存储结构. 孩子兄弟表示法是每个结点包括三部分内容:结点值,指向结点第一个孩子结点的指针和指向结点 下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点). 孩子兄弟表示法的存储结构描述如下: typedef struct CSNode{ ElemType data; struct CSNode *Firstchild,*nextsibling; }CSNode,*CSTree; 这种存储表示法比较灵活.其最大的优点就是可以方便地实现树转换为二叉树的操作,易于查找 结点的孩子等,但缺点是从当前结点查找双亲比较麻烦.如果为每个结点增设一个parent域指向 其父结点,则查找结点的父结点也很方便. 树,森林与二叉树的转换 由于二叉树和树都可以用二叉链表作为存储结构,则以二叉链表作为媒介可以导出树与二叉树的 一个对应关系,即给定一棵树,可以找到唯一的一棵二叉树与之对应. 从物理结构上来看,树的孩子兄弟表示法与二叉树的二叉链表表示法相同, 即每个结点共有两个指针,分别指向结点第一个孩子和结点的下一兄弟结点,而二叉链表中使用 双指针.因此,就可以用一个同一存储结构的不同解释将一棵树转换转换为二叉树. 树转换为二叉树的规则: 每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点.(左孩子右结点) 由于根结点没有兄弟,所以由树转换而得的二叉树没有右子树. 将森林转换为二叉树的规则与树类似. 先将森林中的每一棵树转换为二叉树,再将第一棵树的根作为转换后的二叉树的根, 第一棵树的左子树作为转换后二叉树根的左子树,第二棵作为转换后的二叉树的右子树 第三棵树作为转换后二叉树根的右子树的右子树,依次类推,就可将森林转换为二叉树. 二叉树转换为森林的规则:若二叉树非空,则二叉树根及其左子树为第一棵的二叉树形式, 二叉树根的右子树又可以看作是由除第一棵树外的森林转换后的二叉树,应用同样的方法, 直到最后产生一棵没有右边子树的二叉树为止,这样就得到了原森林. 二叉树转换为树的规则与此类似,二叉树转换为树或森林是唯一的. 树转换成二叉树的画法: 1.在兄弟结点之间加一连线 2.对每一个结点,只保留它与第一个子结点的连线,与其他子节点的连线全部抹掉 3.与树根为轴心,顺时针旋转45度 森林转换为二叉树的画法: 1.将每棵树的根相连 2.将森林中的每棵树转换成相应的二叉树 3.以第一棵树的根为轴心顺时针旋转45度 树与森林的遍历 树的遍历操作是以某种操作访问树中每一个结点,且仅访问一次. 树的遍历操作主要有先根遍历和后根遍历. (1)先根遍历:若树非空,则先返回根结点,再按从左到右的顺序遍历根结点的每一棵子树. 其访问顺序与这棵树相应二叉树的先序遍历顺序相同. (2)后跟遍历:若树非空,则按从左到右的顺序遍历根结点的每一棵子树,之后再访问根结点. 其访问顺序与这棵树相应二叉树的中序遍历顺序相同. 另外,树也有层次遍历,与二叉树的层次遍历思想基本相同. 即按层序依次访问各结点: 根据森林和树相互递归的定义,可得到森林的两种遍历方法: (1)先序遍历森林. 若森林为非空,则按照如下规则进行遍历: 访问森林中第一棵树的根结点 先序遍历第一棵树中根结点的子树森林. 先序遍历第一棵树中之后剩余的树构成的森林 (2)中序遍历森林. 若森林为非空,则按如下规则进行遍历: 中序遍历森林中的第一棵树的根结点的子树森林 访问第一棵树的根结点 中序遍历除了第一棵树之后剩余的树构成的森林. 树与二叉树的应用 1.二叉排序树的定义 二叉排序树,也称为二叉查找树. 二叉排序树或者一棵空树,或者是一棵具有下列特性的非空二叉树: (1)若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值 (2)若右子树非空,则右子树上所有结点关键字值均大于根结点的关键字值 (3)左右子树本身也是一棵二叉排序树 由此定义可知,二叉排序树是一个递归的数据结构 可以方便地使用递归算法对二叉排序树进行各种运算. 根据二叉排序树的定义,有左子树结点值<根结点值<右子树结点值 所以,对于二叉排序树进行中序遍历,可以得到一个递增的有序序列. 2.二叉排序树的查找 二叉排序树的查找是从根结点开始,沿某一个分支逐层向下进行比较的过程. 若二叉排序树非空,将给定值与根结点的关键字比较,若相等,则查找成功. 若不等,则当根结点的关键字大于给定关键字值时,在根结点的左子树中查找,否则 在根结点的右子树中查找.(显然这是一个递归的过程) 二叉排序树的非递归查找算法: BSTNode *BST_Search(BiTree T, ElemType key, BSTNode *&p){ //查找函数返回指向关键字值为key的结点指针,若不存在,返回NULL //p指向被查找结点的双亲,用于插入和删除操作 p = NULL; while (T != NULL && key != T -> data){ p = T; if (key < T -> data){ T = T -> lchild; } else { T = T -> rchild; } } return T; } 3.二叉排序树的插入 二叉排序树作为一种动态集合,其特点是树的结构通常不是一次生成的,而是在查找过程中,当树 中不存在关键字等于给定值的结点时再进行插入. 由于二叉排序树是递归定义的,插入结点的过程是,若原二叉排序树为空,则直接插入结点; 否则,若关键字k大于根结点关键字,则插入到左子树中. 若关键字k大于根结点关键字,则插入到右子树. int BST_Insert(BiTree &T, KeyType k){ //在二叉排序树T中插入一个关键字为k的结点 //原树为空,新插入的记录为根结点 if (T == NULL){ T = (BiTree)malloc(sizeof(BSTNode)); T -> key = k; T -> lchild = T -> rchild = NULL; return 1; //返回1,表示成功 } //树中存在相同关键字的结点 else if (k == T -> key){ return 0; } //插入到T的左子树 else if (k < T -> key){ return BST_Insert(T->lchild, k); } //插入到T的右子树 else { return BST_Insert(T->rchild, k); } } 4.二叉排序树的构造 void Creat_BST(BiTree &T, KeyType str[], int n){ //用关键字数组str[]建立一个二叉排序树 T = NULL; int i = 0; while (i < n){ BST_Insert(T, str[i]); i++; } } 5.二叉排序树的删除 删除操作的实现过程按照3种情况来处理: 1.如果被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质 2.若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树.替代z的位置 3.若结点z有左右两棵子树,则令z的直接后继(或直接前继)替代z,然后从二叉排序树中 删去这个直接后继(或直接前继),这样就转换成第一或第二种情况. 平衡二叉树 1.平衡二叉树的定义 为了避免树的高度增长过快,降低二叉排序树的性能. 我们规定在插入和删除二叉树结点时,要保证任意结点的左\右子树高度差的绝对值不超过1 将这样的二叉树称为平衡二叉树,简称平衡树(AVL树).定义结点左子树或右子树的高度差为该 结点的平衡因子,则平衡二叉树结点的平衡因子只可能是-1,0,或1 因此,平衡二叉树可定义为它或者是一棵空树,或者是具有下列性质的二茬树: 它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度叉的绝对值不超过1. 2.平衡二叉树的插入 二茬排序树保证平衡的基本思想:每当在二叉排序树中插入(或删除)一个结点时,首先要检查其 插入路径上的结点是否因为此次操作而导致了不平衡.如果导致了不平衡,则先找到插入路径上离 插入结点最近的平衡因子绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的 前提下,调整各结点的位置关系,使之重新达到平衡. 3.平衡二叉树的查找 在平衡二叉树上进行查找的过程与二叉排序树相同. 因此,在查找的过程中和给定值进行比较关键字个数不超过树的深度. 假设以Nh表示深度为h的平衡树中含有的最少结点数.显然,N0=0,N1=1,N2=2 并且有Nh = Nh-1 + Nh-2 + 1. 可以证明:含有n个结点平衡二叉树的最大深度为O(log2N) 因此,平衡二叉树的平均查找长度为(log2N) 图的定义 邻接矩阵法\邻接表法 图结构的存储 邻接多重表\十字链表 深度优先遍历 图的遍历 图 广度有限遍历 最小生成树:Prim算法,Kruskal 图的相关应用 最短路径:Dijkstra算法,Floyd算法 拓补排序:AOV网 关键路径:AOE网 图的基本概念 图的定义: 图G由顶点集V和边集E组成,记为G=(V,E) 其中V(G)表示图G中顶点的有限非空集; E(G)中顶点之间的关系(边)集合.若V={v1,v2,v3..vn} 用|V|表示图G中顶点的个数,也称为图G的阶, E = {(u,v)|u∈V,v∈V},用|E|表示图G中边的条数. 1.有向图 若E是有向边(也称为弧)的有限集合时,则图G为有向图. 弧是顶点的有序对,记为<v,w>,其中v,w是顶点. v称为弧尾,w称为弧头,称为从顶点v到顶点w的弧,也称v邻接到w 例如: G1 = (V1,E1) V1 = {1,2,3,4} E1 = {<1,2>, <2,1>, <2,3>} 2.无向图 若E是无向边(也称为边)的有限集合时,则图G为无向图. 弧是顶点的有序对,记为(v,w),其中v,w是顶点 可以说顶点w和顶点v互为邻接表. 边依附于顶点w和v或者说边(v,w)和顶点v,w相关联. G2 = (V2,E2) V2 = {1,2,3,4} E2 = {(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)} 3.简单图 一个图G如果满足: 1.不存在重复边 2.不存在顶点到自身的边,则称图G为简单图 4.多重图 若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图 多重图的定义和简单图是相对的. 5.完全图(也称简单完全图) 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图. 含有n个顶点的无向完全图,含有n个顶点的无向完全图有n(n-1)/2条边. 在有向图中,如果任意两个顶点之间都存在方向相反的两条弧,则称该图为有向完全图. 含有n个顶点的有向完全图有n(n-1)条有向边. 6.子图 设有两个图G=(V,E)和G'=(V',E') 若V'和V的子集,且E'是E的子集,则称G'是G的子图. 若有满足V(G')=V(G)的子图G',则为G的生成子图. 7.连通,连通图和连通分量 在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的. 若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图. 无向图中的极大连通子图称为连通分量.如果一个图有n个顶点,并且有小于n-1条边 则此图必是非连通图. [注]极大连通子图是无向图的连通分量 极小连通图是既要保持图连通,又要使得边数最少的子图. 8.强连通图,强连通分量 在有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的. 若图中任何一对顶点都是强连通的,则称此图为强连通图.有向图中的极大强连通子图称为有向图的 强连通分量. 9.生成树,生成森林 连通图的生成树是包含图中全部顶点的一个极小连通子图. 若图中顶点数为n,则它的生成树含有n-1条边.对于生成树而言,若砍去它的一条边, 则会变成非连通图,若加上一条边则会形成一个回路.在非连通图中,连通分量的生成树 构成了非连通图的生成森林. [注]包含无向图中全部顶点的极小连通子图,只有生成树满足条件. 因为砍去生成树的任一条边,图将不再连通. 10.顶点的度,入度和出度 图中每个顶点的度定义为以该点为一个端点的边的树目 对于无向图,顶点v的度是指依附于该顶点的边的条数,记为TD(v). (无向图的全部顶点的度之和等于边数的两倍,因为每条边和两个顶点相联系) 11.边的权和网 在一个图中,每条边都可以标上某种含义的数值,该数值称为该边的权值. 这中边上带有权值的图称为带权图,也称为网. 12.稠密图,稀疏图 边数很少的图称为稀疏图,反之称为稠密图. 稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的. 一般当图G满足|E|< |V|*log|V|时,可以将G看成是稀疏图. 13.路径,路径长度和回路 顶点Vp到顶点Vq之间的路径是指顶点序列Vp,Vi1,Vi2,Vim,Vq.路径上边的数目称为路径长度. 第一个顶点和最后一个顶点相同的路径称为回路或环.如果一个图有n个顶点,并且有大于n-1 条边,则此图一定有环. 14. 简单路径,简单回路 在路径序列中,顶点不重复出现的路径称为简单路径.除第一个顶点和最后一个顶点之外,其余顶点 不重复出现的回路称为简单回路. 15. 距离 从顶点u出发到顶点v的最短距离若存在,则此路径的长度称作从u到v的距离. 若从u到v根本不存在路径,则记该距离为无穷(∞) 16. 有向图 有一个顶点的入度为0,其余顶点的入度均为1的有向图称作有向树. 图的存储及基本操作 图的存储必须要完整,准确地反映点集和边集的信息.根据不同图的结构和算法,可以采用不同的 存储方式,但不同的存储方式将队程序的效率产生相当大的影响.因此,所选的存储结构应适合于 欲求解的问题. 无论是无向图还是有向图,主要的存储方式都有两种:邻接矩阵和邻接表. 前者属于图的顺序存储结构,后者属于图的链接存储结构. 邻接矩阵法: 所谓邻接矩阵存储,就是用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息. (即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵. 结点数为n的图G=(V,E)的邻接矩阵A是n*n的.将G的顶点编号为v1,v2..vn. 若(vi,vj)∈E,则A[i][j] = 1,否则A[i][j] = 0. A[i][j] ={ 1 若(vi,vj)或<vi,vj>是E(G)中的边 { 0 若(vi,vj)或<vi,vj>不是E(G)中的边 对于带权图而言,若顶点vi和vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值. 若顶点vi和vj不相连,则用∞来代表这两个顶点之间不存在边. A[i][j] ={ wij 若(vi,vj)或<vi,vj>是E(G)中的边 { 0或无穷大 若(vi,vj)或<vi,vj>不是E(G)中的边 图的邻接矩阵存储结构定义如下: #define MaxVertexNum 100 //顶点数目的最大值 typedef char VertexType; //顶点的数据类型 typedef int EdgeType; //带权图中边上权值的数据类型 typedef struct{ VertexType Vex[MaxVertexNum]; //顶点表 EdgeType Edge[MaxVertexNum][MaxVertexNum+1]; //邻接矩阵 int vexnum,arcnum; //图的当前顶点数和弧数 }MGraph; 图的邻接表存储方法有以下特点: 1.如果G为无向图,则所需的存储空间O(|V|+2|E|) 如果G为有向图,则所需的存储空间为O(|V|+2|E|) 前者的倍数2是由于无向图中,每条边在邻接表中出现了两次 2.对于稀疏图,采用邻接表表示将极大地节省存储空间 3.在邻接表中,给定一顶点,能很容易地找出它的所有邻边. 只需要读取它的邻接表即可.在邻接矩阵中,相同的操作则需要扫描一行,花费时间为O(n) 但是,如果要确定给定的两个顶点间是否存在边,则在邻接矩阵里可以立刻查到,在邻接表中则 需要在相应结点对应的边表中查找另一结点,效率极低. 4.在有向图的邻接表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数即可; 但求其顶点的入度,则需要遍历全部的邻接表.因此也有人采用逆邻接表的存储方式来加速求解 给定顶点的入度. 5.图的邻接表表示并不唯一,这是因为在每个顶点对应的单链表中,各边结点的链接次序可以是 任意的,取决于建立邻接表的算法以及边的输入次序. 十字链表 十字链表是有向图的一种链式存储结构.在十字链表中,对应于有向图中的每条弧有一个结点, 对应于每个顶点也有一个结点.这些结点的结构如下: 弧结点 顶点结点 tailvex headvex hlink tlink info data firstin firstout 弧结点中有五个域: 其中尾域(tailvex)和头域(headvex)分别指示弧尾和弧头这两个顶点在图中的位置. 链域hlink指向弧头相同的下一条弧,链域tlink指向弧头相同的下一条弧. info域指向该弧的相关信息.这样弧头相同的弧在同一个链表上,弧尾相同的弧也在同一个链表上. 顶点结点中有三个域:data域存放顶点相关的数据信息,如顶点名称. firstin和firstout两个域分别指向以该顶点为弧头或弧尾的第一个弧结点. 图的十字链表存储结构定义如下: #define MaxVertexNum 1000 //图中顶点数目的最大值 typedef struct ArcNode{ //边表结点 int tailvex, headvex; //该弧的头结点 //分别指向弧头相同和弧尾相同的结点 struct ArcNode *hlink,*tlink; //InfoType info //相关指针信息 }ArcNode; typedef struct VNode{ //顶点表结点 VertexType data; //顶点信息 ArcNode *firstin, *firstout; //指向第一条入弧和出弧 }VNode; typedef struct{ //邻接表 VNode xlist[MaxVertexNum]; //图的顶点数和弧数 int vexnum,arcnum; //GLGraph是以十字邻接存储的图类型 }GLGraph; [注]在十字链表中,既容易找到vi为尾的弧,也容易找到vi为头的弧度,因而容易求得顶点的出度和入度. 图的十字链表表示是不唯一的,但是一个十字链表表示确定一个图. 邻接多重表 邻接多重表是无向图的另一种链式存储结构. 在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求得两个顶点之间是否存在边,或需要对 边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率极低. 与十字链表类似,在邻接多重表中,每一条边用一个结点表示,其结构如下: |mark| |ivex| |ilink| |jvex| |jlink| |info| 其中,mark为标志域,用以标记该条边是否被搜索. ivex和jvex为该边依附的两个顶点在图中的位置; ilink指向下一条依附顶点ivex的边,jlink指向下一条依附于顶点jvex的边. info为指向和边相关的各种信息的指针域 每一个顶点也用一个结点表示,它由如下所示的两个域组成 | data | firstedge | 其中,data域存储该顶点的相关信息,firstedge域指示第一条依附于该顶点的边. 在邻接多重表中,所有依附于同一个顶点的边串联在同一链表中,由于每条边依附于两个顶点, 则每个边结点同时链接在两个链表中. 图的邻接多重表存储结构定义如下: #define MaxVertexNum 100 //图中顶点数目的最大值 typedef struct ArcNode{ //边表结点 bool mark; //访问标记 int ivex,jvex; //分别指向该弧的两个结点 struct ArcNode *ilink, *jlink; //分别指向两个顶点的下一条边 //InfoType info //相关信息指针 }ArcNode; typedef struct VNode{ //顶点表结点 VertexType data; //顶点信息 ArcNode *firstedge; //指向第一条依附该顶点的边 }VNode; typedef struct{ VNode adjmulist[MaxVertexNum]; //邻接表 int vexnum,arcnum; //图的顶点数和弧数 }AMLGraph; //AMLGraph是以十字邻接存储的图类型 图的存储及基本操作 图的基本操作是独立于图的存储结构的.而对于不同的存储方式,操作算法的具体实现会有着不同 的性能.基本操作有以下: Adjacent(G,x,y): 判断图G是否存在边<x,y>或(x,y) Neighbors(G,x): 列出图G中结点x邻接的边 InsertVertex(G,x): 在图G中插入顶点x DeleteVertex(G,x): 在图G中删除顶点x AddEdge(G,x): 如果无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边 RemoveEdge(G,x,y): 如果无向边(x,y)或有向边<x,y>存在,则向图G中添加该边 FirstNeighbor(G,x): 求图G中顶点x的第一个邻接点,若有则返回顶点号.若x没有邻接点或 图中不存在x,则返回-1 NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点, 返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1. Get_edge_value(G,x,y):获取图G中边(x,y)或<x,y>对应的权值 Set_edge_value(G,x,y,v):设置图G中边(x,y)或者<x,y>对应的权值为v. 此外,还有图的遍历算法:按照某一种方式访问图中每一个顶点且仅访问一次. 图的遍历算法包括深度优先遍历和广度优先搜索. 图的遍历 图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中所有顶点访问依次 且访问一次.注意到树是一种特殊的图.所以树的遍历实际上也可以看作是一种特殊的图的遍历. 图的遍历是图的一种最基本操作,其他许多操作都建立在图的遍历操作基础之上. 图的遍历主要有两种算法:广度优先搜索和深度优先搜索 包括广度优先搜索和深度优先搜索在内的几乎所有图的搜索算法都可以抽象为优先级搜索,或最佳优先搜索. 广度优先搜索会优先考虑最早被发现的顶点,也就是说离起点越近的顶点优先级别越高. 深度优先搜索会优先考虑最后被发现的顶点. 广度优先搜索类似于二叉树的层序遍历算法. 它的基本思想是:首先访问起始顶点v,接着由v出发依次访问v的各个未访问的邻接顶点w1,w2,wi, 然后再依次访问w2..wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,在访问它们所有 未被访问过的邻接顶点..依次类推,直到图中所有顶点都被访问过为止. 类似的思想,还将应用于Dijkstra单源最短路径算法和Prim最小生成树算法. 广度优先算法是一种分层的查找过程. 每向前走异步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法. 为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点. 一般情况下,其递归形式的算法十分简洁,过程如下: bool Visited[MAX_VERTEX_NUM]; //访问标记数组 void BFSTraverse(Graph G){ //对于图进行深度优先遍历,访问函数为visit() for (i=0; i <G.vexnum; ++i){ //初始化已访问标记数据 visited[i] = FALSE; } //初始化辅助队列Q InitQueue(Q); //对于0号顶点开始遍历 for (i=0; i < G.vexnum; ++i){ //对每个连通分量调用一次BFS if (!visited[i]){ //vi未访问,从Vi开始BFS BFS(G,i); } } } void BFS(Graph G, int v){ //从顶点v出发,广度优先遍历图G //算法借助一个辅助队列Q visit(v);//访问初始顶点v visited[v] = TRUE; //对v做已访问标记 EnQueue(Q,v);//顶点v入队列 while (!isEmpty(Q)){ DeQueue(Q,v); //检测v所有邻接表 for (w = FirstNeighbor(G,v); w >= 0; w = NextNeighbor(G,v,w)){ //w为v的尚未访问的邻接顶点 if (!visited[w]){ //访问顶点w visit(w); //对w做已访问标记 visited[w] = TRUE; //顶点w入队列 EnQueue(Q,w); }//if } }//while } 辅助数组visited[]标志顶点是否被访问过,它的初始状态为FALSE. 在图中的遍历过程中,一旦某一个顶点vi被访问,就立即置visited[i]为TRUE,防止它被多次访问 BFS算法的性能分析: 无论是邻接表还是邻接矩阵的存储方式,BFS都需要借助一个辅助队列Q. n个顶点均需入队一次,在最坏的情况下,空间复杂度为O(|V|) 当采用邻接表存储方式时,每个顶点均需搜索一次.故时间复杂度为O(|V|). 在搜索任一顶点的邻接点时,每个边至少访问一次,故时间复杂度为O(|E|). 算法总的时间复杂度为O(|V|+|E|).当采用邻接矩阵存储方式时,查找每个顶点的邻接点 所需时间为O(|V|),故算法总的时间复杂度为O(|V*V|) 2.BFS算法求解单源最短路径问题 如果图G = (V,E)为非带权图,定义从顶点u到顶点v的最短路径d(d,v)为从u到v的任何路径 中最少的边数;如果从u到v没有通路,则d(d,v)=∞. 使用BFS,我们可以求解一个满足上述定义的非带权图的单源最短路径,这是由广度优先搜索总是按照 距离由近到远来遍历图中每个顶点的性质决定的. BFS算法求解单源最短路径问题的算法如下: void BFS_MIN_Distance(Graph G,int u){ //d[i]表示从u到i结点的最短路径 for (i=0; i < G.vexnum; ++i){ d[i] = ∞; } visited[u] = TRUE; d[u] = 0; EnQueue(Q,u); while (!isEmpty(Q)){ DeQueue(Q,u); for (w=FirstNeighbor(G,u); w>=0; w=NextNeighbor(G,u,w)){ if (!visited[w]){ visited[w] = TRUE; d[w] = d[u] + 1; EnQueue(Q,w); }//if }//while } } 3.广度优先生成树 在广度遍历的过程中,我们可以的一棵遍历树,称为广度优先生成树. 需要注意的是,一给定图的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的, 但由于邻接表存储表示不唯一,因此广度优先生成树也是不唯一的. 深度优先搜索 与广度优先搜索不同,深度优先搜索(DFS)类似于树的先序遍历. 它的基本思想是:首先访问图中某一个其初始顶点v,然后从v出发,访问与v邻接且未被访问的 任一顶点w1,再访问与w1邻接且未被访问过的顶点,若它还有邻接顶点未被访问过,则从该点开始 继续上述搜索过程,直到图中所有顶点均被访问过为止. 一般情况下,其递归形式的算法十分简洁.下面描述其算法过程: bool visited[MAX_VERTEX_NUM]; // void DFSTraverse(Graph G){ //对图G进行深度优先遍历,访问函数为visit() for (v=0; v < G.vexnum; ++v){ visited[v] = FALSE; } for (v=0; v <G.vexnum; ++v){ if (!visited[v]){ DFS(G,v); } } } void DFS(Graph G,int v){ //从顶点v出发,采用递归思想,深度优先遍历图G visit(v); //访问顶点v visited[v] = TRUE; //设已访问标记 for (w = FirstNeighbor(G,v); w>=0; w= NextNeighbor(G,v,w));{ if (!visited[w]){ //w为u的尚未访问的邻接顶点 DFS(G,S); }//if } } [注]图的邻接矩阵表示是唯一的,但对于邻接表来说,如果边的输入次序不同,生成的邻接表也 不同.因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于 邻接表的遍历所得到的DFS序列和BFS序列是不唯一的. 1.DFS算法的性能分析 DFS算法是一个递归算法,需要借助一个递归工作栈,因它的空间复杂度为O(|V|). 遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所采用的存储结构. 当以邻接矩阵表示时,查找每个顶点的邻接表所需时间为O(|V|).因此总的时间复杂度为O(|V|2) 当以邻接表表示时,查找所有顶点的邻接表所需时间为O(|E|),因此访问顶点所需时间为O(|V|), 此时,总的时间复杂度为O(|V|+|E|) 2.深度优先的生成树和生成森林 与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树. 也即对于连通图DFS才可以产生深度优先生成树,否则产生的将是深度优先生成树. 和BFS类似,基于邻接表存储的深度优先生成树是不唯一的. 图的遍历与图的连通性 图的遍历算法可以用来判断图的连通性. 对于无向图来说,如果无向图是连通的,则从任一结点出发,仅需一次遍历就能够访问图中所有顶点. 如果无向图是非连通的,则从某一个顶点出发,依次遍历只能访问到该顶点所在连通分量的所有顶点. 而对于图中其他连通分量的顶点,则无法通过这次遍历访问.对于有向图来说,若从初始点到图中的 每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点. 因此在BFSTraverse()或DFSTraverse()中添加了第二个for循环,再选取初始点,继续进行遍历, 以防一次无法遍历图的所有顶点.对于无向图,上述两个函数调用BFS(G,i)或DFS(G,i)的次数 等于该图的连通分量数;而对于有向图,则不是这样,因为一个连通的有向图分为强连通和非强连通的 它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用BFS(G,i)或者DFS(G,i) 无法访问到该连通分量的所有顶点. 图的相关应用 1.最小生成树: 一个连通图的生成树是图的极小连通子图,它包含图中的所有顶点,并且只含尽可能少的边. 这意味着对于生成树来说,若是砍去它的一条边,就会使得生成树编程连通图;若给它增加 一条边就会形成图中的一条回路. 对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和) 也可能不同.设T为G的所有生成树的集合,若t为T中边的权值之和最小的那棵生成树, 则T称为G的最小生成树. 不难看出,最小生成树具有如下性质: 1.最小生成树不是唯一的,即最小生成树的树形不唯一. T中可能有多个最小生成树.当图G中的各边权值互不相等时,G的最小生成树是唯一的. 若无向连通图G的边比顶点数少1,即G本身就是一棵树时,G的最小生成树就是它本身. 2.最小生成树的边的权值之和总是唯一的,虽然最小生成树不唯一,但其对应的边的权值之和总是 唯一的,而且是最小的. 3.最小生成树的边数为顶点数减1 构造最小生成树有多种算法,但大多数算法都利用最小生成树的下列性质: 假设G=(V,E)是一个带权连通无向图,U是顶点集V的一个非空集. 若(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树 基于该性质的最小生成树的算法主要有: Prim算法和Kruskal,它们都基于贪心算法的策略.对于这两种算法的掌握不应该拘泥于其代码的 实现,而应该掌握算法的本质含义和基本思想,并能够通过手工模拟算法的实现步骤. 通用的最小生成树算法: GENERIC_MST(G){ T = NULL; while T 未形成一棵生成树; do 找到一条最小代价边(u,v)并且加入T加入T后会不会产生回路; T = T U (u,v); } 通用的算法采用每次加入一条边来逐渐形成一棵树. 1.Prim普里姆算法 Prim算法的执行非常类似于寻找图的最短路径的Dijkstra算法. 假设N={V,E}是连通图,Er是N上最小生成树中边的集合. 算法从Vr={uo}(uo∈V),Er={}开始,重复执行下述操作,在所有u∈Vt,v∈V-Vt的边中找一条 代价最小的边(uo,vo)并入集合Er,同时将vo并入Vt,直至Vr=V为止.此时Er中必有n-1条边. 则T={Vt,Er}为N的最小生成树. 简单实现如下: void Prim(G,T){ T = 空; //初始化空树 U = {w}; //添加任一顶点 //若树中不包含全部顶点 while((V-U)!=空){ 设(u,v)是使u∈U与v∈(V-U),且权值最小的边 T = TU{(u,v)}; //边归入树 U = U U{v}; //顶点归入树 } } Prim算法的时间复杂度为O(|V2|),不依赖于|E|.因此它适用于求解边稠密的图的最小生成树 虽然采用其他方法可以改进Prim算法的时间复杂度,但增加了实现的复杂性. 2.Kruskal克鲁斯卡尔算法 与Prim算法从顶点开始扩展最小生成树不同,Kruskal算法是一种按权值的递增次序选择合适的边 来构造最小生成树的方法.假设N=(V,E)是连通网,对应的最小生成树T=(Vt,Et). Kruskal克鲁斯卡尔算法简单实现如下: void Kruskal(V,T){ T = V; //初始化树T,仅含顶点 numS = n; //连通分量数 // while (numS > 1) { 从E中取出权值最小的边(v,u); if (v和u属于T中不同的连通分量){ T = T U {(v,u)}; //将此边加入生成树中 numS--; //连通分量数-1 } } } 最短路径 广度优先搜索查找最短路径只是对无权图而言,若图是带权图,则把从一个顶点v0到图中其余任一顶点 vi的一条路径(可能不止一条)上所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径 长度最短的那条路径也称作最短路径. 求解最短路径的算法通常都依赖于一种性质,也就是两点之间的最短路径也包含了路径上其他顶点 的最短路径.带权有向图G的最短路径问题,一般可以分为两类:一是单源最短路径,即求图中某一 顶点到其他各顶点的最短路径,可通过经典的Dijkstra算法求解:二是求每一对顶点的最短路径, 可通过Floyd-Warshall算法来求解. Floyd算法求解各顶点之间最短路径问题 求所有顶点之间的最短路径问题描述如下:已知一个各边权值均大于0的带权有向图,对每一对 顶点vi≠vj,要求求出vi和vj之间的最短路径和最短路径长度. 拓补排序 有向无环图:一个有向图不存在环,则称为有向无环图,简称DAG图. AOV网:如果用DAG图表示一个工程,其顶点表示活动,用有向边<vi,vj>表示活动vi必须先于vj 进行的这样一种关系,则将这种有向图称为顶点表示活动的网络.记为AOV网.在AOV网中,活动Vi 是活动Vj的直接前驱,活动Vj是活动Vi的直接后继,这种前驱和后继关系具有传递性质,且任何活动 Vi不能以它自己作为自己的前驱或后继. 拓补排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图 的一个拓补排序. 1.每个顶点出现且只出现一次 2.若顶点A在序列中排序在顶点B的前面,则在图中不存在从顶点B到顶点A的路径. 或者定义为:拓补排序是对有向无环图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径, 那么在排序中顶点B出现在顶点B的后面.每个DAG都有一个或多个拓补排序序列. 常用的一种拓补排序算法的步骤: 1.从DAG图中选择一个没有前驱结点并输出 2.从图中删除该顶点和所有以它为起点的有向边 3.重复1,2操作,直到当前的DAG图或当前图中不存在无前驱的顶点为止. 而后一种情况则说明有向图中必然存在环.