廣度優先搜索算法

一、簡介

廣度優先搜索算法(Breadth-First Search,BFS)是一種盲目搜尋法,目的是系統地展開並檢查圖中的所有節點,以找尋結果。換句話說,它並不考慮結果的可能位置,徹底地搜索整張圖,直到找到結果爲止。BFS並不使用經驗法則算法。

廣度優先搜索讓你能夠找出兩樣東西之間的最短距離,不過最短距離的含義有很多!使用廣度優先搜索可以:

  1. 編寫國際跳棋AI,計算最少走多少步就可獲勝;
  2. 編寫拼寫檢查器,計算最少編輯多少個地方就可將錯拼的單詞改成正確的單詞,如將READED改爲READER需要編輯一個地方;
  3. 根據你的人際關係網絡找到關係最近的醫生。

二、例子

假設你居住在舊金山,要從雙子峯前往金門大橋。你想乘公交車前往,並希望換乘最少。可乘坐的公交車如下。

爲找出換乘最少的乘車路線,你將使用什麼樣的算法?
一步就能到達金門大橋嗎?下面突出了所有一步就能到達的地方。

金門大橋未突出,因此一步無法到達那裏。兩步能嗎?

金門大橋也未突出,因此兩步也到不了。三步呢?

金門大橋突出了!因此從雙子峯出發,可沿下面的路線三步到達金門大橋。

 還有其他前往金門大橋的路線,但它們更遠(需要四步)。這個算法發現,前往金門大橋的最短路徑需要三步。這種問題被稱爲最短路徑問題(shorterst-path problem)。你經常要找出最短路徑,這可能是前往朋友家的最短路徑,也可能是國際象棋中把對方將死的最少步數。解決最短路徑問題的算法被稱爲廣度優先搜索。要確定如何從雙子峯前往金門大橋,需要兩個步驟。
(1) 使用圖來建立問題模型。
(2) 使用廣度優先搜索解決問題。
下面介紹什麼是圖,然後再詳細探討廣度優先搜索。

三、圖

圖是由頂點的有窮非空集合和頂點之間邊的集合組成,通過表示爲G(V,E),其中,G標示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。

無邊圖:若頂點Vi到Vj之間的邊沒有方向,則稱這條邊爲無項邊(Edge),用序偶對(Vi,Vj)標示。

對於下圖無向圖G1來說,G1=(V1, {E1}),其中頂點集合V1={A,B,C,D};邊集合E1={(A,B),(B,C),(C,D),(D,A),(A,C)}:

有向圖:若從頂點Vi到Vj的邊是有方向的,則成這條邊爲有向邊,也稱爲弧(Arc)。用有序對(Vi,Vj)標示,Vi稱爲弧尾,Vj稱爲弧頭。如果任意兩條邊之間都是有向的,則稱該圖爲有向圖。

有向圖G2中,G2=(V2,{E2}),頂點集合(A,B,C,D),弧集合E2={<A,D>,{B,A},<C,A>,<B,C>}.

權:有些圖的邊和弧有相關的數,這個數叫做權。這些帶權的圖通常稱爲網。

四、廣度優先搜索算法

假設你經營着一個芒果農場,需要尋找芒果銷售商,以便將芒果賣給他。在Facebook,你與芒果銷售商有聯繫嗎?爲此,你可在朋友中查找。

這種查找很簡單。首先,創建一個朋友名單。

 然後,依次檢查名單中的每個人,看看他是否是芒果銷售商。

 假設你沒有朋友是芒果銷售商,那麼你就必須在朋友的朋友中查找。

 檢查名單中的每個人時,你都將其朋友加入名單。

 這樣一來,你不僅在朋友中查找,還在朋友的朋友中查找。別忘了,你的目標是在你的人際關係網中找到一位芒果銷售商。因此,如果Alice不是芒果銷售商,就將其朋友也加入到名單中。這意味着你將在她的朋友、朋友的朋友等中查找。使用這種算法將搜遍你的整個人際關係網,直到找到芒果銷售商。這就是廣度優先搜索算法。

五、查找最短路徑

再說一次,廣度優先搜索可回答兩類問題。
第一類問題:從節點A出發,有前往節點B的路徑嗎?(在你的人際關係網中,有芒果銷售商嗎?)
第二類問題:從節點A出發,前往節點B的哪條路徑最短?(哪個芒果銷售商與你的關係最近?)
剛纔你看到了如何回答第一類問題,下面來嘗試回答第二類問題——誰是關係最近的芒果銷售商。例如,朋友是一度關係,朋友的朋友是二度關係。

 在你看來,一度關係勝過二度關係,二度關係勝過三度關係,以此類推。因此,你應先在一度關係中搜索,確定其中沒有芒果銷售商後,纔在二度關係中搜索。廣度優先搜索就是這樣做的!在廣度優先搜索的執行過程中,搜索範圍從起點開始逐漸向外延伸,即先檢查一度關係,再檢查二度關係。順便問一句:將先檢查Claire還是Anuj呢?Claire是一度關係,而Anuj是二度關係,因
此將先檢查Claire,後檢查Anuj。

你也可以這樣看,一度關係在二度關係之前加入查找名單。

你按順序依次檢查名單中的每個人,看看他是否是芒果銷售商。這將先在一度關係中查找,再在二度關係中查找,因此找到的是關係最近的芒果銷售商。廣度優先搜索不僅查找從A到B的路徑,而且找到的是最短的路徑。

 注意,只有按添加順序查找時,才能實現這樣的目的。換句話說,如果Claire先於Anuj加入名單,就需要先檢查Claire,再檢查Anuj。如果Claire和Anuj都是芒果銷售商,而你先檢查Anuj再檢查Claire,結果將如何呢?找到的芒果銷售商並非是與你關係最近的,因爲Anuj是你朋友的朋友,而Claire是你的朋友。因此,你需要按添加順序進行檢查。有一個可實現這種目的的數據
結構,那就是隊列(queue)。

六、隊列

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。隊列中沒有元素時,稱爲空隊列。

隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。因爲隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱爲先進先出(FIFO—first in first out)線性表。

順序隊列

建立順序隊列結構必須爲其靜態分配或動態申請一片連續的存儲空間,並設置兩個指針進行管理。一個是隊頭指針front,它指向隊頭元素;另一個是隊尾指針rear,它指向下一個入隊元素的存儲位置,如圖所示

每次在隊尾插入一個元素是,rear增1;每次在隊頭刪除一個元素時,front增1。隨着插入和刪除操作的進行,隊列元素的個數不斷變化,隊列所佔的存儲空間也在爲隊列結構所分配的連續空間中移動。當front=rear時,隊列中沒有任何元素,稱爲空隊列。當rear增加到指向分配的連續空間之外時,隊列無法再插入新元素,但這時往往還有大量可用空間未被佔用,這些空間是已經出隊的隊列元素曾經佔用過得存儲單元。

順序隊列中的溢出現象:

(1) "下溢"現象:當隊列爲空時,做出隊運算產生的溢出現象。“下溢”是正常現象,常用作程序控制轉移的條件。

(2)"真上溢"現象:當隊列滿時,做進棧運算產生空間溢出的現象。“真上溢”是一種出錯狀態,應設法避免。

(3)"假上溢"現象:由於入隊和出隊操作中,頭尾指針只增加不減小,致使被刪元素的空間永遠無法重新利用。當隊列中實際的元素個數遠遠小於向量空間的規模時,也可能由於尾指針已超越向量空間的上界而不能做入隊操作。該現象稱爲"假上溢"現象。

循環隊列

在實際使用隊列時,爲了使隊列空間能重複使用,往往對隊列的使用方法稍加改進:無論插入或刪除,一旦rear指針增1或front指針增1 時超出了所分配的隊列空間,就讓它指向這片連續空間的起始位置。自己真從MaxSize-1增1變到0,可用取餘運算rear%MaxSize和front%MaxSize來實現。這實際上是把隊列空間想象成一個環形空間,環形空間中的存儲單元循環使用,用這種方法管理的隊列也就稱爲循環隊列。除了一些簡單應用之外,真正實用的隊列是循環隊列。 [2] 

在循環隊列中,當隊列爲空時,有front=rear,而當所有隊列空間全佔滿時,也有front=rear。爲了區別這兩種情況,規定循環隊列最多只能有MaxSize-1個隊列元素,當循環隊列中只剩下一個空存儲單元時,隊列就已經滿了。因此,隊列判空的條件時front=rear,而隊列判滿的條件時front=(rear+1)%MaxSize。隊空和隊滿的情況如圖:

七、廣度優先搜索算法實現

我們要從“你”出發找到“ANUJ”,關係表示爲下圖,使用廣度優先搜索算法

 先概述一下這種算法的工作原理。

但這樣可能會出現一些問題,Peggy既是Alice的朋友又是Bob的朋友,因此她將被加入隊列兩次:一次是在添加Alice的朋友時,另一次是在添加Bob的朋友時。因此,搜索隊列將包含兩個Peggy。

但你只需檢查Peggy一次,看她是不是芒果銷售商。如果你檢查兩次,就做了無用功。因此,檢查完一個人後,應將其標記爲已檢查,且不再檢查他。
如果不這樣做,就可能會導致無限循環。假設你的人際關係網類似於下面這樣。

一開始,搜索隊列包含你的所有鄰居。

現在你檢查Peggy。她不是芒果銷售商,因此你將其所有鄰居都加入搜索隊列。

接下來,你檢查自己。你不是芒果銷售商,因此你將你的所有鄰居都加入搜索隊列。

以此類推。這將形成無限循環,因爲搜索隊列將在包含你和包含Peggy之間反覆切換。

檢查一個人之前,要確認之前沒檢查過他,這很重要。爲此,你可使用一個列表來記錄檢查過的人。

首先,需要使用代碼來實現圖。圖由多個節點組成。
每個節點都與鄰近節點相連,如果表示類似於“你→Bob”這樣的關係呢?好在你知道的一種結構讓你能夠表示這種關係,它就是散列表!
記住,散列表讓你能夠將鍵映射到值。在這裏,你要將節點映射到其所有鄰居。

圖不過是一系列的節點和邊,因此在JAVA中,你可以使用HashMap來表示一個圖。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

public class BFS {


    public static void main(String[] args) {
        HashMap<String,String[]> hashMap=new HashMap<>();
        hashMap.put("YOU",new String[]{"CLAIRE","ALICE","BOB"});
        hashMap.put("CLAIRE",new String[]{"YOU","JONNY","THON"});
        hashMap.put("JONNY",new String[]{"CLAIRE"});
        hashMap.put("THOH",new String[]{"CLAIRE"});
        hashMap.put("ALICE",new String[]{"YOU","PEGGY"});
        hashMap.put("BOB",new String[]{"YOU","PEGGY","ANUJ"});
        hashMap.put("PEGGY",new String[]{"BOB","ALICE"});
        hashMap.put("ANUJ",new String[]{"BOB"});
        Node target = findTarget("YOU","ANUJ",hashMap);
        //打印出最短路徑的各個節點信息
        printSearPath(target);
    }

    /**
     * 打印出到達節點target所經過的各個節點信息
     * @param target
     */
    static void printSearPath(Node target) {
        if (target != null) {
            System.out.print("找到了目標節點:" + target.id + "\n");

            List<Node> searchPath = new ArrayList<Node>();
            searchPath.add(target);
            Node node = target.parent;
            while(node!=null) {
                searchPath.add(node);
                node = node.parent;
            }
            String path = "";
            for(int i=searchPath.size()-1;i>=0;i--) {
                path += searchPath.get(i).id;
                if(i!=0) {
                    path += "-->";
                }
            }
            System.out.print("步數最短:"+path);
        } else {
            System.out.print("未找到了目標節點");
        }
    }

    static Node findTarget(String startId,String targetId,HashMap<String,String[]> map) {
        List<String> hasSearchList = new ArrayList<String>();
        LinkedBlockingQueue<Node> queue=new LinkedBlockingQueue<>();
        queue.offer(new Node(startId,null));
        while(!queue.isEmpty()) {
            Node node = queue.poll();
            if(hasSearchList.contains(node.id)) {
                continue;
            }
            System.out.print("判斷節點:" + node.id +"\n");
            if (targetId.equals(node.id)) {
                return node;
            }
            hasSearchList.add(node.id);
            if (map.get(node.id) != null && map.get(node.id).length > 0) {
                for (String childId : map.get(node.id)) {
                    queue.offer(new Node(childId,node));
                }
            }
        }
        return null;
    }

    static class Node{
        public String id;
        public Node parent;
        public Node(String id,Node parent) {
            this.id = id;
            this.parent = parent;
        }
    }
}

運行時間

如果你在你的整個人際關係網中搜索芒果銷售商,就意味着你將沿每條邊前行(記住,邊是從一個人到另一個人的箭頭或連接),因此運行時間至少爲O(邊數)。
你還使用了一個隊列,其中包含要檢查的每個人。將一個人添加到隊列需要的時間是固定的,即爲O(1),因此對每個人都這樣做需要的總時間爲O(人數)。所以,廣度優先搜索的運行時間爲O(人數 + 邊數),這通常寫作O(V + E),其中V爲頂點(vertice)數,E爲邊數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章