資料結構基礎(五)圖以及DFS、BFS
概念
定義
圖是一種較線性表和樹更為複雜的資料結構
相較於線性表的一對一(每個結點只有一個前驅後驅)和樹的一對多(層級結構,上層結點可以與多個下層結點相關),圖形結構的元素關係是多對多(即結點之間的關係可以是任意的)
圖可分為有向圖和無向圖
術語
連通圖:對於無向圖,如果任意兩個結點之間都是通的,則稱之為連通圖
連通分量:對於無向非連通圖,極大連通子圖稱為其連通分量
強連通圖:對於有向圖,任意兩個結點有路徑,則稱之為強連通圖
強連通分量:對於有向圖非連通圖,極大連通子圖稱為其強連通分量
連通圖的生成樹:該圖的極小連通子圖,它含有圖中全部n個頂點和只有足以構成一棵樹的n-1條邊,稱為圖的生成樹
例如上圖a中無向圖G3,圖b便為其三個連通分量
上圖為連通分量圖的生成樹
圖的儲存結構
圖的常用的儲存結構有:鄰接矩陣、鄰接連結串列、十字連結串列、鄰接多重表和邊表,其中鄰接矩陣和鄰接連結串列是較常用的表示方法。
鄰接矩陣
即用一維陣列放置其頂點,二維陣列放置頂點之間的關係
例如對於無向圖,A[i][j]=0表示i結點和j結點之間不連通,A[i][j]=1表示連通;對於有向圖,A[i][j]表示i結點和j結點之間的權值。
該二維陣列又稱為鄰接矩陣
鄰接連結串列
即用一個數組儲存所有頂點,而每個頂點有一個域指向一個單鏈表,該單鏈表儲存該頂點所有鄰接頂點及其相關資訊。
如上圖,頭結點(頂點)中data儲存該頂點相關資訊,firstarc儲存單鏈表第一個結點的位置;
表結點(連結串列結點)中adjvex儲存該領接頂點位置,nextarc儲存連結串列下一個結點位置,info儲存邊相關資訊(例如有向圖中的權值)
下圖為鄰接連結串列的圖解
實現程式碼
給出手動實現的鄰接矩陣程式碼:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 弧
*/
class Arc{
//弧連線的兩個頂點
Object v1,v2;
//圖:-1 無連線 >=0有連線
int Type;
//弧的資訊
String info;
public Arc() {}
public Arc(Object v1, Object v2, int type, String info) {
super();
this.v1 = v1;
this.v2 = v2;
Type = type;
this.info = info;
}
@Override
public String toString() {
return v1+" "+v2+" "+Type+" "+info;
}
}
/**
* 領接矩陣圖
*/
public class MatrixGraph {
/**預設頂點最大個數*/
final static int defaultSize=10;
/**頂點集合*/
List<Object> vertexs;
/**邊的關係矩陣*/
Arc[][] arcs;
/**最大頂點個數*/
int maxLen;
/**弧的個數*/
int arcNum;
/**
* 預設建構函式
*/
public MatrixGraph() {
this.maxLen = defaultSize;
this.vertexs = new ArrayList<Object>(maxLen);
this.arcs = new Arc[maxLen][maxLen];
this.arcNum = 0;
}
/**
* 帶參建構函式
* @param vers 頂點陣列
*/
public MatrixGraph(Object[] vers) {
this.maxLen=vers.length;
this.vertexs=new ArrayList<Object>(Arrays.asList(vers));
this.arcs = new Arc[maxLen][maxLen];
this.arcNum = 0;
}
/**
* 新增頂點
* @param v
*/
public void addVertex(Object v) {
vertexs.add(v);
if(vertexs.size()>maxLen)
extendSize();
}
/**
* 擴充套件最大頂點個數和領接矩陣
*/
public void extendSize() {
maxLen*=2;
arcs=Arrays.copyOf(arcs, maxLen);
}
/**
* 新增一條弧
* @param a
* @param b
*/
public void addArc(Object a,Object b) {
addArc(a, b, 0);
}
/**
* 新增一條帶權弧
* @param a
* @param b
* @param weight
*/
public void addArc(Object a,Object b,int weight) {
int i=vertexs.indexOf(a);
int j=vertexs.indexOf(b);
if(i==-1||j==-1){
throw new ArrayIndexOutOfBoundsException("該頂點不存在");
}
arcs[i][j]=new Arc(a, b, weight, "");
arcNum++;
}
/**
* 刪除a到b的弧
* @param a
* @param b
*/
public void removeArc(Object a,Object b) {
int i=vertexs.indexOf(a);
int j=vertexs.indexOf(b);
if(i==-1||j==-1){
throw new ArrayIndexOutOfBoundsException("該頂點不存在");
}
if(arcs[i][j]!=null){
arcs[i][j]=null;
arcNum--;
}
else {
System.out.println("該弧已經不存在");
}
}
/**
* 刪除一個頂點
* @param a
*/
public void removeVertexs(Object a) {
int i=vertexs.indexOf(a);
if(i==-1)
throw new ArrayIndexOutOfBoundsException("該頂點不存在");
vertexs.remove(i);
//重新生成鄰接矩陣
int length=arcs.length;
for (int j = 0; j < length-1; j++) {
for (int z = 0; z < length-1; z++) {
if(z>=i)
arcs[j][z]=arcs[j][z+1];
}
arcs[j][length-1]=null;
if(j>=i)
arcs[j]=arcs[j+1];
}
arcs[length-1]=new Arc[length];
}
/**
* 清空圖
*/
public void clear() {
vertexs=null;
arcs=null;
arcNum=0;
maxLen=defaultSize;
}
/**
* 列印圖的資訊
*/
public void printGraph() {
for (int i = 0; i < arcs.length; i++) {
for (int j = 0; j < arcs[i].length; j++) {
if(arcs[i][j]!=null)
System.out.println(arcs[i][j]);
}
}
}
public static void main(String[] args) {
Object obj[] = { 'A', 'B', 'C', 'D', 'E', 'F' };
MatrixGraph graph = new MatrixGraph(obj);
graph.addArc('A','C',5);
graph.addArc('B','A',2);
graph.addArc('C','B',15);
graph.addArc('E','D',4);
graph.addArc('F','E',18);
graph.addArc('A', 'F', 60);
graph.addArc('C', 'F', 70);
graph.printGraph();
System.out.println("--------------");
graph.removeVertexs('A');
graph.printGraph();
System.out.println("--------------");
graph.removeArc('C', 'B');
graph.printGraph();
}
}/**Output:
A C 5
A F 60
B A 2
C B 15
C F 70
E D 4
F E 18
--------------
C B 15
C F 70
E D 4
F E 18
--------------
C F 70
E D 4
F E 18 */
圖的遍歷
圖的遍歷有兩種:DFS(Deep First Search)和BFS(Breadth First Search)
這兩個演算法算是我在ACM呆的時候做題最多的演算法了。
DFS(深度優先演算法)
即每次遍歷時偏向縱深方向搜尋,類似樹的先序遍歷,可用遞迴或者棧實現。
遞迴實現:
1. 訪問頂點v;visited[v]=1;//演算法執行前visited[n]=0
2. w=頂點v的第一個鄰接點;
3. while(w存在)
if(w未被訪問)
從頂點w出發遞迴執行該演算法;
w=頂點v的下一個鄰接點;
棧實現:
1. 棧S初始化;visited[n]=0;
2. 訪問頂點v;visited[v]=1;頂點v入棧S
3. while(棧S非空)
x=棧S的頂元素(不出棧);
if(存在並找到未被訪問的x的鄰接點w)
訪問w;visited[w]=1;
w進棧;
else
x出棧;
BFS(廣度優先演算法)
即每次遍歷時分層搜尋,先搜尋完每個頂點的領接結點,再對領接結點重複這一步。可用佇列實現
佇列實現
1. 初始化佇列Q;visited[n]=0;
2. 訪問頂點v;visited[v]=1;頂點v入佇列Q;
3. while(佇列Q非空)
v=佇列Q的對頭元素出隊;
w=頂點v的第一個鄰接點;
while(w存在)
如果w未訪問,則訪問頂點w;
visited[w]=1;
頂點w入佇列Q;
w=頂點v的下一個鄰接點。
下面是兩種遍歷的具體程式碼
/**
* 深度優先遍歷
* @param o
* @return
*/
public String dfs(Object o) {
StringBuilder result=new StringBuilder();
Stack<Object> stack=new Stack<Object>();
//訪問標記陣列
Boolean[] visit=new Boolean[vertexs.size()];
//初始化所有頂點為未訪問
for (int i = 0; i < visit.length; i++) {
visit[i]=false;
}
//放入起始結點入棧
stack.push(o);
visit[vertexs.indexOf(o)]=true;
result.append(o);
//利用棧進行深度優先遍歷
while (!stack.isEmpty()) {
//訪問棧的頂點
Object out=stack.peek();
Object next=getNext(out, visit);
if(next!=null){
stack.push(next);
visit[vertexs.indexOf(next)]=true;
result.append("->"+next);
}
else{
stack.pop();
}
}
return result.toString();
}
/**
* 廣度優先遍歷
* @param o
* @return
*/
public String bfs(Object o) {
StringBuilder result = new StringBuilder();
Queue queue = new LinkedList<Object>();
// 訪問標記陣列
Boolean[] visit = new Boolean[vertexs.size()];
// 初始化所有頂點為未訪問
for (int i = 0; i < visit.length; i++) {
visit[i] = false;
}
//放入起始結點入佇列
queue.add(o);
visit[vertexs.indexOf(o)]=true;
result.append(o);
//利用佇列進行廣度優先遍歷
while (!queue.isEmpty()) {
//訪問堆頂點
Object out=queue.peek();
Object next=getNext(out, visit);
if(next!=null){
queue.add(next);
visit[vertexs.indexOf(next)]=true;
result.append("->"+next);
}else{
queue.poll();
}
}
return result.toString();
}
/**
* 獲取o結點的下一個未被訪問的領接結點
* @param o
* @param visit
* @return
*/
private Object getNext(Object o,Boolean[] visit){
int index=vertexs.indexOf(o);
for (int j = 0; j < arcs.length; j++) {
if(arcs[index][j]!=null&&visit[j]==false)
return vertexs.get(j);
}
return null;
}