1、圖的基本概念
- 按照有無方向分爲無向圖、有向圖。無向圖中如果任意兩個頂點之間都存在邊則稱爲無向完全圖,含有n個頂點的無向完全圖含有條邊;有向中如果任意兩個頂點之間都存在兩條方向相反的邊則稱爲有向完全圖,含有n個頂點的有向完全圖含有條邊;
- 簡單圖:圖中沒有重複邊以及頂點到自身的邊(環)
- 圖的邊(無向邊或者有向邊)帶上權值就稱爲網
- 無向圖中如果任意兩個頂點都是連通的則稱爲連通圖;有向圖中任意一對頂點,如果從To和從To都是連通的,則稱該有向圖爲強連通圖。
- 無向圖中連通且含有n個節點n-1條邊的爲生成樹
2、圖的存儲結構
1、鄰接矩陣存儲
用兩個數組來表示圖。一個一維數組用來存儲圖中頂點信息,一個二維數組(鄰接矩陣)用來存儲圖中的邊或弧的信息。
網的每一條邊都帶有權值,如果用鄰接矩陣來表示網呢?
下面看一下鄰接矩陣存儲結構的定義:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /*頂點表*/
EdgeType arc[MAXVEX][MAXVEX]; /*鄰接矩陣,用來表示邊的情況*/
int numVertexes, numEdges; /*圖中當前的頂點數和邊數*/
}MGraph;
鄰接矩陣存儲方式的缺點:存儲稀疏圖(邊數相對於頂點數較少的圖)會浪費大量存儲空間。
2、鄰接表存儲
鄰接表的處理方式如下:
- 圖中的頂點用一個一維數組存儲,頂點數組中的每個數據元素除了需要存儲頂點的值以外,還需要存儲指向第一個鄰接點的指針(用一維數組比用鏈表的好處是:數組可以比較容易容易的讀取頂點信息)
- 圖中每個頂點的所有鄰接點構成一個線性表,用單鏈表存儲。在無向圖中,該鏈表被稱爲邊表;在有向圖中,該鏈表被稱爲出邊表。
如下圖所示:
如果是網,則用下面的存儲方式:
下面看一下鄰接表存儲結構的定義:
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;
鄰接表存儲方式的缺點: 若圖爲有向圖的話,從鄰接表中只能找到某一個頂點的出度,如果想要知道它的入度的話,則需要遍歷整個圖;如果使用逆鄰接表的話,也只能找到某一個頂點的入度,想要知道它的出度的話,則需要遍歷整個圖。
3、十字鏈表
十字鏈表將鄰接表和逆鄰接表整合到了一起。存儲方式如下:
重新定義頂點表結構如下:
- firstin: 入邊表頭指針,指向該頂點的入邊表中第一個結點
- firstout: 出邊表頭指針,指向該頂點的出邊表中的第一個結點
重新定義邊表結點結構如下:
- tailvex: 弧尾頂點在頂點表中的下標
- headvex: 弧頭頂點在頂點表中的下標
- headlink: 入邊表指針域,指向終點相同的下一條邊
- taillink: 出邊表指針域,指向起點相同的下一條邊
- 如果是網的話,還需要增加一個weight域用來存儲權值
十字鏈表存儲的示例:
十字鏈表的好處就是它把鄰接表和逆鄰接表整合到了一起,這樣既容易找到以爲尾的弧,也容易找到以爲頭的弧,可以輕易求得頂點的出度和入度。
4、鄰接多重表
鄰接多重表有益於對無向圖中的邊的操作,重新定義的邊表結點結構如下:
- ivex、jvex是與某條邊依附的兩個頂點在頂點表中的下標
- ilink指向依附頂點ivex的下一條邊
- jlink指向依附頂點jvex的下一條邊
如下圖所示:
5、邊集數組
邊集數組由兩個一維數組構成。一個存儲頂點的信息;另一個是存儲邊的信息,邊數組的每個數據元素由這條邊的起點下標(begin)、終點下標(end)和權值(weight)組成。
3、圖的遍歷(深度優先遍歷、廣度優先遍歷)
深度優先遍歷:
鄰接矩陣的深度優先遍歷僞代碼:
鄰接表的深度優先遍歷僞代碼:
複雜度分析:
- 鄰接矩陣表示法需要將鄰接矩陣的每個元素都遍歷一次,時間複雜度爲
- 鄰接表 表示法需要將頂點表和邊表的每個元素都遍歷一次,時間複雜度爲
廣度優先遍歷:
鄰接矩陣存儲方式的廣度優先遍歷僞代碼:
鄰接表存儲方式的廣度優先遍歷僞代碼:
廣度優先遍歷的時間複雜度與深度優先遍歷的時間複雜度是相同的。總結一句:深度優先遍歷就是縱向發展,廣度優先遍歷是橫向發展。
4、最小生成樹
最小生成樹是相對於網而言的:其含有網的全部n個節點n-1條邊,並且n-1條邊的權值之和最小。
生成最小生成樹的算法:
- 普里姆算法
- 克魯斯卡爾算法
4-1 普里姆算法
基本思想:從任意頂點v開始,先把該頂點加入一個集合V中,然後不斷的選取與集合V的所有鄰接邊中權值最小的邊和頂點加入該集合,直到集合V中的頂點數量等於網的頂點數。
4-2 克魯斯卡爾算法
基本思想:先把所有的邊按照權值排序,然後從權值最小的邊開始遍歷所有的邊:如果該邊加入集合中不構成環,則將該邊以及其鄰接的頂點一起加入;否在捨棄這一條邊。所有的邊遍歷完後,集合中的頂點和邊就是原圖的最大生成樹。
該算法爲了能夠方便的對邊按照權值進行排序,對圖採用了邊集數組的存儲方式,邊集數組的數據元素結構定義如下:
typedef struct {
int begin;
int end;
int weight;
} Edge;
如何判斷一個新的頂點和邊加入不會形成迴路:並查集
克魯斯卡爾的僞代碼實現:
void MiniSpanTree_Krukal(MGraph G)
{
int i, n, m;
Edge edges[MAXEDGE];
int parent[MAXVEX];
··· //邊集數組排序
//初始化並查集
for(int i = 0; i < G.numVertexes; i++)
parent[i] = i;
//循環遍歷每一條邊
for(int i = 0; i < G.numEdges; i++)
{
n = Find(parent, edges[i].begin); //找到begin所在集合的代表元素
m = Find(parent, edges[i].end); //找到end所在集合的代表元素
if(n != m)
{
// 並查集合並
parent[n] = m;
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
// 並查集查找
int Find(int *parent, int f)
{
while(f != parent[f])
f = parent[f];
return f;
}
複雜度分析: Find()函數的時間複雜度由e決定,時間複雜度爲loge,加上外層的For循環,總的時間複雜度爲eloge。
兩種尋找最小生成樹的方法的差別:克魯斯卡爾算法主要是針對邊來展開的,對於稀疏圖來說有很大的優勢;普里姆算法是從點出發考慮的,更適合稠密圖。
5、最短路徑
5-1 迪傑斯特拉算法
迪傑斯特拉算法能夠方便的求得網中的某一個源點到其餘各個頂點的最短路徑,時間複雜度爲。
迪傑斯特拉算法的最終輸出可以認爲是兩個數組:
- : 表示從頂點到的最短路徑長度
- : 表示頂點的最短路徑中的前驅頂點
迪傑斯特拉算法的限制:圖中不可以有負的權值存在
迪傑斯特拉算法的關鍵在於如何更新上面所說的這兩個數組:
5-2 洛伊德算法
弗洛伊德算法可以方便求得圖中的任一頂點到其它所有頂點的最短路徑。時間複雜度爲。
6、拓補排序
6-1 什麼是AOV網?
- 一個有向無環圖
- 頂點表示活動
- 弧表示活動之間的優先關係
6-2 什麼是拓補序列?
AOV網的一個頂點序列,該頂點序列滿足:包含網中所有頂點,並且若網中存在從到的弧,則在頂點序列中一定排在的前面。
6-3 拓補排序算法
- 從圖中選擇一個入度爲0的頂點並輸出
- 刪除該頂點以及從該頂點出發的所有邊
- 重複上面兩步,直到所有頂點都被刪除或者沒有入度爲0的頂點(如果是這種情況就說明圖中有環了)
6、關鍵路徑
略