1. 程式人生 > >經典演算法研究系列:四、教你通透徹底理解:BFS和DFS優先搜尋演算法

經典演算法研究系列:四、教你通透徹底理解:BFS和DFS優先搜尋演算法

4、教你通透徹底理解:BFS和DFS優先搜尋演算法

作者:July  二零一一年一月一日

---------------------------------

本人蔘考:演算法導論
本人宣告:個人原創,轉載請註明出處。

ok,開始。

翻遍網上,關於此類BFS和DFS演算法的文章,很多。但,都說不出個所以然來。
讀完此文,我想,
你對圖的廣度優先搜尋和深度優先搜尋定會有個通通透透,徹徹底底的認識。

---------------------

咱們由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));
}

===================

ok,本文完。

            July、二零一一年一月一日。Happy 2011 new year!

作者宣告:
本人July對本部落格所有任何內容和資料享有版權,轉載請註明作者本人July及出處。
永遠,向您的厚道致敬。謝謝。July、二零一零年十二月二日。