圖說演算法使用JavaScript(三十四)

10-3  圖形走訪的訣竅

樹的追蹤目的是欲拜訪樹的每一個節點一次,可用的方法有中序法、前序法和後序法等三種。而圖形追蹤的定義如下:

一個圖形G=(V,E),存在某一頂點vEV,我們希望從v開始,經由此節點相鄰的節點兒去拜訪G中其他節點,這稱之為(圖形追蹤)。也就是從某一個頂點V1開始,走訪可以經由V1到達的頂點,接著在走訪下一個頂點直到全部的頂點走訪完畢為止。

在走訪的過程中可能會重複經過某些頂點及邊線。經由圖形的走訪可以判斷該圖形是否連通,並找出連通單元及路徑。圖形走訪的方法有兩種:(先深後廣走訪)及(先廣後深走訪)。

10-3-1  先深後廣走訪法

先身後廣走訪的方式有點類似前序走訪。適從圖形的某一頂點開始走訪,被拜訪過的頂點就作上已拜訪記號,接著走訪此頂點的所有相鄰且未拜訪過的頂點中的任一個頂點,並作上已拜訪的記號,再以該點為新的起點繼續進行先深厚廣的搜尋。
這種圖形追蹤方法結合了遞迴及堆疊兩種資料結構的技巧,由於此方法會造成無窮迴路,所以必須加入一個變數,判斷該點是否已經走訪完畢。我們以下徒來看看這個方法的走訪過程。

故先深後廣的走訪順序為:頂點1、頂點2、頂點3、頂點4、頂點5。

深度優先函數的演算法如下:

var DFs=(current)=> {
       run[current] = 1;
       process.stdout.write(“[“+current+”]”);
       while (Head[current].first) != null) {
                  if (run[Head[current].first.x] == 0)
                              Dfs(Head[current].first.x);
                 Head[current].first = Head[current].first.next;
       }
}

JS                     dfs.js

class Node {
	constructor(x) {
		this.x = x;
		this.next = null;
	}
}

class GraphLink {
	constructor() {
		this.first = null;
		this.last = null;
	}

	IsEmpty() {
		return this.first == null;
	}

	Print() {
		let current = this.first;
		while (current != null) {
			process.stdout.write('['+current.x+'] ');
			current = current.next;
		}
			process.stdout.write('\n');
	}

	Insert(x) {
		let newNode = new Node(x);
		if (this.IsEmpty()) {
			this.first = newNode;
			this.last = newNode;
		}
		else {
			this.last.next = newNode;
			this.last = newNode;
		}
	}
}

run=[];
Head=[];
for (i=0; i<9; i++)
	Head[i] = new GraphLink();

var DFs=(current)=> {
	run[current] = 1;
	process.stdout.write("["+current+"]");
	while ((Head[current].first) != null) {
		if (run[Head[current].first.x] == 0) 
			DFs(Head[current].first.x);

		Head[current].first = Head[current].first.next;
	}
}

Data=[];

Data=[[1,2],[2,1],[1,3],[3,1],[2,4],
		[4,2],[2,5],[5,2],[3,6],[6,3],
		[3,7],[7,3],[4,5],[5,4],[6,7],
		[7,6],[5,8],[8,5],[6,8],[8,6]];

run=[];
Head=[];

for (i=0; i<9; i++)
	Head[i]=new GraphLink();

process.stdout.write("圖形的相鄰串接內容:\n");

for (let i=1; i<9; i++) {
	run[i]=0;
	process.stdout.write('頂點'+ i +'=>');
	Head[i] = new GraphLink();
	for (j=0; j<20; j++) {
		if (Data[j][0]==i) {
			DataNum = Data[j][1];
			Head[i].Insert(DataNum);
		}
	}
	Head[i].Print();
}
process.stdout.write("深度優先走訪頂點:\n");
DFs(1);
process.stdout.write('\n');

10-3-2  先廣後深搜尋法

之前所談到先深候廣是利用堆疊及遞迴的技巧來走訪圖形,而先廣後深(Breadth-First Search, BFS)走訪方式則是以佇列及遞迴來走訪,也是從圖形的某一頂點開始走訪,被拜訪過的頂點就做以拜訪的記號。接著走訪此頂點的所有相鄰且為拜訪過的頂點中任意一個頂點,並做上已拜訪的記號,再以該點為新的起點繼續進行先廣後深的搜尋。我們以下圖來看看BFS的走訪過程:

所以,先廣後深的走訪順序為:頂點1、頂點2、頂點5、頂點3、頂點4。

先廣後深函數的演算法如下:

var bfs=(current)=> {
       enqueue(current);
       run[current]=1;
       process.stdout.write(‘[‘+current+’]’);
       while (front!=rear) {
                  current=dequeue();
                  tempnode=Head[current].first;
                  while (tempnode!=null) {
                             if (run[tempnode.x]==0) {
                                  enqueue(tempnode.x);
                                  run[tempnode.x]=1;
                                  process.stdout.write(‘[‘+tempnode.x+’]’);
                            }
                 tempnode=tempnode.next;
                }
      }
}

書本範例程式有問題            bfs.js

圖說演算法使用JavaScript(三十三)

10-2-2相鄰串列法

前面所介紹相鄰矩陣法,優點是藉著矩陣的運算,可以求取許多特別的應用,如要在圖形中加入新邊時,這個表示法的插入與刪除相當簡易。不過可慮到稀疏矩陣空間浪費的問題,如要計算所有頂點的分支度時,其時間複雜度為O(n^2)。

因此可以考慮更有效率的方法,就是相鄰串列法(adjaceny list)。這種表示法就是將一個n列相鄰矩陣,表示成n個鏈結串列,這種做法和相鄰矩陣相比較節省空間,如計算所有頂點的分支度時,其時間複雜度為O(n+e),缺點是圖形新邊的加入或刪除會更動到相關的串列鏈結,較為麻煩費時。

首先將圖形的n個頂點形成n個串列首,每個串列中的節點表示它們和首節點之間有邊相連。節點宣告如下:

class  list_node  {
           constructor ()  {
           this.val=0;
           this.next=null;
          }
}

在無向圖形中,因為對稱的關係,若有n個頂點、m個邊,則形成n個串列首,2m個節點。若為有向圖形中,則有n個串列首,以及m個頂點,因此相鄰串列中,求所有頂點分枝度所需的時間複雜度為O(n+m)。現在分別來討論下圖中兩個範例,該如何使用相鄰串列表示:

首先來看(A)圖,因為5個頂點使用5個串列首,V1串列代表頂點1,與頂點1相鄰的頂點有2及5,依此類推。

接著來看(B)圖,因為4個頂點使用4個串列首,V1串列代表頂點1,與頂點1相鄰的頂點有2,依此類推。

JS                    adjaceeency_matrix.js

class list_node {
	construnctor() {
		this.val=0;
		this.next=null;
	}
}

head=[];
for (i=0; i<6; i++) {
	head[i]=new list_node();
}

var newnode=new list_node();

//圖形宣告
data=[[1,2],[2,1],[2,5],[5,2],
		[2,3],[3,2],[2,4],[4,2],
		[3,4],[4,3],[3,5],[5,3],
		[4,5],[5,4]];

process.stdout.write('圖形的鄰接串列內容:\n');
process.stdout.write('-------------------------------------------\n');
for (i=1; i<6; i++)  {
	head[i].val=i;
	head[i].next=null;
	process.stdout.write('頂點 '+i+'=>');
	ptr=head[i];
	for (j=0; j<14; j++)  {
		if (data[j][0]==i) {
			newnode.val=data[j][1];
			newnode.next=null;
			while (ptr!=null) ptr=ptr.next;
			ptr=newnode;
			process.stdout.write('['+newnode.val+'] ');
		}
	}
	process.stdout.write('\n');
}

PHP                adjacency_matrix.php

PHP的串列,不會寫。
搞不清楚 javascript    newnode.val  跟  newnode.next如何改寫成PHP。

10-2-3 相鄰複合串列法

上面介紹了兩個圖形表示法都是從頂點的觀點出發,但如果要處理的是(邊)則必須使用相鄰多元串列,相鄰多元串列是處理無向圖形的另一種方法。相鄰多元串列的節點是存放邊線的資料,其結構如下:

其中相關特性說明如下:

M:是紀錄該邊是否被找過的一個位元之欄位。
V1及V2:是所記錄的邊的起點與終點。
LINK1:在尚有其他頂點與V1相連的情況下,此欄位會指向下一個與V1相連的邊結點,如果已經沒有任何頂點與V1相連時,則指向null。
LINK2:在尚有其他頂點與V2相連的情況下,此欄位會指向下一個與V2相連的邊節點,如果已經沒有任何頂點與V2相連時,則指向null。

例如有三條邊線(1,2)(1,3)(2,4),則邊線(1,2)表示法如下:

我們現在已相鄰多元串列表示下圖所示:

首先分別把頂點及邊的節點找出。

10-2-4 索引表格法

索引表格表示法,是一種用一維陣列來依序儲存與各頂點相鄰的所有頂點,並建立索引表格,來記錄個頂點在此一維陣列中第一個與該頂點相鄰的位置。我們將以下圖來說明索引表格法的實例。

則索引表格法的表示外觀為:

圖說演算法使用JavaScript(三十二)

10-1-4有向圖形

有向圖形(Digrph)是一種每一個邊都可使用有序對<V1,V2>來表示,並且<V1,V2>與<V2,V1>是表示方向不同的邊,而所謂<V1,V2>,是指V1為尾端指向為頭部的V2。如下圖所示:

V={A,B,C,D,E}
E={<A,B>、<B,C>、<C,D>、<C,E>、<E,D>、<D,B>}

接下來則是有向圖形的相關定義介紹:

*完整圖形(Complete Graph):具有n個頂點且恰有n*(n-1)個邊的有向圖形,如下圖所示:

*路徑(Path):有向圖形中從頂點Vp到頂點Vq的路徑是指一串由頂點所組成的連續有像序列。

*強連接(Strongly Connected):有向圖形中,如果每個相異的成對頂點Vi,Vj有直接路徑,同時,有另一條路徑從Vj到Vi,則稱此圖為強連接。如下圖:

*強連接單元(Strong Connected Component):有向圖形中構成強連接的最大子圖,在下圖(A)中是強連接,但(B)就不是。

而圖(B)中的強連接單元如下:

*出分支度(Out-degree):是指有向圖形中,以頂點V為劍尾的邊數目。

*入分支度(In-degree):是指有向圖形中,以頂點V為箭頭的邊數目,下圖中V4的入分支度為1,出分支度為0,V2的入分支度為4,出分支度為1。

10-2不能不學的圖形表示法

知道圖形的各種定義與觀念後,有關圖形的資料表示法就易顯重要了。常用來表達圖形資料結構的方法很多,本節中將介紹四種表示法。

10-2-1相鄰矩陣法

圖形A有n個頂點,以n*n的二為矩陣陣列表示。此矩陣的定義如下:

相關特性說明如下:

接著就實際來看一個範例,請以相鄰矩陣表示右邊的無向圖:由於下圖共有5個頂點,故使用5*5的二維陣列存放圖形。下圖中,先找和1相鄰的頂點有哪些,把和1相鄰的頂點座標填入1。

跟頂點1相鄰的有頂點2及頂點5,所以完成下表:

其他頂點依此類推可以得到相鄰矩陣:

無向/有向圖形的6*6相鄰矩陣演算法如下:

for (i=0; i<10; i++) {
for (j=0; j<2; j++) {
for (k=0; k<6; k++) {
tmpi=data[i][0];
tmpj=data[i][1];
arr[tmpi][tmpj]=1;
}
}
}
process.stdout.write('無向圖形矩陣:\n');
for (i=1; i<6; i++) {
for (j=1; j<6; j++)
process.stdout.write('['+arr[i][j]+'] ');
process.stdout.write('/n');
}

無向圖形

JS                 undirected.js

arr=[[0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0]];

data=[[1,2],[2,1],[1,5],[5,1],
	  [2,3],[3,2],[2,4],[4,2],
	  [3,4],[4,3]];

for (i=0; i<10; i++) {
	for (j=0; j<2; j++) {
		for (k=0; k<6; k++) {
			tmpi=data[i][0];
			tmpj=data[i][1];
			arr[tmpi][tmpj]=1;
		}
	}
}
process.stdout.write('無向圖形矩陣:\n');
for (i=1; i<6; i++) {
	for (j=1; j<6; j++) 
		process.stdout.write('['+arr[i][j]+'] ');
	process.stdout.write('\n');
}

PHP                    undirected.php

$arr = array(
    array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0)
);

//圖形各邊的起點值及終點值
$data = array(
	array(1,2),array(2,1),array(1,5),
	array(5,1),array(2,3),array(3,2),
	array(2,4),array(4,2),array(3,4),
	array(4,3)
);

for ($i=0; $i<10; $i++) {
	for ($j=0; $j<2; $j++) {
		for ($k=0; $k<6; $k++) {
			$tmpi=$data[$i][0];
			$tmpj=$data[$i][1];
			$arr[$tmpi][$tmpj]=1;
		}
	}
}

for ($i=1; $i<6; $i++) {
	for ($j=1; $j<6; $j++) {
		echo "[".$arr[$i][$j]."]";
	}
	echo "<br>";
}

有向圖形

JS                directed.js

arr=[[0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0],
	 [0,0,0,0,0,0]];

data=[[1,2],[2,1],[2,3],[2,4],
	  [4,3],[4,1],[2,4]];

for (i=0; i<6; i++) {
	for (j=0; j<6; j++) {
			tmpi=data[i][0];
			tmpj=data[i][1];
			arr[tmpi][tmpj]=1;
	}
}
process.stdout.write('有向圖形矩陣:\n');
for (i=1; i<6; i++) {
	for (j=1; j<6; j++) 
		process.stdout.write('['+arr[i][j]+'] ');
	process.stdout.write('\n');
}

PHP                directed.php

$arr = array(
    array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0),
	array(0,0,0,0,0,0)
);

//圖形各邊的起點值及終點值
$data = array(
	array(1,2),array(2,1),array(2,3),
	array(2,4),array(4,3),array(4,1)
	);

$count=count($data);
//echo $count."<br>";

for ($i=0; $i<$count; $i++) {      //讀取圖形資料
	for ($j=0; $j<$count; $j++) {       //填入arr矩陣
			$tmpi=$data[$i][0];    //tmpi為起始頂點
			$tmpj=$data[$i][1];    //tmpj為終止頂點
			$arr[$tmpi][$tmpj]=1;  //有邊的點填入1
	}
}

for ($i=1; $i<6; $i++) {
	for ($j=1; $j<6; $j++) {
		echo "[".$arr[$i][$j]."]"; //列印矩陣內容
	}
	echo "<br>";
}