學習筆記
學習書目:《算法圖解》- Aditya Bhargava
圖簡介
今天是五一,假如我要從家出發去公園玩,現在可去公園的公交車路線如下:
現在,我想找一條換乘最少的線路,該使用什麼樣的算法呢?
我們先找出一步就能到達的地方,顯而易見,一步能到達A、B;再找出兩步能到達的地方,經過簡單尋找,我們發現兩步能到達E、D、C三個地;第三步呢?可以看出第三步可以到達公園和E。很好!此時我們就找到了換乘最少的路線:家–1-->路–>A–3路–>E–5路–>公園
這種問題被稱爲最短路徑問題。我們要找出最短路徑,這可能是前往朋友家的最短路徑,也可能是國際象棋中把對方將死的最少步數。解決最短路徑問題的算法被稱爲廣度優先搜索。
我們解決這個問題時,用了兩個步驟:
(1)使用圖來建立問題模型
(2)使用廣度優先搜索解決問題
圖是啥
圖模擬了一組連接,比如可像下圖一樣表示小黃欠我錢:
我們再看一幅更復雜的圖:
可以看到這幅圖由節點和邊組成,一個節點可能與衆多節點直接相連,這些節點被稱爲鄰居。比如,我是小黃的鄰居,但奶奶不是小黃的鄰居;奶奶既是我的鄰居,又是大白的鄰居;但是,奶奶沒有鄰居,因爲雖然有指向她的箭頭,卻沒有從她出發的箭頭。這種圖叫做有向圖,其中的關係是單向的。無向圖則沒有箭頭,直接相連的節點互爲鄰居。
我們看到,下面兩個圖是等價的:
廣度優先搜索
廣度優先搜索是一種用於圖的查找算法,可幫助回答兩類問題。
第一類問題:從節點A出發,有前往節點B的路徑嗎?
第二類問題:從節點A出發,前往節點B的哪條路徑最短?
假如我是一位作者,我想在我的Twitter的朋友列表裏找一位編輯。我的想法很簡單,先在朋友裏找有沒有編輯,沒有的話就在朋友的朋友裏尋找,再沒有的話,就在朋友的朋友的朋友裏尋找…以此類推
現在我有3個朋友:
我先創建一個朋友名單:
['Huang', 'Hei', 'Bai']
然後依次檢查我的3個朋友是否是編輯。假如我沒有朋友是編輯,那麼我就必須在朋友的朋友中尋找:
比如,我發現Huang不是編輯,那我就把它的朋友Write和Tim加入我的朋友名單,並把Huang從名單中剔除:
['Hei', 'Bai', 'Write', 'Tim']
使用這種算法將搜遍我的整個人際關係網,直到找到編輯。這就是廣度優先搜索算法。
尋找最短路徑
由我在Twitter的朋友列表裏找編輯的例子中,我們已經回答了廣度優先搜索的第一個問題(從節點A出發,有前往節點B的路徑嗎?)現在,我們就要回答第二個問題,即哪位編輯是離我關係最近。比如,我的朋友和我是一度關係,我朋友的朋友和我是二度關係。在我看來,一度關係勝過二度關係。因此,我要現在一度關係中尋找編輯,沒有的話,再從二度關係中尋找編輯,以此類推。
需要注意的是,我們必須把一度關係查找完,才能查找二度關係。比如,我必須先查找完Huang, Hei, Bai才能查找Write等二度關係。
以我們剛剛建立的朋友名單爲例,一度關係在二度關係之前加入名單:
['Hei', 'Bai',' Write', 'Tim']
我們按順序依次檢查名單中的每個人,看看他是否是編輯。這將先在一度關係中查找,再在二度關係中查找,因此找到的是關係最近的編輯。
注意!只有按順序查找才能找到與我關係最近的編輯。換句話說Bai先於Tim加入名單,就要先檢查Bai。有一個可實現這種目的的數據結構,那就是隊列。
隊列
隊列類似於棧,你不能隨機地訪問隊列中的元素。隊列只支持兩種操作:入隊和出隊。如果我將A和B加入隊列,先加入隊列的A也將先出隊,而後加入的B則會後出隊。
隊列是一種先進先出(First In First Out,FIFO)的數據結構,而棧是一種後進先出(Last In First Out,LIFO)的數據結構。
實現圖
現在,我們將用散列表來表達這種我-->Huang
的關係,並用python代碼來實現圖:
graph = {}
graph['me'] = ['Huang', 'Hei', 'Bai']
graph['Huang'] = ['Write', 'Tim']
graph['Bai'] = ['Tim']
graph['Hei'] = ['Black', 'Ada']
graph['Write'] = []
graph['Tim'] = []
graph['Black'] = []
graph['Ada'] = []
我們看到Write、Tim、Black、Ada沒有鄰居,因爲只有指向它們的箭頭,卻沒有從它們出發的箭頭。
實現算法
python代碼:
from collections import deque
#判斷誰是編輯
def person_is_edit(name):
return name[-1] == 'a'
#假設名字最後一個字母是a就是編輯
#查找某人的關係列表中誰是編輯
def search(name):
search_queue = deque()
search_queue += graph[name]
searched = []
#記錄已經檢查過的人,防止低效率和無限循環
while search_queue:
person = search_queue.popleft()
if person not in searched:
if person_is_edit(person):
print('I find you {}!'.format(person))
return True
else:
search_queue += graph[person]
searched.append(person)
return False
search('me')
控制檯輸出:
I find you Ada!
我們看到,我們在上面的python代碼中加入了一個已搜索名單searched,這是爲了防止循環和低效率。比如,Huang和Bai都有一個朋友Tim,但是我們只需要檢查一次Tim,否則重複查詢就是做了無用功。
並且如果出現下面這種情況,我們就會進入死循環:
所以,在檢查一個人是否是編輯之前,確認此人是否被檢查過,就十分重要了。
運行時間
如果我在我的關係網中搜尋編輯,那就意味着我沿着每條邊前行,因此運行時間至少是O(邊數)。這裏,我們還使用了一個隊列,其中包含要檢查的每個人。將一個人添加到隊列需要的時間是固定的,即爲O(1),因此對每個人都這樣做需要的總時間爲O(人數)。所以,廣度優先搜索的運行時間爲O(人數 + 邊數),這通常寫作O(V + E),其中V 爲頂點數,E 爲邊數。