一,概述
彌諾陶洛斯(Minotaur)是希臘神話中半人半牛的怪物,它藏身于一個精心設計的迷宮之中。這個迷宮的結構極其復雜,一般人一旦進入其中,都休想走出來。不過,在公主阿里阿德涅(Ariadne)的幫助下,古希臘英雄特修斯(Theseus)還是想出了一個走出迷宮的方法。特修斯帶上一團線去闖迷宮,在入口處,他將繩線的一頭綁在門上。然后,他不斷查找迷宮的各個角落,而繩線的另一頭則始終抓在他的手里,跟隨他穿梭于蜿蜒曲折的迷宮之中。借助如此簡單的工具,特修斯不僅終于找到了怪物并將其殺死,而且還帶著公主輕松地走出了迷宮。
特修斯之所以能夠成功,關鍵在于他借助繩線來掌握迷宮內各個通道之間的聯接關系,而在很多的問題中,掌握類似的信息都是至關重要的。通常,這類信息所反映的都是一組對象之間的二元關系,比如城市交通圖中聯接于不同點之間的街道,或 Internet 中聯接于兩個 IP 之間的路由等。
在某種程度上,我們前面所討論過的樹結構也可以攜帶和表示這種二元關系,只不過樹結構中的這類關系僅限于父、子節點之間。然而在一般情況下,我們所需要的二元關系往往是定義在任意一對對象之間。實際上,這樣的二元關系恰恰正是圖論(Graph theory )
** 圖(Graph)可以表示為 G = (V, E):**
其中集合 V 中的對象稱作頂點(Vertex);
而集合 E 中的每一元素都對應于 V 中某一對頂點??說明這兩個頂點之間存在某種關系??稱作邊(Edge)。
這里還約定 V 和 E 都是有限集,通常記 n = |V|、m = |E|。
二,無向圖、混合圖及有向圖
圖中的邊可以是沒有方向的,也可以是有方向的。
如果邊 e = (u, v)所對應的頂點 u 和 v 不是對等的,或者存在某種次序,就稱 e 為有向邊(Directed edge)。
如果 u 和 v 的次序無所謂,e 就是一條無向邊(Undirected edge)。
注意:無向邊(u, v)也可以記作(v, u),而有向邊(u, v)和(v, u)則是不同的兩條邊。
(a)無向圖(b)混合圖(c)有向圖
頂點v的關聯邊的總數,稱為v的度數(Degree),記作deg(v)。以 (a)為例,有deg(a) = deg(c) = 3。
v 出邊總數稱作v的出度(Out-degree),記作outdeg(v);
v入邊總數稱作v的入度(In-degree),記作indeg(v)。
以(c)為例,有outdeg(a) = indeg(a) = outdeg(c) = indeg(c) =3。
圖中所含的邊并不見得能構成一個集合(Set),準確地說它們構成了一個復集(Multiset)??其中允許出現重復的元素。
比如,若在某對頂點之間有多條無向邊,或者兩條有向邊的起點和終點
完全一樣,就屬于這種情況。這類重復的邊也稱作平行邊(Parallel e dges)或多重邊(Multiple edges)。例如,要是用頂點表示城市,用邊表示城市之間的飛機航線,則有可能在某一對城市之間存在多條航線;如果用頂點表示演員,則某兩位演員合演過的影片很有可能不止一部。
無論是無向圖還是有向圖,還有另一種特殊情況:與某條邊關聯的是同一個頂點(如上圖中的頂點a)??這樣的邊稱作** 自環(Self-loop)**。
不含上述特殊邊的圖,稱作簡單圖(Simple graph)。對簡單圖而言,其中的邊必然構成一個集合,而且每條邊只能聯接于不同的頂點之間。本文我們討論的圖都限于簡單圖。
三,通路、環路及可達分量
通路——所謂圖中的一條通路或路徑(Path),就是由(不一定互異的)m+1 個頂點與 m 條邊交替構成的一個序列ρ = {v 0 , e 1 , v 1 , e 2 , v 2 , …, e m , v m },m ≥ 0,而且 e i = (v i-1 , v i ),1 ≤ i ≤ m。
環路—— 長度 m ≥ 1 的路徑,若第一個頂點與最后一個頂點相同,則稱之為環路(Cycle)。
可達分量——對于指定的頂點 s,從 s 可達的所有頂點所組成的集合,稱作 s 在 G 中對應的可達分量,記作 V r (G, s)。
四,樹、森林和連通圖
無向圖 G = (V, E):
若 G 中不含任何環路,則稱之為森林(Forest)。
連通的森林稱作樹(Tree)。
不難看出,森林中的每一連通分量都是一棵樹,反之亦然。
設 G 為由 n 個頂點與 m 條邊組成一幅無向圖:
- (1)若 G 是連通的,則 m ≥ n-1;
- (2)若 G 是一棵樹,則 m = n-1;
- (3)若 G 是森林,則 m ≤ n-1。
五,圖 ADT
(這里以有向圖為例介紹圖結構及其算法)
作為一種抽象數據類型,有向圖必須支持以下操作:
操作方法 | 功能描述 |
---|---|
vNumber() | 返回頂點集 V 的規模 |
eNumber() | 返回邊集 E 的規模 |
vertices() | 返回所有頂點的迭代器 |
vPositions() | 返回所有頂點位置的迭代器 |
edges() | 返回所有邊的迭代器 |
ePositions() | 返回所有邊位置的迭代器 |
areAdjacent(u, v) | 判斷頂點 u 和 v 是否相鄰 |
remove(v) | 將頂點 v 從頂點集中刪除,并返回之 |
insert(v) | 在頂點集 V 中插入新頂點 v,并返回其位置 |
insert(e) | 在邊集 E 中插入新邊 e,并返回其位置 |
六,圖的實現
** 基于鄰接矩陣(Adjacency matrix)的圖的實現 **
圖 ADT 有多種實現方式,其中最直接的就是基于鄰接矩陣(Adjacency matrix)的實現。
若圖 G 中包含 n 個頂點,我們就使用一個 n×n 的方陣 A,并使每一頂點都分別對應于某一行(列)。
既然圖所描述的是這些頂點各自對應的元素之間的二元關系,故可以很自然地將任意一對元素 u 和 v 之間可能存在二元關系與矩陣 A 中對應的單元 A[u, v]對應起來:
1 或 true 表示存在關系,0 或 false 表示不存在關系。
這一矩陣中的各個單元分別描述了一對元素之間可能存在的鄰接關系,故此得名。
以上基于鄰接矩陣的實現方法直觀易懂、思路簡明,而且能夠高效地實現圖的大多數 ADT 操作。
但是矩陣結構是靜態的,通常都是事先估計一個較大的整數 N,然后創建一個 N×N 的矩陣。
然而,圖的規模往往都是動態變化的,因此如果 N 不是足夠大,極有可能出現因空間“不足”而無法加入新頂點的情況??而實際上,此時系統并非沒有更多空間可以提供。為了降低這種情況發生的概率,必須選用足夠大的 N,而如此一來,單元閑置的程度也將加劇。
** 鄰接表 **
由上面的分析可知,鄰接矩陣的空間效率之所以低,是因為其中大量的單元所對應的邊有可能并未在圖中出現,這也是靜態向量結構普遍的不足。
既然如此,為什么不將向量改進為列表呢?實際上,按照這一思路的確可以導出圖結構的另一種表示與實現形式——鄰接表(Adjacency list)。
基于列表實現的頂點與邊的結構:
** 具體由以下3個接口和對應的3個類實現:**
(有向)圖的頂點結構接口:
package dsa.Graph;
import dsa.Iterator.Iterator;
import other.Position;
public interface Vertex {
/*
* (有向)圖的頂點結構接口
*/
// 常量
final static int UNDISCOVERED = 0;// 尚未被發現的頂點
final static int DISCOVERED = 1;// 已被發現的頂點
final static int VISITED = 2;// 已訪問過的頂點
// 返回當前頂點的信息
public Object getInfo();
// 將當前頂點的信息更新為x,并返回原先的信息
public Object setInfo(Object x);
// 返回當前頂點的出、入度
public int outDeg();
public int inDeg();
// 返回當前頂點所有關聯邊、關聯邊位置的迭代器
public Iterator inEdges();
public Iterator inEdgePositions();
public Iterator outEdges();
public Iterator outEdgePositions();
// 取當前頂點在所屬的圖的頂點集V中的位置
public Position getVPosInV();
// 讀取、設置頂點的狀態(DFS + BFS)
public int getStatus();
public int setStatus(int s);
// 讀取、設置頂點的時間標簽(DFS)
public int getDStamp();
public int setDStamp(int s);
public int getFStamp();
public int setFStamp(int s);
// 讀取、設置頂點至起點的最短距離(BFS或BestFS)
public int getDistance();
public int setDistance(int s);
// 讀取、設置頂點在的DFS、BFS、BestFS或MST樹中的父親
public Vertex getBFSParent();
public Vertex setBFSParent(Vertex s);
}
(有向)圖的邊結構接口
package dsa.Graph;
import other.Position;
public interface Edge {
/*
* (有向)圖的邊結構接口
*/
// 常量
final static int UNKNOWN = 0;// 未知邊
final static int TREE = 1;// 樹邊
final static int CROSS = 2;// 橫跨邊
final static int FORWARD = 3;// 前向跨邊
final static int BACKWARD = 4;// 后向跨邊
// 返回當前邊的信息(對于帶權圖,也就是各邊的權重)
public Object getInfo();
// 將當前邊的信息更新為x,并返回原先的信息
public Object setInfo(Object x);
// 取當前邊在所屬的圖的邊集E中的位置
public Position getEPosInE();
// 取v[i]在頂點集V中的位置(i=0或1,分別對應于起點、終點)
public Position getVPosInV(int i);
// 當前邊在其兩個端點的關聯邊集I(v[i])中的位置
public Position getEPosInI(int i);
// 讀取、設置邊的類別(針對遍歷)
public int getType();
public int setType(int t);
}
(有向)圖結構接口
package dsa.Graph;
import dsa.Iterator.Iterator;
import other.Position;
public interface Graph {
/*
* (有向)圖結構接口
*/
// 取圖中頂點、邊的數目
public int vNumber();
public int eNumber();
// 取圖中所有頂點、頂點位置的迭代器
public Iterator vertices();
public Iterator vPositions();
// 返回圖中所有邊、邊位置的迭代器
public Iterator edges();
public Iterator ePositions();
// 檢測是否有某條邊從頂點u指向v
public boolean areAdjacent(Vertex u, Vertex v);
// 取從頂點u指向v的邊,若不存在,則返回null
public Edge edgeFromTo(Vertex u, Vertex v);
// 將頂點v從頂點集中刪除,并返回之
public Vertex remove(Vertex v);
// 將邊e從邊集中刪除,并返回之
public Edge remove(Edge e);
// 在頂點集V中插入新頂點v,并返回其位置
public Position insert(Vertex v);
// 在邊集E中插入新邊e,并返回其位置
public Position insert(Edge e);
}
基于鄰接邊表實現圖的頂點結構
package dsa.Graph;
import dsa.Iterator.Iterator;
import dsa.List.List;
import dsa.List.List_DLNode;
import other.Position;
public class Vertex_List implements Vertex {
/*
* 基于鄰接邊表實現圖的頂點結構
*/
// 變量
protected Object info;// 當前頂點中存放的數據元素
protected Position vPosInV;// 當前頂點在所屬的圖的頂點表V中的位置
protected List outEdges;// 關聯邊表:存放以當前頂點為尾的所有邊(的位置)
protected List inEdges;// 關聯邊表:存放以當前頂點為頭的所有邊(的位置)
protected int status;// (在遍歷圖等操作過程中)頂點的狀態
protected int dStamp;// 時間標簽:DFS過程中該頂點被發現時的時刻
protected int fStamp;// 時間標簽:DFS過程中該頂點被訪問結束時的時刻
protected int distance;// 到指定起點的距離:BFS、Dijkstra等算法所確定該頂點到起點的距離
protected Vertex bfsParent;// 在最短距離樹(BFS或BestFS)中的父親
// 構造方法:在圖G中引入一個屬性為x的新頂點
public Vertex_List(Graph G, Object x) {
info = x;// 數據元素
vPosInV = G.insert(this);// 當前頂點在所屬的圖的頂點表V中的位置
outEdges = new List_DLNode();// 出邊表
inEdges = new List_DLNode();// 入邊表
status = UNDISCOVERED;
dStamp = fStamp = Integer.MAX_VALUE;
distance = Integer.MAX_VALUE;
bfsParent = null;
}
// 返回當前頂點的信息
public Object getInfo() {
return info;
}
// 將當前頂點的信息更新為x,并返回原先的信息
public Object setInfo(Object x) {
Object e = info;
info = x;
return e;
}
// 返回當前頂點的出、入度
public int outDeg() {
return outEdges.getSize();
}
public int inDeg() {
return inEdges.getSize();
}
// 返回當前頂點所有關聯邊、關聯邊位置的迭代器
public Iterator inEdges() {
return inEdges.elements();
}
public Iterator inEdgePositions() {
return inEdges.positions();
}
public Iterator outEdges() {
return outEdges.elements();
}
public Iterator outEdgePositions() {
return outEdges.positions();
}
// 取當前頂點在所屬的圖的頂點集V中的位置
public Position getVPosInV() {
return vPosInV;
}
// 讀取、設置頂點的狀態(DFS + BFS)
public int getStatus() {
return status;
}
public int setStatus(int s) {
int ss = status;
status = s;
return ss;
}
// 讀取、設置頂點的時間標簽(DFS)
public int getDStamp() {
return dStamp;
}
public int setDStamp(int s) {
int ss = dStamp;
dStamp = s;
return ss;
}
public int getFStamp() {
return fStamp;
}
public int setFStamp(int s) {
int ss = fStamp;
fStamp = s;
return ss;
}
// 讀取、設置頂點至起點的最短距離(BFS)
public int getDistance() {
return distance;
}
public int setDistance(int s) {
int ss = distance;
distance = s;
return ss;
}
// 讀取、設置頂點在的DFS、BFS、BestFS或MST樹中的父親
public Vertex getBFSParent() {
return bfsParent;
}
public Vertex setBFSParent(Vertex s) {
Vertex ss = bfsParent;
bfsParent = s;
return ss;
}
}
基于鄰接邊表實現圖的邊結構
package dsa.Graph;
import dsa.Deque.DLNode;
import other.Position;
public class Edge_List implements Edge {
/*
* 基于鄰接邊表實現圖的邊結構
*/
// 變量
protected Object info;// 當前邊中存放的數據元素
protected Position ePosInE;// 當前邊在所屬的圖的邊表中的位置
protected Position vPosInV[];// 當前邊兩個端點在頂點表中的位置
protected Position ePosInI[];// 當前邊在其兩個端點的關聯邊表中的位置
// 約定:第0(1)個頂點分別為尾(頭)頂點
// 禁止頭、尾頂點相同的邊
protected int type;// (經過遍歷之后)邊被歸入的類別
// 構造方法:在圖G中,生成一條從頂點u到v的新邊(假定該邊尚不存在)
public Edge_List(Graph G, Vertex_List u, Vertex_List v, Object x) {
info = x;// 數據元素
ePosInE = G.insert(this);// 當前邊在所屬的圖的邊表中的位置
vPosInV = new DLNode[2];// 當前邊兩個端點在頂點表中的位置
vPosInV[0] = u.getVPosInV();
vPosInV[1] = v.getVPosInV();
ePosInI = new DLNode[2];// 當前邊在其兩個端點的關聯邊表中的位置
ePosInI[0] = u.outEdges.insertLast(this);// 當前邊加入u的鄰接(出)邊表
ePosInI[1] = v.inEdges.insertLast(this);// 當前邊加入v的鄰接(入)邊表
type = UNKNOWN;
}
// 返回當前邊的信息
public Object getInfo() {
return info;
}
// 將當前邊的信息更新為x,并返回原先的信息
public Object setInfo(Object x) {
Object e = info;
info = x;
return e;
}
// 取當前邊在所屬的圖的邊集E中的位置
public Position getEPosInE() {
return ePosInE;
}
// 取v[i]在頂點集V中的位置(i=0或1,分別對應于起點、終點)
public Position getVPosInV(int i) {
return vPosInV[i];
}
// 當前邊在其兩個端點的關聯邊集I(v[i])中的位置
public Position getEPosInI(int i) {
return ePosInI[i];
}
// 讀取、設置邊的類別(針對遍歷)
public int getType() {
return type;
}
public int setType(int t) {
int tt = type;
type = t;
return tt;
}
}
基于鄰接邊表實現圖結構
package dsa.Graph;
import dsa.Iterator.Iterator;
import dsa.List.List;
import dsa.List.List_DLNode;
import other.Position;
public class Graph_List implements Graph {
/*
* 基于鄰接邊表實現圖結構
*/
// 變量
protected List E;// 容器:存放圖中所有邊
protected List V;// 容器:存放圖中所有頂點
// 構造方法
public Graph_List() {
E = new List_DLNode();
V = new List_DLNode();
}
// 取圖的邊表、頂點表
protected List getE() {
return E;
}
protected List getV() {
return V;
}
// 取圖中頂點、邊的數目
public int vNumber() {
return V.getSize();
}
public int eNumber() {
return E.getSize();
}
// 取圖中所有頂點、頂點位置的迭代器
public Iterator vertices() {
return V.elements();
}
public Iterator vPositions() {
return V.positions();
}
// 返回圖中所有邊、邊位置的迭代器
public Iterator edges() {
return E.elements();
}
public Iterator ePositions() {
return E.positions();
}
// 檢測是否有某條邊從頂點u指向v
public boolean areAdjacent(Vertex u, Vertex v) {
return (null != edgeFromTo(u, v));
}
// 取從頂點u指向v的邊,若不存在,則返回null
public Edge edgeFromTo(Vertex u, Vertex v) {
for (Iterator it = u.outEdges(); it.hasNext();) {// 逐一檢查
Edge e = (Edge) it.getNext();// 以u為尾的每一條邊e
if (v == e.getVPosInV(1).getElem())// 若e是(u, v),則
return e;// 返回該邊
}
return null;// 若不存在這樣的(u, v),則返回null
}
// 將頂點v從頂點集中刪除,并返回之
public Vertex remove(Vertex v) {
while (0 < v.outDeg())// 將以v為尾的所有邊
remove((Edge) (((Vertex_List) v).outEdges.first()).getElem());// 逐一刪除
while (0 < v.inDeg())// 將以v為頭的所有邊
remove((Edge) (((Vertex_List) v).inEdges.first()).getElem());// 逐一刪除
return (Vertex) V.remove(v.getVPosInV());// 在頂點表中刪除v
}
// 將邊e從邊集中刪除,并返回之
public Edge remove(Edge e) {
((Vertex_List) e.getVPosInV(0).getElem()).outEdges.remove(e.getEPosInI(0));// 從起點的出邊表中刪除e
((Vertex_List) e.getVPosInV(1).getElem()).inEdges.remove(e.getEPosInI(1));// 從終點的入邊表中刪除e
return (Edge) E.remove(e.getEPosInE());// 從邊表中刪除e
}
// 在頂點集V中插入新頂點v,并返回其位置
public Position insert(Vertex v) {
return V.insertLast(v);
}
// 在邊集E中插入新邊e,并返回其位置
public Position insert(Edge e) {
return E.insertLast(e);
}
}
這里主要涉及三個算法,具體分析如下:
- 判斷任意一對頂點是否相鄰
算法:areAdjacent(u, v)
輸入:一對頂點u和v
輸出:判斷是否有某條邊從頂點u指向v
{
取頂點u的出邊迭代器it;
通過it逐一檢查u的每一條出邊e;
一旦e的終點為v,則報告true;
若e的所有出邊都已檢查過,則返回false;
}
- 刪除邊
算法:RemoveEdge(e)
輸入:邊e = (u, v)
輸出:將邊e從邊集E中刪除
{
從起點u的出邊鄰接表中刪除e;
從終點v的入邊鄰接表中刪除e;
從邊表E中刪除e;
}
- 刪除頂點
算法:removeVertex(v)
輸入:頂點v
輸出:將頂點v從頂點集V中刪除
{
掃描v的出邊鄰接表,(調用removeEdge()算法)將所有邊逐一刪除;
掃描v的入邊鄰接表,(調用removeEdge()算法)將所有邊逐一刪除;
在頂點表V中刪除v;
}