且有 个结点的二叉树就叫满二叉树
完全二叉树 :深度为 的具有 个结点的二叉树,当且仅当每一个结点都与深度为 的满二叉树 中的编号 为 的结点一一对应时,称为完全二叉树 (叶子结点的编号是连续的,左侧树必须满元素 )
叶子只可能分布在层次最大的两层上, 对任意结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
性质1 在二叉树的第i层上至多有 个结点
性质2 深度为k的二叉树至多有 个结点(k>=1)
性质3 对任何一个二叉树 ,如果其叶子为 ,度为2的结点为 ,\则
eg:这里叶子结点7 8 9 10 11 12有6个,度为2的有1 2 3 4 5 有五个
总结点数n 又
总边个数B =
性质4 具有n个结点的完全二叉树 的深度为
性质5 如果有一颗 个结点的完全二叉树( 深度为 [ )的结点按层编号(从第1层到第[ 层,每层从左到右),对任一结点i (1<= <=n),有
如果 =1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i /2]。 如果2 ,则结点i为叶子结点,无左孩子;否则, 其左孩子是结点2i。 如果 ,则结点 无右孩子;否则,其右孩 子是结点2i + 1。 总结 : 编号为i,他的父结点为 ,左结点为2i,右节点
2.存储结构
1.二叉树的顺序存储结构 2.二叉树的链式存储结构 typedef struct BiNode { int data; struct BiNode *lchild ,*rchile ; }BiNode,*BiTree;
在n个结点的二叉链表中,有 个空指针域
必有2n个链域,除根节点外,每个结点有且仅有一个双亲,所有只会有n-1个结点的链域存放指针,指向非空子女结点
3.三叉链表 typedef struct TriTNode { int data; struct TriTNode *Ichild ,*parent ,*rchild ; }TriTNode,*TriTree;
3.遍历二叉树和线索二叉树 1.类型 先序遍历:根左右
中序遍历:左根右
后续遍历:左右根
先—根左右
ABELDHMIJ
中—左根右
ELBAMHIDJ
后—左右根
LEBMIHJDA
实例:
2.根据遍历序列确定二叉树(先 中 后序) 实例1:先序+中序 先:A B C D E F G H I
中:C D B F E A I H G J
解题思路
由先知A必为根,B必为左 由中知 CDBFE在左部,IHGJ在右边
由先序知道B为根,由中序知道CD为左子树,FE为右子树
由先序知道G是根,那么I H为左子树,J为右子树
由中序CD左根右知道,c为左,d为右,先序知道E为根,中序知道F为左
由中序知道I为左子树
实例2—中序+后续 中序序列:BDCEAFHG 后序序列:DECBHGFA 由中序后续知道根为A,BDCE为左根 FHG为右根 后序知道B为根,中序推出没有左根,c为下一个根 左右根D为左,E为右边 后序知道F为根 由中序知道F没有左,那么H为左,G为根
3.遍历的算法实现-先序遍历
代码实现 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ printf ("%d\t" ,T->data); PerOrderTraverse(T->lchild); PerOrderTraverse(T->rchile); } }
递归代码解释 首先进入函数,此时T为传入的根结点
打印根节点
第一次调用:根左右 函数指向根B
进入第二层循环 遍历左,左为空此时返回
回到第二层循环此时 PerOrderTraverse(T->lchild);为空,那么自动执行下一条语句PerOrderTraverse(T->rchile);
进入循环,执行到D
再向下执行为空返回到第一次循环
再执行C
(●ˇ∀ˇ●)明白了吗!
4.遍历的算法实现-中序遍历 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ PerOrderTraverse(T->lchild); printf ("%d\t" ,T->data); PerOrderTraverse(T->rchile); } }
5.历的算法实现-后序遍历 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ PerOrderTraverse(T->lchild); PerOrderTraverse(T->rchile); printf ("%d\t" ,T->data); } }
6.二叉树遍历小总结
时间复杂度O(n)//每个结点只访问一次
空间复杂度O(n)//栈占用的最大辅助空间
7.中序遍历非递归算法-栈 void InOrderTraverse (BiTree T) { BiTree P; InitStack(S); P=T; while (p||StackEmpty(S)) { if (P) { Push(S,p); p=p->Ichild; } else { Pop(S,q); printf ("%d" ,q->data); p=q->rchild; } } }
8.二叉树的层次遍历
typedef struct { BTNode data[MAXSIZE]; int front,rar; }SqQueue; void LevelOrder (BTNode *b) { BTNode *p; SqQueue *qu; initQueue(qu); enQuenue(qu,b); while (!QueueEmpty(qu)){ deQueue(qu,p); printf ("%c" ,p->data);访问结点p if (p->Ichild!=NULL ) enQueue (qu,p->Ichild) ; if (p->rchild!=NULL )enqueue(qu,p->rchild); } }
9.二叉树遍历算法的应用 1.二叉树的建立 void CreateBiTree (BiTree &T) { char ch; scanf (&ch);if (ch=="#" ) T=NULL ;else { if (!(T=(BiTNode *)malloc (sizeof (BiTNode)))) exit (OVRTFLOW); T->data=ch; CreateBiTree(T->Ichild); CreateBiTree(T->rchild); } }
2.复制二叉树 int Copy (BiTree T,BiTree &newT) { if (T==NULL ) { NewT=NULL ; return 0 ; }else { NewT=new BiTNode; NewT->data=T->data; Copy(T->lchild,NewT->lchild); Copy(T->rchild,NewT->rchild); } return OK; }
3.计算二叉树的深度 4.计算二叉树的结点总数 如果为空树则结点为0 否则,结点个数为左子树个数+右子树结点个数再+1 int NodeCount (Bitree T) { if (T==NULL ) return 0 ; else return NodeCount(T->lchild)+ NodeCount(T->rchild)+1 ; }
5.计算叶子结点的个数 如果是空树返回0 否则,为左子树的叶子结点+右子树的叶子结点 int LeafCount (BiTree T) { if (T==NULL ) return 0 ; if (T->lchild==NULL &&T->rchile==NULL ) return 1 ; else return LeafCount(T->lchild)+leafCount(T->rchile); }
10.线索二叉树 如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱 ; 如果某孩子的右结点为空,则将空的右孩子的指针域改为指向其后继 ;
这里是依照遍历来判断前驱后继,而不是图
1.定义 typedef struct BiThNode { int data; int ltag,rtag; struct BiThNode *lchild ,*rchild ; };
2.线索二叉树画法
3.遍历算法 408不要求掌握
11.李阳的交换左右子树 #include <stdio.h> #include <stdlib.h> typedef struct TreeNode { int val; struct TreeNode *left ; struct TreeNode *right ; }TreeNode; TreeNode * creatNode (int val) { TreeNode * newNode=(TreeNode*) malloc (sizeof (TreeNode)); if (!newNode) { printf ("失败的树 " ); exit (-1 ); } newNode->val=val; newNode->left=NULL ; newNode->right=NULL ; return newNode; } void swaplr (TreeNode * root) { if (root==NULL ) { return ; } TreeNode * temp=root->left; root->left=root->right; root->right=temp; swaplr(root->left); swaplr(root->right); } void preOrderTraversal (TreeNode*root) { if (root==NULL ) { return ; } printf ("%d " , root->val); preOrderTraversal(root->left); preOrderTraversal(root->right); } void freeTree (TreeNode* root) { if (root == NULL ) { return ; } freeTree(root->left); freeTree(root->right); free (root); } int main () { TreeNode * root=NULL ; root= creatNode(1 ); root->left = creatNode(2 ); root->right = creatNode(3 ); root->left->left = creatNode(4 ); root->left->right = creatNode(5 ); preOrderTraversal(root); printf ("\n" ); swaplr(root); printf ("Pre-order traversal after swapping:\n" ); preOrderTraversal(root); printf ("\n" ); freeTree(root); return 0 ; }
6.树和森林 1.定义 森林 :是 棵互不相交的树的集合
2.双亲表示法 实现 :定义结构数组
存放树的结点
每个结点含两个域
数据域 :存放结点本身信息
双亲域 :指示本结点的双亲结点在数组中的位置
特点 :找双亲容易,找孩子难
typedef struct PTNode { TElemType data; int parent; }PTNode; typedef struct { PTNode nodes[MAXTSIZE]; int n,r; }PTree;
3.孩子链表 把每个结点的孩子结点排列起来,看成是一个线性表, 用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
解释 :每个结点都有一个单链表,叶子节点的单链表是空表,然后再将这些链表的头指针存放在数组中
孩子结点结构
typedef struct CTNode { int child; struct CTNode *next ; }* ChildPrt;
双亲结点结构
typedef struct { TElemType data; ChildPrt firstchild; }CTBox;
树结构
typedef struct { CTBox nodes[MAXTSIZE]; int n,r; }CTree;
特点 :找孩子容易,找双亲难
4.*孩子兄弟表示法(二叉树表示法,二叉链表表示法) 1.定义 实现 :用二叉链表作树的存储结构,链表中的美观结点的指针域分别指向其第一个孩子节点 和下一个兄弟节点
typedef struct CSNode { ELemtype data; struct CSNode *firstchild ,*nextsibling ; }CSNode,*CSTree;
左孩子 右兄弟,是兄弟的就来砍我
特点:找孩子,找兄弟简单,找双亲难
5.*树与二叉树的转换 1.定义 将树转化为二叉树,利用二叉树的算法实现对树的操作 由于树和二叉树都可以用二叉链表作存储结构,则以二叉树链表作媒介可以导出树与二叉树之间的对应关系
2.操作 1.将树转为二叉树 加线:在兄弟之间加一连线
抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
旋转:以树的根结点为轴心,将整树顺时针转45°
2.将二叉树转为树 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩….沿分支找到的所有右孩子,都与p的双亲用线连起来 抹线:抹掉原二叉树中双亲与右孩子之间的连线 调整:将结点按层次排列,形成树结构
6.*森林和二叉树的转换(二叉树与多棵树之间的关系) 1.森林转化为二叉树 将各棵树分别转换成二叉树
将每棵树的根结点用线相连
以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
2.二叉树转为森林 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二又树(去掉全部右孩线)
还原:将孤立的二又树还原成树(孤立二叉再还原)
7.树与森林的遍历 1.树的遍历的三种方式{先根(次序),后根,层次遍历}
2.森林的遍历 将森林看作3部分构成
森林中第一棵树的根结点; 森林中第一棵树的子树森林, 森林中其它树构成的森林。 1.先序遍历 若森林不空 则:
访问森林中第一棵树的根结点; 先序遍历 森林中第一棵树的子树森林;先序遍历 森林中(除第一棵树之外)其余树构成的森林。
2.中序遍历 若森林不空,则
1.中序遍历 森林中第一棵树的子树森林;
2.访问森林中第一棵树的根结点;
3.中序遍历 森林中(除第一棵树之外)其余树构成的森林。
对应:213
3.小案例
7.*哈夫曼树及其应用 408要求: 1.哈夫曼(Huffman)树和哈夫曼编码 2.并查集及其应用 3.堆及其应用(25新增)
1.基本概念 路径 :从树中的一个结点到另一个结点之间的分支构成这两个结点间的路径
结点路径的长度 :两结点间的路径上的分支数
树的路径长度 :从树根到每一个结点的路径长度之和,记作TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权 :将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度 :从根节点 到该结点之间的路径长度 与该结点的权 的乘积
树的带权的路径长度 :树中的所有叶子 结点带权路径长度之和 (WPL)
最优树 :带权路径最短的树(最优树)度要相同
最优二叉树 :带权路径长度(WPL)最短的二叉树
满二叉树不一定是哈夫曼树
具有相同带权结点的哈夫曼树不唯一
贪心算法 :构造哈夫曼树时首先选择权最小的叶子结点
2.哈夫曼算法 1.构造方法 根据 个给定的权值 构成的 棵二叉树的森林 ,其中 只有一个带权为 的根节点
构造森林全是根
在 中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
选用两小造新树
在 中删除这两棵树,同时将新得到的二叉校加入森林中。·
删除两小添新人
4. 重复2,3剩单根
总结 哈夫曼树的结点只有度为0或2的没有度为1的结点 包含n各叶子结点的哈夫曼树共有 个结点 包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点 在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。 经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。 可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支结点的度均不为1。 2.哈夫曼树的算法 顺序结构(一维数组) typedef struct { int weight; int parent,lch,rch; }HTNode,*HUffmanTree;
void CreatHuffmanTree (HuffmanTree &HT, int n) { int m,i; if (n<=1 )return ; m=2 *n-1 ; HT=new HTNode[m+1 ]; for (i=1 ;i<=m;++i){ HT[i].lch=0 ; HT[i].rch=0 ; HT[i].parent=0 ; } for (i=1 ;i<=n;++i) cin >>HT[i].weight;for ( i=n+1 ;i<=m; i++){ Select(HT, i-1 ,s1,s2); HT[s1].parent=i; HT[s2] .parent=i; HT[i].llch=s1;HT[i].rch=s2; HT[i].weight=HT[s1].weight + HT[s2].weight; } }
初始化HT[1…….2n-1]: lch=rch=parent=0 输入初始n个叶子结点:置HT[1…..n]的weight值; 进行以下n-1次合并,依次产生n-1个结点HT[i], i=n+1…..2n-1: a)在HT[1..i-1]中选两个未被选过(从parent ==0的结点中选)的weight最小的两个结点HT[s1]和HT[s2], s1、s2为两个最小结点下标; b)修改HT[s1]和HT[s2]的parent的值:HT[s1].parent=i;HT[s2].parent=i; c)修改新产生的HT[i]: .HT[i].weight=HT[s1].weight + HT[s2].weight;. HT[i]. Ich=s1; HT[i]. rch=s2; 3.哈夫曼编码 前缀编码:任意一字符不是另一个字符的前缀
1.方法 1、统计字符集中每个字符在电文中出现的平均概率 (概率越大, 要求编码越短)。 2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。 3、在哈夫曼树的每个分支上标上O或1: 结点的左分支标0,右分支标1 把从根到每个吐子的路径上的标号连接起来,作为该叶子代表的字符的编码。
左分支标记0,右分支标记1
问题 1.为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀
2.为什么哈夫曼编码能够保证字符编码总长最短? 因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。
性质 1:哈夫曼编码是前缀编码
性质 2:哈夫曼编码是最优前缀码
2.实现 void CreatHuffmanCode (HuffmanTree HT, HuffmanCode & HC, int n) { int i,cd,start; HC=new char *[n+1 ]; cd=new char [n]; cd[n-1 ]='\0’;//编码结束符 for(i=1; i<=n; ++i){//逐个字符求哈夫曼编码 start=n-1; c=i; f=HT[i].parent; while(f!=O){//从叶子结点开始向上回溯,直到根结点 --start;//回溯一次start向前指一个位置 if (HT[f].Ichild= =c) cd[start]= ' O' ;//结点c是f的左孩子,则生成代码O else cd[start]= ' 1 ' ;//结点c是f的右孩子,则生成代码1 c=f; f=HT[f].parent;//继续向上回溯 } //求出第i个字符的编码 HC[i]= new char [n-start];//为第i个字符串编码分配空间 strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中 } delete cd;//释放临时空间 }// CreatHuffanCode
4.编码的实现
八、图 1.图的定义和基本术语 1.图的定义 图 :
V:顶点(数据元素)的有穷非空集合
E:边的有穷集合
无向图 :每条边都没有方向
有向图 :每条边都有方向
完全图 :任意两个点都有一条边相连
稀疏图 :有很少边或弧的图(e<nlogn)
稠密图 :有较多边或弧的图
网 :边/弧带权的图
邻接 :有边/弧相连的两个顶点之间的关系。
存在 ,则称 和 ;互为邻接点 ;(无向图)
存在$$,则称v邻接到 ,邻接于 (有向图)
关联 :边/弧与顶点的关系
存在$(V_i,V_j)/称 为 该 边 弧 关 联 于 v_i和 v_j$
顶点的度 :与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和。
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
顶点v的出度是以v为始点的有向边的条数,记作OD(v)
路径 :接续的边构成的顶点序列
路径长度 :路径上边或弧的数目/权值之和。回路(环) :第一个顶点和最后一个顶点相同的路径。简单路径 :除路径起点和终点可以相同外,其余顶点均不相同的路径。
简单回路(简单环) :除路径起点和终点相同外,其余顶点均不相同的路径。
连通图(强连通图) 在无(有)向图 中,若对任何两个顶点v、u都右在从v至到u的路径称G是连通图(裾连通图)
权与网 图中边或弧所具有的相关数称为权 。表明从一个顶点到另一个顶点的距离或耗费。 带权的图称为网 。
子图 设有两个图G= (V,{E})、G1= (V1,{E1}),若V1⊆V,E1⊆E,则称G1是G的子图
连通分量(强连通分量) 无向图G的极大连通子图 称为G的连通分量 。 极大连通子图意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通。
有向图G的极大强连通子图称为G的强连通分量
极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。
极小连通子图: 该子图是G的连通子图,在该子图中删除任何一天边子图不再连通。生成树: 包含无向图G所有顶点的极小连通子图。生成森林: 对非连通图,由各个连通分量的生成树的集合。
2.图的类型定义 1.图的抽象数据类型定义如下: ADT Graph{数据对象V :具有相同特性的数据元素的集合,称为顶点集。 数据关系R: R={VR} VR={|| v,w⊆V ^ p(v,w),表示从v到w的弧,P(v,w)定义了弧的信息
}
2. 图的操作
3.图的存储结构
1.数组(邻接矩阵)表示法
无向图邻接矩阵
分析1:无向图的邻接矩阵是对称的
分析2:顶点i的度=第i行(列)中的1的个数
特别:完全的邻接矩阵中,对角元素为0,其余为1
有向图的邻接矩阵
分析1:有向图的邻接矩阵可能不是对称的
分析2:顶点的出度=第i行元素之和( 指向&V_2& 和 )
顶点的出度=第i列元素之和
顶点的度=第i行元素之和+第i列元素之和
有向网的邻接矩阵 网(即有权图)的邻接矩阵表示法
定义为
V_i V_j或 V_i,V_j∊
无 边 弧
如果两个顶点之间存在弧或边,那么我就记录两个顶点为权 ,如果不存在则记录无穷大
2.邻接矩阵的存储形式 1.用两个数组分别存储顶点表和邻接矩阵 #define MVNum 100 typedef char VerTexType;typedef int ArcType;typedef struct {VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum, arcnum;}AMGraph;
2.采用邻接矩阵表示法创建无向网 算法思想 (1)输入总顶点数和总边数。
(2)依次输点的信息存人顶点表中。
(3)初始化邻接矩阵,使每个权值初始化为极大值。
(4)构造邻接矩阵
代码先欠着
3.邻接矩阵的好处和坏处 好处 直观、简单、好理解 方便检查任意一对顶点间是否存在边 方便找任一顶点的所有“邻接点”(有边直接相连的顶点) 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”)·无向图:对应行(或列)非O元素的个数·有向图:对应行非0元素的个数是“出度”;对应列非0元素的个数是“入度” 坏处 3.邻接表表示法(链式) 1.无向图的邻接表 ·按编号顺序将顶点数据存储在一维数组中;·
关联同一顶点的边(以顶点为尾的弧)︰
·用线性链表存储
data表示顶点本身,firstarc表示第一条边的指针(以v1为例子,下标为3或为1的元素的指针)adjvex表示邻接的顶点,nextarc表示下一元素的指针
特点: ·邻接表不唯一 ·若无向图中有 个顶点、 条边,则其邻接表需 个头结点和 个表结点。适宜存储稀疏图。 无向图中顶点v的度为第i个单链表中的结点数。 2.有向图
特点: 顶点 的出度 为第i个单链表中的结点个数。 顶点 的入度 为整个单链表中邻接点域值是 的结点个数。
3.链式代码 1.定义代码 顶点
typedef struct VNode { VerTexType data; ArcNode * firstarc; }VNode,AdjList[MVNum];
边结点
#define MVNum 100 typedef struct ArcNode {int adjvex;struct ArcNode * nextarc ;OtherInfo info; }ArcNode;
图结点
typedef struct { AdjList vertices; int vexnum, arcnum; } ALGraph;
2.采用邻接表表示法创建无向网的算法思想 【算法思想】
输入总顶点数和总边数。 建立顶点表 依次输入点的信息存入顶点表中 使每个表头结点的指针域初始化为NULL 创建邻接表 依次输入每条边依附的两个顶点确定两个顶点的序号i和j,建立边结点 将此边结点分别插入到 和 对应的两个边链表的头部 5.邻接表的特点 1.特点 4.邻接矩阵与邻接表表示方法的关系
2.联系: 邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
2.区别: 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
邻接矩阵的空间复杂度为 ,而邻接表的空间复杂度为
3.用途: 邻接矩阵多用于榈密图;而邻接表多用于稀疏图
5.十字链表
1.简介 十字链表 (Orthogonal List)是有向图 的另一种链式存储结构。我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。 有向图中的每一条弧对应十字链表中的一个弧结点 ,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。
2.具体 data:数据
firstin:第一个入度边
firstout:第一个出度边
tailvex:弧尾位置
headvex:弧头位置
hlink:弧头相同的下一条弧
tlink:弧尾相同的下一条弧
6.邻接多重表
4.图的遍历 1.定义 遍历定义:
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 ,它是图的基本运算
遍历实质 :找每个顶点的邻接点的过程。
图的特点 : 图中可能存在回路 ,且图的任一顶点都可能与其它顶点相通在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。
如何避免回路 :
解决思路:设置辅助数组 ,用来标记每个被访问过的顶点。
初始状态 为0 ·顶点i被访问,改 为1,防止被多次访问 2.深度优先(DFS) 1.连通图的遍历 方法: 在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w1 再从w出发,访问与w邻接但还未被访问过的顶点W2; 然后再从w出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问; 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
实现
bool visited[MAX_VERTEX_NUM];void DFSTraverse (Graph G) { int v; for (v=0 ;v<G;v++) { visited[v]=false ; for ( v = 0 ; v < G; ++v) { if (!visited[v]) { DFS(G,v); } } } } void DFS (Grap G,int V) { int w; visit(v); visited[v]=true ; for (w=FirstNeighbor(G,v);w>=0 ;w=NextNeighbor(G,v,w)) { if (!visited[w]) { DFS(G,w); } } }
效率分析 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在的行,时间复杂度为 。
用邻接表来表示图,虽然有 个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为 .
结论 :稠密图 适于在邻接矩阵上进行深度遍历;稀疏图 通于在邻接表上进行深度遍历。2.广度优先遍历 1.方法 方法:从图的某一结点出发,首先依次访问该结点的所有邻接点 ,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点 重复此过程,直至所有顶点均被访问为止。
2.实现 bool visited [Max_VERTEX_NUM];void BFSTraverse (Grap G) { for (int i = 0 ; i < G.vexnum; ++i) { visited[i]=false ; } InitQueue(Q); for ( i = 0 ; i <G.vexnum ; ++i) { if (!visited[i]){ BFS(G,i); } } } void BFS (Graph G,int v) { visit(v); visited[v]=true ; Enqueue(Q,v); while (!isEmpty(Q)){ DeQueue(Q,v); for (int w = FirstNeighbor(G,v); w>0 ; w=NextNeighbor(G,v,w)) { if (!visited[w]){ visit(w); visited[w]=true ; Enqueue(Q,w); } } } }
3.效率分析 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要 循环检测矩阵中的整整一行( n个元素),总的时间代价为O( )。 用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为 。 4.效率比较 ·空间复杂度相同,都是O(n)(借用了堆栈或队列) ; ·时间复杂度只与存储结构,(邻接矩阵或邻接表)有关,而与搜索路径无关。 5.图的应用 1.最小生成树 1.生成树的简介 生成树 :所有顶点均由边连接在一起,但不存在回路
一个图可以有许多棵不同的生成树
所有生成树具有以下共同特点
生成树的顶点个数与图的顶点个数相同; 生成树是图的极小连通子图 ,去掉一条边则非连通;· 一个有n个顶点的连通图的生成树有 条边; ·在生成树中再加一条边必然形成回路。 生成树中任意两个顶点间的路径是唯一 的; 含有 个顶点 条边的图不一定是最小生成树
2.无向图的生成树
3.最小生成树 最小生成树 :给定一个无向网络在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
构造最小生成树 构造最小生成树的算法很多,其中多数算法都利用了MST 的性质。
MST性质 :设N =(V, E)是一个连通网,U是顶点集V的一个非空子集。若边(u, v)是一条具有最小权值的边,其中u∈u,v∈V-U,则必存 在一棵包含边(u, v)的最小生成树。
Prim-普里姆算法 算法思想
设 是连通网, 是N上最小生成树中边的集合。
初 始 令 ,∊ TE=${ }。
在所有∊ ,∊ 的边∊ 中,找一条代价最小的边 。
将 并入集合 ,同时 并入U
重复上述操作直至 ,则 为 的最小生成树。
从顶点往下找最小的权
Kruskal-克鲁斯卡尔算法 所有边按权值排序,然后选择最小的
当有循环时舍弃这条边
当所有边连通时结束
与prim算法不同的是他是按排序来找最小,prim是依次选最小
设连通网 ,令最小生D树初始状态为只有n个顶点而无边的非连通图 ,每个顶点自成一个连通分量。
在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
依此类推,直至T中所有顶点都在同一连通分量上为止。
最小生成树可能不唯一
两种比较
2.最短路径 1.定义 最短路径与最小生成树不同 ,路径上不一定包含n个顶点,也不一定包含n-1条边。
单源最短路径-Dijkstra迪杰斯特拉算法
所有顶点间的最短路径—Floyd弗洛伊德算法
2.Dijkstra算法 概述 1.初始化:先找出从源点 到各终点v的直达路径 , 即通过一条弧到达的路径。
2选择:从这些路径中找出一条长度最短的路径 。
3.更新:然后对其余各条路径进行适当调整:
若在图中存在弧 ,且 则以路径 代替 。
在调整后的各条路径中,再找长度最短的路径)依此类推。
(先找出直达的,然后与不直达的比较,有小的就更新被比较的)
具体-按路径长度递增次序产生最短路径 1、把V分成两组:辅助数组D存放。 (1) S:已求出最短路径的顶点的集合。 (2)T=V -s∶尚未确定最短路径的顶点集合。
2、将T中顶点按最短路径递增的次序加入到S中,
保证:(1)从源点 到S中各顶点的最短路径长度都不大于从 到T中任何顶点的最短路径长度。
(2)每个顶点对应一个距离值: S中顶点:从 到此顶点的最短路径长度。
T中顶点:从 到此顶点的只包括S中顶点作中间顶点的最短路径长度。
3.Floyd 弗洛伊德算法 算法思想 ·逐个顶点试探 ·从到v,的所有可能存在的路径中· 选出一条长度最短的路径
3.拓扑排序 有向无环图:无环的有向图,简称DAG
一个结点可能有多个前驱,但是没有回路
1.AOV网 定义 用一个有向图表示一个工程的各子工程及其相冥制约的关系,其中以顶点表示活动 ,弧表示活动之间的优先制约关系 ,称这种有向图为顶点表示活动的网 ,简称AOV网(Activity On Vertex network)
特点 若从i到j有一条有向路径,则i是j的前驱;j是i的后继。 若是网中有向边,则i是j的直接前驱;j是i的直接后继 AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。 检测AOV 网中是否存在环方法: 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV 网必定不存在环。
2.AOE网 用一个有向图表承一个工程的各子工程及其相互制约的关系,以弧表示活动 ,以顶点表示活动的开始或结束事件 ,称这种有向图为边表示活动的网 ,简称为AOE网(Activity On Edge)。
3.拓扑排序 在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧存在,则在这个序列甲,I一疋排仕J的前面,具有这种性质的线性序列称为拓扑有序序列 ,相应的拓扑有序排序的算法称为拓扑排序。
4.关键路径
对于AOE网,我们关心两个问题:
(1)完成整项工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?
关键路径 :路径长度最长的路径。 路径长度:路径上各活动持续时间之和。
———>求解关键路径问题
由若干个关键活动组成的就是关键路径
ve(j)最早开始时间:从原点开始找到权的最大值
vl(n)最迟发生时间:从汇点向前,找到最小值
具体计算
e(i)就是弧尾的长度
eg: v5就是6(v5连接的是v2,只用算v2),
l(i)就是vl-路径上的权值
eg: v5就是7-a4的权=7-1=6
讨论 1、若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。 如: a11、a10、a8、a7。
2、如果一个活动处于所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间。如: a1、a4。
3、处于所有的关键路径上的活动完成时间不能缩短太多),否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。
如:a1由6天变成3天,就会改变关键路径。
李阳
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 李阳的秘密小屋 !