图:记录关系、联系,由数个点Vertex、数条边Edge组成。
同构 Isomorphism 英[ˌaɪsəʊ’mɔ:fɪzəm] :两张图的连接方式一样。
图的存储结构:
阵列
记录所有的点与所有的边。直观、节省空间、但不适于计算。
如图,记录(0,1) (0,3) (0,4)
邻接矩阵 adjacency matrix
无向图的边数组是个对称矩阵(aij=aji)。
顶点vi的度:vi所在行的元素之和
有向图讲究入度和出度。顶点vi的入度是vi所在列的元素之和,出度是所在行的元素之和
矩阵的值也可以是边的权重。
typedef struct{
VertexType vexes[MAX]; //顶点表
EdgeType arc[MAX][MAX]; //邻接矩阵
int numVertexes,numEdges; //顶点数,边数
} MGraph;
邻接表 adjacency lists
把一張圖上的點依序標示編號。每一個點,後方串連所有相鄰的點
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[MAX];
typedef struct{
AdjList adjList;
int numVertexes,numEdges;
} GraphAdjList;
图的遍历:
两种遍历算法:深度优先遍历 Depth First Search,广度优先遍历 Breadth First Search,。
深度优先遍历
从图中某个顶点出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直到图中所有和v有路径相同的顶点都被访问到。
以下图为例。从A点出发,做上表示走过的记号。我们给自己定一个原则,在没有碰到重复顶点的情况下,始终往右手边走。于是走向B,接着C、D、E、F。继续往右手边走,发现A点已经访问过了,此时退回F点,走向从右数的第二个通道到达顶点G,G有三条通道,B和D都走过,所以走向H,H的两条通道D和E都走过。
此时是否已经遍历了所有顶点呢?没有。所以按原路返回,回到G,没有未走过的通道,回到F,没有未走过的通道,回到E,有一条通往H,验证后也是走过的。回到D,有三条道未走过,一条条来。走到I,这是个未标记过的顶点,标记下来。继续返回直到返回顶点A,确认完成遍历任务,找到了所有的9个顶点。
将这个过程化为上图。这其实就是一个递归的过程。这里以连通图为例,如果是非连通图,需要对它的连通分量分别进行深度优先遍历。
图的存储方式是邻接矩阵:
typedef int Boolean;
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!=NULL){
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);
}
}
}
对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O(n2)的时间。
而邻接表做存储结构时,找邻接点所需时间取决于顶点和边的数量,所以是O(n+e),显然对于点多边少的稀疏图,邻接表结构使得算法在时间效率上大大提高。
广度优先遍历
(依照編號順序)不斷找出尚未遍歷的點當作起點,進行下述行為:
一、把起點放入queue。
二、重複下述兩步驟,直到queue裡面沒有東西為止:
甲、從queue當中取出一點。
乙、找出跟此點相鄰的點,並且尚未遍歷的點,
通通(依照編號順序)放入queue。
只觀察離開 queue 的時刻,可以發現 BFS 優先走遍距離起點最近之處,優先讓 BFS Tree 變得寬廣,因而得名 Breadth-first Search 。這個遍歷順序能夠解決許多圖論問題!
时间复杂度:使用的資料結構為 adjacency matrix 的話,可以以 O(V^2) 時間遍歷整張圖;使用 adjacency lists 的話,可以以 O(V+E) 時間遍歷整張圖。 V 是圖上的點數, E 是圖上的邊數。
bool adj[9][9]; // 一張圖,資料結構為adjacency matrix。
bool visit[9]; // 記錄圖上的點是否遍歷過,有遍歷過為true。
void BFS()
{
// 建立一個queue。
queue<int> q;
// 全部的點都初始化為尚未遍歷
for (int i=0; i<9; i++)
visit[i] = false;
for (int k=0; k<9; k++)
if (!visit[k])
{
一、把起點放入queue。
q.push(k);
visit[k] = true;
// 二、重複下述兩點,直到queue裡面沒有東西為止:
while (!q.empty())
{
// 甲、從queue當中取出一點。
int i = q.front(); q.pop();
// 乙、找出跟此點相鄰的點,並且尚未遍歷的點,
// 依照編號順序通通放入queue。
for (int j=0; j<9; j++)
if (adj[i][j] && !visit[j])
{
q.push(j);
visit[j] = true;
}
}
}
}