1.29k likes | 1.38k Views
第八章 图. 赵建华 南京大学计算机系. 内容. 图的基本概念 图的存储表示 图的遍历与连通性 最小生成树 最短路径 活动网络. 图的基本概念. 定义 : 图是一个二元组: Graph = ( V , E ) 顶点集合 V = { x | x 某个数据对象 } 是有穷非空集合; E = {( x , y ) | x , y V } 是边 (edge ) 的集合。. 有向图 与无向图. 有向图 顶点对 <x, y> 是有序的。 x 称为 始点 , y 为 终点 。
E N D
第八章 图 赵建华 南京大学计算机系
内容 • 图的基本概念 • 图的存储表示 • 图的遍历与连通性 • 最小生成树 • 最短路径 • 活动网络
图的基本概念 • 定义:图是一个二元组: Graph=( V, E ) • 顶点集合V = { x | x 某个数据对象}是有穷非空集合; • E = {(x, y) | x, y V }是边 (edge)的集合。
有向图与无向图 • 有向图 • 顶点对 <x, y> 是有序的。x称为始点,y为终点。 • <x,y>和<y,x>是不同的边(if x!=y)。 • 无向图 • 顶点对(x, y)是无序的。 • (x, y)和(y,x)是同一条边。
完全图 • 完全图 • 有n 个顶点的无向图有 n(n-1)/2条边, 则此图为完全无向图。任意两个结点之间有一条边。 • 有n 个顶点的有向图有n(n-1)条边, 则此图为完全有向图。任意两个结点对之间有一条边。 0 0 0 0 1 1 1 2 2 1 4 3 3 5 6 2 2
0 0 0 0 1 2 1 1 2 2 3 3 3 3 • 邻接顶点 • 如果(u, v) 是 E(G) 中的一条边,则称 u 与 v 互为邻接顶点。 • 子图 • 设有两个图G=(V, E) 和G'=(V', E')。若V ' V 且E'E, 则称图G'是图G的子图。 • 权 • 某些图的边具有与它相关的数, 称之为权。这种带权图叫做网络。 子图
顶点的度 • 一个顶点v的度是与它相关联的边的条数,记作TD(v)。 • 在有向图中, 顶点的度等于该顶点的入度与出度之和。 • 顶点 v 的入度/出度 • 在有向图中,以v为终点/始点的有向边的条数。记作ID(v)/OD(v)。 • 路径 • 在图 G=(V, E) 中, 若从顶点vi 出发, 沿一些边经过一些顶点vp1,vp2, …,vpm,到达顶点vj。则称顶点序列(vi vp1 vp2 ... vpm vj)为从顶点vi 到顶点 vj 的路径。 • 要求 (vi, vp1)、(vp1, vp2)、...、(vpm, vj) 应是属于E的边。
路径长度 • 非带权图的路径长度是指此路径上边的条数。 • 带权图的路径长度是指路径上各边的权之和。 • 简单路径 • 若路径上各顶点 v1, v2, ..., vm均不 互相重复, 则称这样的路径为简单路径。 • 回路 • 若路径上第一个顶点 v1 与最后一个顶点vm 重合, 则称这样的路径为回路或环。 0 0 0 1 2 1 2 1 2 3 3 3
连通图与连通分量 • 在无向图中, 若从顶点v1到顶点v2有路径, 则称顶点v1与v2是连通的。 • 如果图中任意一对顶点都是连通的, 则称此图是连通图。 • 非连通图的极大连通子图叫做连通分量。 • 强连通图与强连通分量 • 在有向图中, 若对于每一对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径, 则称此图是强连通图。 • 非强连通图的极大强连通子图叫做强连通分量。 • 生成树 • 一个连通图的生成树是其极小连通子图,在 n 个顶点的情形下,有n-1条边。
图的抽象数据类型 class Graph { //对象: 由一个顶点的非空集合和一个边集合构成 //每条边由一个顶点对来表示。 public: Graph(); //建立一个空的图 void insertVertex (const T& vertex); //插入一个顶点vertex, 该顶点暂时没有边 void insertEdge (int v1, int v2, int weight); //在图中插入一条边(v1, v2, w) void removeVertex (int v); //在图中删除顶点v,以及所有关联到它的边
void removeEdge (int v1, int v2); //在图中删去边(v1,v2) bool IsEmpty(); //若图中没有顶点, 则返回true, 否则返回false TgetWeight (int v1, int v2); //函数返回边(v1,v2) 的权值 int getFirstNeighbor (int v); //给出顶点 v 第一个邻接顶点的位置 int getNextNeighbor (int v, int w); //给出顶点 v 的某邻接顶点 w 的下一个邻接顶点 };
图的存储表示 • 图的模板基类 • 模板的数据类型参数表 <class T, class E> , • T是顶点数据的类型,E是边上所附数据的类型。 • 可以表示最复杂的情况:即带权无向图 • 处理非带权图时,边不附带数据,因此数据类型参数表改为 <class T>。 • 如果使用的是有向图,也可以对程序做相应的改动。
图的模板基类 const int maxWeight = ……; //表示的权值 const int DefaultVertices = 30; //最大顶点数(=n) template <class T, class E> class Graph { //图的类定义 protected: int maxVertices; //图中最大顶点数 int numEdges; //当前边数 int numVertices; //当前顶点数 int getVertexPos (T vertex); //给出顶点vertex在图中位置 public:
… … … //构造函数析构函数 bool GraphEmpty () const //判图空否 int NumberOfVertices () ; //返回当前顶点数 int NumberOfEdges (); //返回当前边数 virtual TgetValue (int i); //取第i个顶点的值 virtual EgetWeight (int v1, int v2); //取边上权值 //取顶点 v 的第一个邻接顶点以及下一个邻接结点 virtual int getFirstNeighbor (int v); virtual int getNextNeighbor (int v, int w); //结点和边的操作 virtual bool insertVertex (const T vertex); virtual bool insertEdge (int v1, int v2, E cost); virtual bool removeVertex (int v); virtual bool removeEdge (int v1, int v2); };
邻接矩阵 (Adjacency Matrix) • 图的邻接矩阵表示包含 • 一个记录各个顶点信息的顶点表(表示集合V) • 一个表示各个顶点之间关系的邻接矩阵(表示集合E) • 设图 A = (V, E) 是一个有 n个顶点的图, 图的邻接矩阵是一个二维数组 A.edge[n][n]: 注意:edge可以看作是二维的bitSet表示方法 如果图是无向的,那么必然是对称矩阵
0 1 2 3 0 1 2 • 无向图的邻接矩阵是对称的; • 有向图的邻接矩阵可能是不对称的。
有向图:第i行中1的个数就是顶点 i的出度,第 j 列中1的个数就是顶点 j的入度。 • 无向图:第 i行 (列)中1的个数就是顶点i的度。 (注意,无向图的邻接矩阵是对称的)
8 2 3 6 9 3 2 5 4 0 1 1 网络(带权图)的邻接矩阵 例如:
用邻接矩阵表示的图的类定义 template <class T, class E> class Graphmtx : public Graph<T, E> { … … … … //输入输出友元 private: T * VerticesList; //顶点表 E **Edge; //邻接矩阵 int getVertexPos (T vertex); //给出顶点vertex在图中的位置
public: … … … … //构造函数与析构函数的声明 T*getValue (int i); //获取第i个顶点的值, //i 不合理则返回NULL; EgetWeight (int v1, int v2); //取边(v1,v2)上权值 //取顶点 v 的第一个/下一个邻接顶点,用于便利邻接顶点 int getFirstNeighbor (int v); int getNextNeighbor (int v, int w); //插入/删除结点和边 bool insertVertex (const T vertex); bool insertEdge (int v1, int v2, E cost); bool removeVertex (int v); bool removeEdge (int v1, int v2); };
template <class T, class E> Graphmtx<T, E>::Graphmtx (int sz) { //构造函数 maxVertices = sz; numVertices = 0; numEdges = 0; int i, j; VerticesList = new T[maxVertices]; //创建顶点表 //申请邻接矩阵的空间,已经默认E为int Edge = (int **) new int *[maxVertices]; for (i = 0; i < maxVertices; i++) Edge[i] = new int[maxVertices]; //矩阵初始化,按照带权图的方式初始化,不存在任何边 for (i = 0; i < maxVertices; i++) for (j = 0; j < maxVertices; j++) Edge[i][j] = (i == j)?0 : maxWeight; };
template <class T, class E> int Graphmtx<T, E>::getFirstNeighbor (int v) { //给出顶点位置为v的第一个邻接顶点的位置, //如果找不到, 则函数返回-1 if (v != -1) { for (int col = 0; col < numVertices; col++) if (Edge[v][col] && Edge[v][col] < maxWeight) return col; } return -1; };
template <class T, class E> int Graphmtx<T, E>::getNextNeighbor (int v, int w) { //给出顶点 v 的某邻接顶点 w 的下一个邻接顶点 if (v != -1 && w != -1) { for (int col = w+1; col < numVertices; col++) if (Edge[v][col] && Edge[v][col] < maxWeight) return col; } return -1; }; 每次调用时,使用上一次的位置w作为参数
邻接表 (Adjacency List) • 邻接矩阵的改进形式 • 把邻接矩阵的各行分别组织为一个单链表。 • 同一个顶点发出的边链接在同一个边链表中; • 每一个链结点代表一条边(边结点)。结点中有另一顶点的下标dest和指针link。 • 对于带权图,边结点中还保存权值cost。 • 顶点表: • 第 i 个顶点中保存该顶点的数据,以及它对应边链表的头指针adj。 边集E按照始结点,分为n个集合;每个集合用单链表表示
data adj dest link dest link A 0 1 2 3 A B C D 1 3 0 2 B C 1 D 0 无向图的邻接表 • 统计某顶点对应边链表中结点个数,可得该顶点的度。 • 某条边(vi, vj)在邻接表中有两个边结点,分别在第 i 个顶点和第 j 个顶点对应的边链表中。
A data adj dest link 0 1 2 A B C 1 dest link B 0 2 邻接表 (出边表) C data adj dest link 0 1 2 1 A B C 0 1 逆邻接表 (入边表) 有向图的邻接表和逆邻接表
6 A D data adj dest cost link 9 0 1 2 3 15 36 A B C D 5 2 28 B C 8 32 19 (出边表) (顶点表) 网络 (带权图) 的邻接表 • 统计出边表中结点个数,得到该顶点的出度; • 统计入边表中结点个数,得到该顶点的入度。
在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。 • 设图中有 n 个顶点,e 条边, • 表示无向图时需要 n 个顶点结点,2e 个边结点; • 表示有向图时,(不考虑逆邻接表)需要 n 个顶点结点,e 个边结点。 • 当 e远远小于 n2 时,可以节省大量的存储空间。 • 把同一个顶点的所有边链接在一个单链表中,也使得某些操作更为便捷。(firstNeighbor,nextNeighbor)
用邻接表表示的图的类定义 template <class T, class E> struct Edge { //边链表结点的定义 intdest; //边的另一顶点位置 Ecost; //边上的权值 Edge<T, E> *link; //下一条边链指针 Edge () {} //构造函数 Edge (int num, E cost)//构造函数 : dest (num), weight (cost), link (NULL) { } bool operator != (Edge<T, E>& R) const { return dest != R.dest; } //判边等否 };
template <class T, class E> struct Vertex { //顶点的定义 T data; //顶点的名字 Edge<T, E> *adj; //边链表的头指针 };
template <class T, class E> class Graphlnk : public Graph<T, E> { //图的类定义 friend istream& operator >> (istream& in, Graphlnk<T, E>& G); //输入 friend ostream& operator << (ostream& out, Graphlnk<T, E>& G); //输出 private: Vertex<T, E> *NodeTable; //顶点表 (各边链表的头结点) //顶点的个数存放在numVertices,从基类继承 int getVertexPos (const T vertx) { //给出顶点vertex在图中的位置 for (int i = 0; i < numVertices; i++) if (NodeTable[i].data == vertx) return i; return -1; }
public: Graphlnk (int sz = DefaultVertices); //构造函数 ~Graphlnk(); //析构函数 //获取信息的接口 TgetValue (int i);//取第i个顶点的值; EgetWeight (int v1, int v2); //取边(v1,v2)权值 //结点和边的操作接口 bool insertVertex (const T& vertex); bool removeVertex (int v); boolinsertEdge (int v1, int v2, E cost); bool removeEdge (int v1, int v2); //遍历邻接结点的接口 int getFirstNeighbor (int v); int getNextNeighbor (int v, int w); };
template <class T, class E> Graphlnk<T, E>::Graphlnk (int sz) { //建立一个空的邻接表 maxVertices = sz; numVertices = 0; numEdges = 0; //创建顶点表数组 NodeTable = new Vertex<T, E>[maxVertices]; if (NodeTable == NULL) { cerr << "存储分配错!" << endl; exit(1); } //初始化边集合(空集) for (int i = 0; i < maxVertices; i++) NodeTable[i].adj = NULL; };
template <class T, class E> Graphlnk<T, E>::~Graphlnk() { //析构函数:删除一个邻接表 //删除所有顶点的边 for (int i = 0; i < numVertices; i++ ) { Edge<T, E> *p = NodeTable[i].adj; //删除第i个顶点的边链表中的所有结点(边的信息) while (p != NULL) { NodeTable[i].adj = p->link; delete p; p = NodeTable[i].adj; } } //删除顶点表数组 delete [ ]NodeTable; };
template <class T, class E> int Graphlnk<T, E>::getFirstNeighbor (int v) { //给出顶点位置为v 的第一个邻接顶点的位置, //如果找不到, 则函数返回-1 //if(v>=0 && v<numVertice) //应该使用这个判断 if (v != -1) { //顶点v存在 Edge<T, E> *p = NodeTable[v].adj; //对应边链表第一个边结点 if (p != NULL) return p->dest; //存在, 返回第一个邻接顶点 } return -1; //第一个邻接顶点不存在 };
template <class T, class E> int Graphlnk<T, E>::getNextNeighbor (int v, int w) { //给出顶点v的邻接顶点w的下一个邻接顶点的位置, //若没有下一个邻接顶点, 则函数返回-1 if (v != -1) { //顶点v存在 Edge<T, E> *p =NodeTable[v].adj; //寻找当前的边;比较低效 while (p != NULL && p->dest !=w) p = p->link; //p==NULL表示w不是v的邻接顶点 //p->link == NULL表示w是最后一个邻接顶点 if (p != NULL && p->link != NULL) return p->link->dest; //返回下一个邻接顶点 } return -1; //下一邻接顶点不存在 }; 在遍历一个顶点的所有边时,可以保存指向当前链表结点的指针。这样寻找下一个邻接顶点时效率更高一些。
template <class T, class E> E graphlnk<T,E>::getWeight(int v1, int v2) { //返回边(v1,v2)上的权值;无边则返回0; if(v1 != -1 && v2 != -1) { //寻找从v1到v2的边; Edge<T,E> *p = NodeTable[v1].adj; //第一条边 while(p!=NULL && p->dest !=v2) p = p->link; //下一条边 if(p!=NULL) return p->cost; } return 0; }
template <class T, class E> E graphlnk<T,E>::insertVertex(const T& vertex) { //插入顶点vertex。成功返回true,失败返回false if (numVertice == maxVertices) return false;//满 //增加一个顶点:插入到最后,计数器加一。 NodeTable[numVertices].data = vertex; numVertices ++; return true; }
template <class T, class E> E graphlnk<T,E>::removeVertex(int v) //删除第v个顶点。成功则返回true,否则返回false; if(v<0 || v>= numVertices) return false; //不合法的顶点号; //下面要做两件事情 //1、删除所有进入v的边;如果是无向图,那么 // 是记录在其它边链表中的所有和v相连的边 //2、删除所有从v出发的边; //3、消除结点的记录,并填补结点的空缺。 // 并修改边中的信息。
对于有向图,应该分两步删除和v相连的边: 1、删除从v出发的边;2、删除到达v的边; 第一步遍历顶点v的链表,第二步遍历所有链表 //删除和v相连的边 Edge<T,E> *p, *s, *t; int i,k; while(NodeTable[v].adj != NULL) { //删除第v个边链表中所有的边; p = NodeTable[v].adj; k=p->dest; s = NodeTable[k].adj; t = NULL; //寻找和p所指结点对称的边;t总是指向s的前一个结点 while(s!=NULL && s->dest != v) { t=s; s=s->link;} if(s != NULL){ //找到了对称边,删除 if(t==NULL)NodeTable[k].adj=s->link; //对称边是首结点; else t->link = s->link; delete s; } NodeTable[v].adj = p->link; //清除顶点v的边链表结点; delete p; numEdges--; }
numVertices --; //删除顶点,将最后的顶点移动到v处 NodeTable[v].data = NodelTable[numVertices].data; p = NodeTable[v].adj = NodeTable[numVertices].adj; while(p!=NULL){ s = NodeTable[p->dest].adj; //原最后一个结点的边; while(s!=NULL) //原来指向最后一个结点的边现在应该指向v; if(s->dest == numVertices){s->dest = v; break;} else s=s->link; } return true; };//end of method; 这里处理的同样是无向图。所以我们可以只修改p的邻接顶点上的边的信息。 处理有向图时需要遍历所有的边链表。 需要调整的原因是:在边里面用下标索引指示结点。现在因为结点的移动导致结点的下标出现变化
mark vertex1 vertex2 path1 path2 邻接多重表 (Adjacency Multilist) • 邻接多重表可以方便地处理图的边 • 每一条边只有一个边结点。结点的结构如下: • Mark:标记域; • vertex1,vertex2存放顶点; • path1,path2:分别指向对应于vertex1和vertex2的边链表; • 还可以增加字段,存放其它和边相关的信息。
data Firstout 无向图的邻接多重表表示 • 顶点的数据结构 • data 存放与该顶点相关的信息,Firstout是指示第一条依附该顶点的边的指针。 • 顶点的数据可以存放在一个顺序表中。 • 和每个顶点相连的边组成一个链表 • Firstout指向第一条边 • 顶点v的边curEdge的下一条边是: (v==curEdge->vertext1)? path1 : path2;
data Fout mark vtx1 vtx2 path1 path2 0 A A e1 e1 0 1 e3 1 B B D e2 e2 0 2 2 C C e3 1 3 3 D 邻接多重表的例子
mark vertex1 vertex2 path1 path2 有向图的邻接多重表 • 邻接多重表(十字链表)可以同时表示邻接表和逆邻接表(即从一个顶点离开的边和到达一个顶点的边)。 • 边结点的结构 • mark:处理标记; • vertex1和vertex2:始顶点和终顶点 • path1指向始顶点与该边相同的下一条边的指针; • path2是指向终顶点与该边相同的下一条边的指针。
data Firstin Firstout • 顶点结点的结构 • 顶点的数据可以存放在一个顺序表中。 • data:存放与该顶点相关的信息; • Firstin 指示以该顶点为始顶点的出边表的第一条边, • Firstout 指示以该顶点为终顶点的入边表的第一条边。
data Fin Fout mark vtx1 vtx2 path1 path2 e6 e1 0 1 0 A E A e2 0 3 e5 1 e2 B e1 D 2 C 1 2 e4 e3 e3 B C 3 e4 D 2 3 4 E e5 3 4 e6 4 0 邻接多重表的结构
图的遍历与连通性 • 从给定连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 (Graph Traversal)。 • 图中可能存在回路,因此可能沿着某些边多次到达一个顶点。 • 为避免重复访问,设置一个辅助数组visited [ ] • visited[i]==0表示i尚未被访问; • visited[i]==1表示i已经被访问; • 遍历时,只访问visited[i]==0的顶点;访问顶点 i后立即将visited[i]设置为 1。
图遍历的种类 • 深度优先搜索DFS (Depth First Search) • 从起始结点开始,不断选择当前结点的相邻结点向前搜索。如果某个结点的所有相邻结点都被探索过,就回溯。 • 广度优先搜索BFS (Breadth First Search) • 从起始结点开始,首先访问当前结点的所有相邻结点;然后访问这些相邻结点的相邻结点;… • 在深度/广度优先搜索的过程中访问到的顶点和边组成深度/广度优先生成树。
1 2 1 2 3 3 A A B B E E 7 7 5 4 5 4 G G D C D C 6 6 H I H I F F 8 9 8 9 深度优先搜索DFS示例 前进 回退 深度优先搜索过程 深度优先生成树