DFS與BFS詳解(很不錯的)
有需要關注微信公眾號:演算法那些事兒
---------------------
咱們由BFS開始:
首先,看下演算法導論一書關於 此BFS 廣度優先搜尋演算法的概述。
演算法導論第二版,中譯本,第324頁。
廣度優先搜尋(BFS)
在Prime最小生成樹演算法,和Dijkstra單源最短路徑演算法中,都採用了與BFS 演算法類似的思想。
//u 為 v 的先輩或父母。
BFS(G, s)
1 for each vertex u ∈ V [G] - {s} 2 do color[u] ← WHITE 3 d[u] ← ∞ 4 π[u] ← NIL //除了源頂點s之外,第1-4行置每個頂點為白色,置每個頂點u的d[u]為無窮大, //置每個頂點的父母為NIL。 5 color[s] ← GRAY //第5行,將源頂點s置為灰色,這是因為在過程開始時,源頂點已被發現。 6 d[s] ← 0 //將d[s]初始化為0。 7 π[s] ← NIL //將源頂點的父頂點置為NIL。 8 Q ← Ø 9 ENQUEUE(Q, s) //入隊 //第8、9行,初始化佇列Q,使其僅含源頂點s。 10 while Q ≠ Ø 11 do u ← DEQUEUE(Q) //出隊 //第11行,確定佇列頭部Q頭部的灰色頂點u,並將其從Q中去掉。 12 for each v ∈ Adj[u] //for迴圈考察u的鄰接表中的每個頂點v 13 do if color[v] = WHITE 14 then color[v] ← GRAY //置為灰色 15 d[v] ← d[u] + 1 //距離被置為d[u]+1 16 π[v] ← u //u記為該頂點的父母 17 ENQUEUE(Q, v) //插入佇列中 18 color[u] ← BLACK //u 置為黑色
由下圖及連結的演示過程,清晰在目,也就不用多說了:
廣度優先遍歷演示地址:
-----------------------------------------------------------------------------------------------------------------
ok,不再贅述。接下來,具體講解深度優先搜尋演算法。
深度優先探索演算法 DFS
//u 為 v 的先輩或父母。
DFS(G)
1 for each vertex u ∈ V [G] 2 do color[u] ← WHITE 3 π[u] ← NIL //第1-3行,把所有頂點置為白色,所有π 域被初始化為NIL。 4 time ← 0 //復位時間計數器 5 for each vertex u ∈ V [G] 6 do if color[u] = WHITE 7 then DFS-VISIT(u) //呼叫DFS-VISIT訪問u,u成為深度優先森林中一棵新的樹 //第5-7行,依次檢索V中的頂點,發現白色頂點時,呼叫DFS-VISIT訪問該頂點。 //每個頂點u 都對應於一個發現時刻d[u]和一個完成時刻f[u]。 DFS-VISIT(u) 1 color[u] ← GRAY //u 開始時被發現,置為白色 2 time ← time +1 //time 遞增 3 d[u] <-time //記錄u被發現的時間 4 for each v ∈ Adj[u] //檢查並訪問 u 的每一個鄰接點 v 5 do if color[v] = WHITE //如果v 為白色,則遞迴訪問v。 6 then π[v] ← u //置u為 v的先輩 7 DFS-VISIT(v) //遞迴深度,訪問鄰結點v 8 color[u] <-BLACK //u 置為黑色,表示u及其鄰接點都已訪問完成 9 f [u] ▹ time ← time +1 //訪問完成時間記錄在f[u]中。 //完
第1-3行,5-7行迴圈佔用時間為O(V),此不包括呼叫DFS-VISIT的時間。
對於每個頂點v(-V,過程DFS-VISIT僅被呼叫依次,因為只有對白色頂點才會呼叫此過程。
第4-7行,執行時間為O(E)。
因此,總的執行時間為O(V+E)。
下面的連結,給出了深度優先搜尋的演示系統:
圖的深度優先遍歷演示系統:
===============
最後,咱們再來看深度優先搜尋的遞迴實現與非遞迴實現
1、DFS 遞迴實現:
void dftR(PGraphMatrix inGraph) { PVexType v; assertF(inGraph!=NULL,"in dftR, pass in inGraph is null/n"); printf("/n===start of dft recursive version===/n"); for(v=firstVertex(inGraph);v!=NULL;v=nextVertex(inGraph,v)) if(v->marked==0) dfsR(inGraph,v); printf("/n===end of dft recursive version===/n"); } void dfsR(PGraphMatrix inGraph,PVexType inV) { PVexType v1; assertF(inGraph!=NULL,"in dfsR,inGraph is null/n"); assertF(inV!=NULL,"in dfsR,inV is null/n"); inV->marked=1; visit(inV); for(v1=firstAdjacent(inGraph,inV);v1!=NULL;v1=nextAdjacent(inGraph,inV,v1)) //v1當為v的鄰接點。 if(v1->marked==0) dfsR(inGraph,v1); }
2、DFS 非遞迴實現
非遞迴版本---藉助結點型別為佇列的棧實現
聯絡樹的前序遍歷的非遞迴實現:
可知,其中無非是分成“探左”和“訪右”兩大塊訪右需藉助棧中彈出的結點進行.
在圖的深度優先搜尋中,同樣可分成“深度探索”和“回訪上層未訪結點”兩塊:
1、圖的深度探索這樣一個過程和樹的“探左”完全一致,
只要對已訪問過的結點作一個判定即可。
2、而圖的回訪上層未訪結點和樹的前序遍歷中的“訪右”也是一致的.
但是,對於樹而言,是提供rightSibling這樣的操作的,因而訪右相當好實現。
在這裡,若要實現相應的功能,考慮將每一個當前結點的下層結點中,如果有m個未訪問結點,
則最左的一個需要訪問,而將剩餘的m-1個結點按從左到右的順序推入一個佇列中。
並將這個佇列壓入一個堆疊中。
這樣,噹噹前的結點的鄰接點均已訪問或無鄰接點需要回訪時,
則從棧頂的佇列結點中彈出佇列元素,將佇列中的結點元素依次出隊,
若已訪問,則繼續出隊(噹噹前佇列結點已空時,則繼續出棧,彈出下一個棧頂的佇列),
直至遇到有未訪問結點(訪問並置當前點為該點)或直到棧為空(則當前的深度優先搜尋樹停止搜尋)。
將演算法通過精簡過的C源程式的方式描述如下:
//dfsUR:功能從一個樹的某個結點inV發,以深度優先的原則訪問所有與它相鄰的結點
void dfsUR(PGraphMatrix inGraph,PVexType inV)
{
PSingleRearSeqQueue tmpQ; //定義臨時佇列,用以接受棧頂佇列及壓棧時使用
PSeqStack testStack; //存放當前層中的m-1個未訪問結點構成佇列的堆疊.
//一些變數宣告,初始化動作
//訪問當前結點
inV->marked=1; //當marked值為1時將不必再訪問。
visit(inV);
do
{
flag2=0;
//flag2是一個重要的標誌變數,用以、說明當前結點的所有未訪問結點的個數,兩個以上的用2代表
//flag2:0:current node has no adjacent which has not been visited.
//1:current node has only one adjacent node which has not been visited.
//2:current node has more than one adjacent node which have not been visited.
v1=firstAdjacent(inGraph,inV); //鄰接點v1
while(v1!=NULL) //訪問當前結點的所有鄰接點
{
if(v1->marked==0) //..
{
if(flag2==0) //當前結點的鄰接點有0個未訪問
{
//首先,訪問最左結點
visit(v1);
v1->marked=1; //訪問完成
flag2=1; //
//記錄最左兒子
lChildV=v1;
//save the current node's first unvisited(has been visited at this time)adjacent node
}
else if(flag2==1) //當前結點的鄰接點有1個未訪問
{
//新建一個佇列,申請空間,並加入第一個結點
flag2=2;
}
else if(flag2==2)//當前結點的鄰接點有2個未被訪問
{
enQueue(tmpQ,v1);
}
}
v1=nextAdjacent(inGraph,inV,v1);
}
if(flag2==2)//push adjacent nodes which are not visited.
{
//將存有當前結點的m-1個未訪問鄰接點的佇列壓棧
seqPush(testStack,tmpQ);
inV=lChildV;
}
else if(flag2==1)//only has one adjacent which has been visited.
{
//只有一個最左兒子,則置當前點為最左兒子
inV=lChildV;
}
else if(flag2==0)
//has no adjacent nodes or all adjacent nodes has been visited
{
//噹噹前的結點的鄰接點均已訪問或無鄰接點需要回訪時,則從棧頂的佇列結點中彈出佇列元素,
//將佇列中的結點元素依次出隊,若已訪問,則繼續出隊(噹噹前佇列結點已空時,
//則繼續出棧,彈出下一個棧頂的佇列),直至遇到有未訪問結點(訪問並置當前點為該點)或直到棧為空。
flag=0;
while(!isNullSeqStack(testStack)&&!flag)
{
v1=frontQueueInSt(testStack); //返回棧頂結點的佇列中的隊首元素
deQueueInSt(testStack); //將棧頂結點的佇列中的隊首元素彈出
if(v1->marked==0)
{
visit(v1);
v1->marked=1;
inV=v1;
flag=1;
}
}
}
}while(!isNullSeqStack(testStack));//the algorithm ends when the stack is null
}
-----------------------------
上述程式的幾點說明:
所以,這裡應使用的資料結構的構成方式應該採用下面這種形式:
1)佇列的實現中,每個佇列結點均為圖中的結點指標型別.
定義一個以佇列尾部下標加佇列長度的環形佇列如下:
struct SingleRearSeqQueue;
typedef PVexType QElemType;
typedef struct SingleRearSeqQueue* PSingleRearSeqQueue;
struct SingleRearSeqQueue
{
int rear;
int quelen;
QElemType dataPool[MAXNUM];
};
其餘基本操作不再贅述.
2)堆疊的實現中,每個堆疊中的結點元素均為一個指向佇列的指標,定義如下:
#define SEQ_STACK_LEN 1000
#define StackElemType PSingleRearSeqQueue
struct SeqStack;
typedef struct SeqStack* PSeqStack;
struct SeqStack
{
StackElemType dataArea[SEQ_STACK_LEN];
int slot;
};
為了提供更好的封裝性,對這個堆疊實現兩種特殊的操作
2.1) deQueueInSt操作用於將棧頂結點的佇列中的隊首元素彈出.
void deQueueInSt(PSeqStack inStack)
{
if(isEmptyQueue(seqTop(inStack))||isNullSeqStack(inStack))
{
printf("in deQueueInSt,under flow!/n");
return;
}
deQueue(seqTop(inStack));
if(isEmptyQueue(seqTop(inStack)))
inStack->slot--;
}
2.2) frontQueueInSt操作用以返回棧頂結點的佇列中的隊首元素.
QElemType frontQueueInSt(PSeqStack inStack)
{
if(isEmptyQueue(seqTop(inStack))||isNullSeqStack(inStack))
{
printf("in frontQueueInSt,under flow!/n");
return '/r';
}
return getHeadData(seqTop(inStack));
}