數據結構——圖基本概念
線性表中的元素是“一對一”的關系,樹中的元素是“一對多”的關系,本章所述的圖結構中的元素則是“多對多”的關系。圖(Graph)是一種復雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關系是任意的。現實生活中的很多事物都可以抽象為圖,例如世界各地接入Internet的計算機通過網線連接在一起,各個城市和城市之間的鐵軌等等。
一、圖的基本概念
1.1 多對多的復雜關系
現實中人與人之間關系非常復雜,比如我認識的朋友,可能他們之間也互相認識,這不是簡單的一對一、一對多,研究人際關系很自然會考慮多對多的情況。圖是一種較線性表和樹更加復雜的數據結構。在圖形結構中,結點之間的關系可以是任意的,圖中任意兩個數據元素之間都可能相關。
定義:圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
在圖中需要註意的是:
(1)線性表中我們把數據元素叫元素,樹中將數據元素叫結點,在圖中數據元素,我們則稱之為頂點(Vertex)。
(2)線性表可以沒有元素,稱為空表;樹中可以沒有節點,稱為空樹;但是,在圖中不允許沒有頂點(有窮非空性)。
(3)線性表中的各元素是線性關系,樹中的各元素是層次關系,而圖中各頂點的關系是用邊來表示(邊集可以為空)。
1.2 紛繁冗多的術語
圖的基本術語有很多,本文只挑選幾個特別重要的來說明,其余的請閱讀相關教材。
(1)無向圖
如果圖中任意兩個頂點之間的邊都是無向邊(簡而言之就是沒有方向的邊),則稱該圖為無向圖(Undirected graphs)。
(2)有向圖
如果圖中任意兩個頂點之間的邊都是有向邊(簡而言之就是有方向的邊),則稱該圖為有向圖(Directed graphs)。
(3)完全圖
①無向完全圖:在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖為無向完全圖。(含有n個頂點的無向完全圖有(n×(n-1))/2條邊)如下圖所示:
②有向完全圖:在有向圖中,如果任意兩個頂點之間都存在方向互為相反的兩條弧,則稱該圖為有向完全圖。(含有n個頂點的有向完全圖有n×(n-1)條邊)如下圖所示:
PS:當一個圖接近完全圖時,則稱它為稠密圖(Dense Graph),而當一個圖含有較少的邊時,則稱它為稀疏圖(Spare Graph)。
(4)頂點的度
頂點Vi的度(Degree)是指在圖中與Vi相關聯的邊的條數。對於有向圖來說,有入度(In-degree)和出度(Out-degree)之分,有向圖頂點的度等於該頂點的入度和出度之和。
(5)鄰接
①若無向圖中的兩個頂點V1和V2存在一條邊(V1,V2),則稱頂點V1和V2鄰接(Adjacent);
②若有向圖中存在一條邊<V3,V2>,則稱頂點V3與頂點V2鄰接,且是V3鄰接到V2或V2鄰接直V3;
PS:無向圖中的邊使用小括號“()”表示,而有向圖中的邊使用尖括號“<>”表示。
(6)路徑
在無向圖中,若從頂點Vi出發有一組邊可到達頂點Vj,則稱頂點Vi到頂點Vj的頂點序列為從頂點Vi到頂點Vj的路徑(Path)。
(7)連通
若從Vi到Vj有路徑可通,則稱頂點Vi和頂點Vj是連通(Connected)的。
(8)權
有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的數叫做權(Weight)。
二、圖的存儲結構
圖的存儲結構除了要存儲圖中的各個頂點本身的信息之外,還要存儲頂點與頂點之間的關系,因此,圖的結構也比較復雜。常用的圖的存儲結構有鄰接矩陣和鄰接表等。
2.1 鄰接矩陣表示法
圖的鄰接矩陣(Adjacency Matrix)存儲方式是用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱為鄰接矩陣)存儲圖中的邊或弧的信息。
(1)無向圖:
我們可以設置兩個數組,頂點數組為vertex[4]={v0,v1,v2,v3},邊數組arc[4][4]為上圖右邊這樣的一個矩陣。對於矩陣的主對角線的值,即arc[0][0]、arc[1][1]、arc[2][2]、arc[3][3],全為0是因為不存在頂點的邊。
(2)有向圖:
我們再來看一個有向圖樣例,如下圖所示的左邊。頂點數組為vertex[4]={v0,v1,v2,v3},弧數組arc[4][4]為下圖右邊這樣的一個矩陣。主對角線上數值依然為0。但因為是有向圖,所以此矩陣並不對稱,比如由v1到v0有弧,得到arc[1][0]=1,而v到v沒有弧,因此arc[0][1]=0。
不足:由於存在n個頂點的圖需要n*n個數組元素進行存儲,當圖為稀疏圖時,使用鄰接矩陣存儲方法將會出現大量0元素,這會造成極大的空間浪費。這時,可以考慮使用鄰接表表示法來存儲圖中的數據。
2.2 鄰接表表示法
首先,回憶我們在線性表時談到,順序存儲結構就存在預先分配內存可能造成存儲空間浪費的問題,於是引出了鏈式存儲的結構。同樣的,我們也可以考慮對邊或弧使用鏈式存儲的方式來避免空間浪費的問題。
鄰接表由表頭節點和表節點兩部分組成,圖中每個頂點均對應一個存儲在數組中的表頭節點。如果這個表頭節點所對應的頂點存在鄰接節點,則把鄰接節點依次存放於表頭節點所指向的單向鏈表中。
(1)無向圖:下圖所示的就是一個無向圖的鄰接表結構。
從上圖中我們知道,頂點表的各個結點由data和firstedge兩個域表示,data是數據域,存儲頂點的信息,firstedge是指針域,指向邊表的第一個結點,即此頂點的第一個鄰接點。邊表結點由adjvex和next兩個域組成。adjvex是鄰接點域,存儲某頂點的鄰接點在頂點表中的下標,next則存儲指向邊表中下一個結點的指針。例如:v1頂點與v0、v2互為鄰接點,則在v1的邊表中,adjvex分別為v0的0和v2的2。
PS:對於無向圖來說,使用鄰接表進行存儲也會出現數據冗余的現象。例如上圖中,頂點V0所指向的鏈表中存在一個指向頂點V3的同事,頂點V3所指向的鏈表中也會存在一個指向V0的頂點。
(2)有向圖:若是有向圖,鄰接表結構是類似的,但要註意的是有向圖由於有方向的。因此,有向圖的鄰接表分為出邊表和入邊表(又稱逆鄰接表),出邊表的表節點存放的是從表頭節點出發的有向邊所指的尾節點;入邊表的表節點存放的則是指向表頭節點的某個頂點,如下圖所示。
(3)帶權圖:對於帶權值的網圖,可以在邊表結點定義中再增加一個weight的數據域,存儲權值信息即可,如下圖所示。
三、圖的模擬實現
PS:由於鄰接矩陣容易造成空間資源的浪費,因此這裏只考慮使用鄰接表來實現。
3.1 總體設計結構
(1)鏈表節點定義
①表頭節點Vertex
/// <summary> /// 嵌套類:存放於數組中的表頭節點 /// </summary> /// <typeparam name="TValue"></typeparam> protected class Vertex<TValue> { public TValue data; // 數據 public Node firstEdge; // 鄰接點鏈表頭指針 public bool isVisited; // 訪問標誌:遍歷時使用 public Vertex() { this.data = default(TValue); } public Vertex(TValue value) { this.data = value; } }
②表節點Node
/// <summary> /// 嵌套類:鏈表中的表節點 /// </summary> protected class Node { public Vertex<T> adjvex; // 鄰接點域 public Node next; // 下一個鄰接點指針域 public Node() { this.adjvex = null; } public Node(Vertex<T> value) { this.adjvex = value; } }
(2)鄰接表總體定義
public class MyAdjacencyList<T> where T : class { private List<Vertex<T>> items; // 圖的頂點集合 public MyAdjacencyList() : this(10) { } public MyAdjacencyList(int capacity) { this.items = new List<Vertex<T>>(capacity); } #region 基本方法:為圖中添加頂點、添加有向與無向邊 #endregion #region 輔助方法:圖中是否包含某個元素、查找指定頂點、初始化visited標誌 #endregion #region 嵌套類:表頭節點與表節點定義 #endregion }
首先,我們使用了一個動態集合List來代替數組存儲Vertex的集合,默認容量為10,且不需要數組存儲空間不夠的情況,簡化了操作。其次,我們要定義一些基本方法,如添加頂點、添加邊。還要定義一些輔助方法,如判斷是否包含某個元素等(詳見完整代碼文件)。最後,我們再實現圖的一些遍歷算法,如深度優先遍歷與廣度優先遍歷(本篇不作介紹,下一篇再介紹)。
3.2 基本方法實現
(1)添加一個頂點
/// <summary> /// 添加一個頂點 /// </summary> /// <param name="item">頂點元素data</param> public void AddVertex(T item) { if (Contains(item)) { throw new ArgumentException("添加了重復的頂點!"); } Vertex<T> newVertex = new Vertex<T>(item); items.Add(newVertex); }
就是往集合裏邊加入新元素;
(2)添加一條邊
這裏需要分為兩種情況,一種是添加無向圖的邊,這時無向圖的兩個頂點都需要記錄邊的信息。另一種則是添加有向圖的邊,這時只需要一條記錄;
①無向圖
/// <summary> /// 添加一條無向邊 /// </summary> /// <param name="from">頭頂點data</param> /// <param name="to">尾頂點data</param> /// <param name="weight">權值</param> public void AddEdge(T from, T to) { Vertex<T> fromVertex = Find(from); if (fromVertex == null) { throw new ArgumentException("頭頂點不存在!"); } Vertex<T> toVertex = Find(to); if (toVertex == null) { throw new ArgumentException("尾頂點不存在!"); } // 無向圖的兩個頂點都需要記錄邊的信息 AddDirectedEdge(fromVertex, toVertex); AddDirectedEdge(toVertex, fromVertex); }
這裏可以看到這兩句代碼,對應的兩個頂點都記錄了邊的信息。
// 無向圖的兩個頂點都需要記錄邊的信息 AddDirectedEdge(fromVertex, toVertex); AddDirectedEdge(toVertex, fromVertex);
②有向圖
/// <summary> /// 添加一條有向邊 /// </summary> /// <param name="from">頭結點data</param> /// <param name="to">尾節點data</param> public void AddDirectedEdge(T from, T to) { Vertex<T> fromVertex = Find(from); if (fromVertex == null) { throw new ArgumentException("頭頂點不存在!"); } Vertex<T> toVertex = Find(to); if (toVertex == null) { throw new ArgumentException("尾頂點不存在!"); } AddDirectedEdge(fromVertex, toVertex); }
③如何添加邊
在實現中,無論是無線圖還是有向圖都是添加的有向邊,只不過無向圖是添加了兩條有向邊:
/// <summary> /// 添加一條有向邊 /// </summary> /// <param name="fromVertex">頭頂點</param> /// <param name="toVertex">尾頂點</param> private void AddDirectedEdge(Vertex<T> fromVertex, Vertex<T> toVertex) { if (fromVertex.firstEdge == null) { fromVertex.firstEdge = new Node(toVertex); } else { Node temp = null; Node node = fromVertex.firstEdge; do { // 檢查是否添加了重復邊 if (node.adjvex.data.Equals(toVertex.data)) { throw new ArgumentException("添加了重復的邊!"); } temp = node; node = node.next; } while (node != null); Node newNode = new Node(toVertex); temp.next = newNode; } }
(3)打印每個頂點及其鄰接點的信息
/// <summary> /// 打印每個頂點和它的鄰接點 /// </summary> /// <param name="isDirectedGraph">是否是有向圖</param> public string GetGraphInfo(bool isDirectedGraph = false) { StringBuilder sb = new StringBuilder(); foreach (Vertex<T> v in items) { sb.Append(v.data.ToString() + ":"); if (v.firstEdge != null) { Node temp = v.firstEdge; while (temp != null) { if (isDirectedGraph) { sb.Append(v.data.ToString() + "→" + temp.adjvex.data.ToString() + " "); } else { sb.Append(temp.adjvex.data.ToString()); } temp = temp.next; } } sb.Append("\r\n"); } return sb.ToString(); }
這裏判斷了是否是有向圖,如果是有向圖則顯示A→B的形式,如果是無向圖則顯示A:B的形式。
完整實現:
/// <summary> /// 模擬圖的鄰接表 /// </summary> /// <typeparam name="T"></typeparam> public class MyAdjacencyList<T> where T : class { private List<Vertex<T>> items; // 圖的頂點集合 public MyAdjacencyList() : this(10) { } public MyAdjacencyList(int capacity) { this.items = new List<Vertex<T>>(capacity); } #region 基本方法:為圖中添加頂點、添加有向與無向邊 /// <summary> /// 添加一個頂點 /// </summary> /// <param name="item">頂點元素data</param> public void AddVertex(T item) { if (Contains(item)) { throw new ArgumentException("添加了重復的頂點!"); } Vertex<T> newVertex = new Vertex<T>(item); items.Add(newVertex); } /// <summary> /// 添加一條無向邊 /// </summary> /// <param name="from">頭頂點data</param> /// <param name="to">尾頂點data</param> /// <param name="weight">權值</param> public void AddEdge(T from, T to) { Vertex<T> fromVertex = Find(from); if (fromVertex == null) { throw new ArgumentException("頭頂點不存在!"); } Vertex<T> toVertex = Find(to); if (toVertex == null) { throw new ArgumentException("尾頂點不存在!"); } // 無向圖的兩個頂點都需要記錄邊的信息 AddDirectedEdge(fromVertex, toVertex); AddDirectedEdge(toVertex, fromVertex); } /// <summary> /// 添加一條有向邊 /// </summary> /// <param name="from">頭結點data</param> /// <param name="to">尾節點data</param> public void AddDirectedEdge(T from, T to) { Vertex<T> fromVertex = Find(from); if (fromVertex == null) { throw new ArgumentException("頭頂點不存在!"); } Vertex<T> toVertex = Find(to); if (toVertex == null) { throw new ArgumentException("尾頂點不存在!"); } AddDirectedEdge(fromVertex, toVertex); } /// <summary> /// 添加一條有向邊 /// </summary> /// <param name="fromVertex">頭頂點</param> /// <param name="toVertex">尾頂點</param> private void AddDirectedEdge(Vertex<T> fromVertex, Vertex<T> toVertex) { if (fromVertex.firstEdge == null) { fromVertex.firstEdge = new Node(toVertex); } else { Node temp = null; Node node = fromVertex.firstEdge; do { // 檢查是否添加了重復邊 if (node.adjvex.data.Equals(toVertex.data)) { throw new ArgumentException("添加了重復的邊!"); } temp = node; node = node.next; } while (node != null); Node newNode = new Node(toVertex); temp.next = newNode; } } /// <summary> /// 打印每個頂點和它的鄰接點 /// </summary> /// <param name="isDirectedGraph">是否是有向圖</param> public string GetGraphInfo(bool isDirectedGraph = false) { StringBuilder sb = new StringBuilder(); foreach (Vertex<T> v in items) { sb.Append(v.data.ToString() + ":"); if (v.firstEdge != null) { Node temp = v.firstEdge; while (temp != null) { if (isDirectedGraph) { sb.Append(v.data.ToString() + "→" + temp.adjvex.data.ToString() + " "); } else { sb.Append(temp.adjvex.data.ToString()); } temp = temp.next; } } sb.Append("\r\n"); } return sb.ToString(); } #endregion #region 輔助方法:圖中是否包含某個元素、查找指定頂點、初始化visited標誌 /// <summary> /// 輔助方法:查找圖中是否包含某個元素 /// </summary> private bool Contains(T item) { if (item == default(T)) return false; return items.Exists(x => x.data == item); } /// <summary> /// 輔助方法:查找指定項並返回 /// </summary> private Vertex<T> Find(T item) { return items.FirstOrDefault(x => x.data == item); } /// <summary> /// 輔助方法:初始化頂點的visited標誌為false /// </summary> private void InitVisited() { foreach (Vertex<T> v in items) { v.isVisited = false; } } #endregion #region 嵌套類1:存放於數組中的表頭節點 /// <summary> /// 嵌套類:存放於數組中的表頭節點 /// </summary> /// <typeparam name="TValue"></typeparam> protected class Vertex<TValue> { public TValue data; // 數據 public Node firstEdge; // 鄰接點鏈表頭指針 public bool isVisited; // 訪問標誌:遍歷時使用 public Vertex() { this.data = default(TValue); } public Vertex(TValue value) { this.data = value; } } #endregion #region 嵌套類2:鏈表中的表節點 /// <summary> /// 嵌套類:鏈表中的表節點 /// </summary> protected class Node { public Vertex<T> adjvex; // 鄰接點域 public Node next; // 下一個鄰接點指針域 public Node() { this.adjvex = null; } public Node(Vertex<T> value) { this.adjvex = value; } } #endregion }View Code
3.3 基本功能測試
這裏我們對基本功能做一下測試,分為無向圖和有向圖,首先插入頂點及對應邊,然後打印頂點及其鄰接表的信息,要構造的無向圖與有向圖如上面兩張圖所示,測試代碼如下所示:
static void MyAdjacencyListTest() { Console.WriteLine("------------無向圖------------"); MyAdjacencyList<string> adjList = new MyAdjacencyList<string>(); // 添加頂點 adjList.AddVertex("A"); adjList.AddVertex("B"); adjList.AddVertex("C"); adjList.AddVertex("D"); //adjList.AddVertex("D"); // 會報異常:添加了重復的節點 // 添加無向邊 adjList.AddEdge("A", "B"); adjList.AddEdge("A", "C"); adjList.AddEdge("A", "D"); adjList.AddEdge("B", "D"); //adjList.AddEdge("B", "D"); // 會報異常:添加了重復的邊 Console.Write(adjList.GetGraphInfo()); Console.WriteLine("------------有向圖------------"); MyAdjacencyList<string> dirAdjList = new MyAdjacencyList<string>(); // 添加頂點 dirAdjList.AddVertex("A"); dirAdjList.AddVertex("B"); dirAdjList.AddVertex("C"); dirAdjList.AddVertex("D"); // 添加有向邊 dirAdjList.AddDirectedEdge("A", "B"); dirAdjList.AddDirectedEdge("A", "C"); dirAdjList.AddDirectedEdge("A", "D"); dirAdjList.AddDirectedEdge("B", "D"); Console.Write(dirAdjList.GetGraphInfo(true)); }
運行結果如下圖所示:
作者:周旭龍
出處:http://edisonchou.cnblogs.com
數據結構——圖基本概念