用 JavaScript 實現尋路算法 —— 編程訓練

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同學們好,我是來自 《","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"技術銀河","attrs":{}},{"type":"text","text":"》的 💎 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"三鑽","attrs":{}},{"type":"text","text":" 。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"尋路算法練習","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"學習尋路算法有什麼好處?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尋路是廣度優先搜索算法","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有的搜索的算法的思路的非常相似","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以在講廣度優先的算法的過程中也可以把深度優先搜索類的都講一遍","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"搜索是算法裏面特別重要,通用型也是特別好的一類算法","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏可以幫助大家在算法方面有一定的提升","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"通過可視化來理解算法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算法裏面也是有 UI 相關的部分的","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"並且有一些 JavaScript 特有的部分","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"學習這部分可以讓大家對 JavaScript 語言提高熟悉程度","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"並且對語言裏面的應用方式獲得一個更深的瞭解","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有最重要的是通過異步編程的特性,來講解一些可視化相關的知識","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過把算法的步驟可視化後,我們就可以非常直觀地看到算法的運轉狀況","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"尋路問題的定義","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"尋路的問題","attrs":{}},{"type":"text","text":" —— 就是在一張地圖上指定一個起點和一個終點,從起點通過橫豎斜各個方向去找到它通往終點的一個路徑。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的這個練習裏面我們會製造一張 100 x 100 個格子的地圖,並且在上面繪製我們的從起點到終點的路徑。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一開始我們會先用簡單的橫豎兩個方向來尋找我們的終點,因爲斜向的路徑會有一定的差異,尤其是在地圖比較空曠的情況下。後面我們會逐漸地增加各種各樣的功能,直到我們把搜素全部解決完。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"地圖編輯器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這麼大規模的尋路問題當中,我們首選需要做一個地圖編輯器。它的功能如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"可以通過鼠標左鍵點擊,或者點擊並拖動就可以繪製地圖","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"可以通過鼠標右鍵點擊,或者點擊並拖動就可以清楚地圖","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"可以保存地圖的數據,刷新地圖後,還可以重新繪製出來","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們需要繪製我們地圖的底盤,在繪製之前我們就需要給我們的 HTML 加入 CSS。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的底盤是一個 100 x 100 格的,所以我們需要給每個格子一個樣式。這裏我們只需要用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"flex","attrs":{}}],"attrs":{}},{"type":"text","text":" 來佈局即可。假設我們每個格子都是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"6px","attrs":{}}],"attrs":{}},{"type":"text","text":" 寬高,然後依次每行排列 100 個。所以我們的 HTML 佈局代碼就是如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"
\n
\n
\n
\n
\n ...\n
","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"container","attrs":{}}],"attrs":{}},{"type":"text","text":" 就是外包框","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"cell","attrs":{}}],"attrs":{}},{"type":"text","text":" 就是裏面的格子","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"佈局的 CSS 就是如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上是佈局的 CSS,但是整個底盤是需要我們用數據來構建的,這樣我們後面才能使用它來做我們尋路的問題。所以這裏我們需要加入 JavaScript 來做整個渲染的過程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTML 的部分我們只需要加入一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"div","attrs":{}}],"attrs":{}},{"type":"text","text":",並且擁有一個 ID 爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"container","attrs":{}}],"attrs":{}},{"type":"text","text":" 即可:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"
","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"實現思路","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"創建一個 10000 個數據的數組,並且給裏面都放入 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"0","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"從左到右,從上到下,循環遍歷所有底盤的格子","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"在遍歷的同時在創建 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"div","attrs":{}}],"attrs":{}},{"type":"text","text":" 元素,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"class","attrs":{}}],"attrs":{}},{"type":"text","text":" 爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"cell","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"遍歷的過程中遇到值爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 的就給予背景顏色 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"#7ceefc","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"添加 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"mousemove","attrs":{}}],"attrs":{}},{"type":"text","text":" (鼠標移動) 監聽","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"鼠標移動監聽中有兩種情況,如果是鼠標左鍵點擊狀態下就加入背景顏色,如果是右鍵點擊的話就是清楚當前背景顏色","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"最後把使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"appendChild","attrs":{}}],"attrs":{}},{"type":"text","text":" 把","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"cell","attrs":{}}],"attrs":{}},{"type":"text","text":" 加入到 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"container","attrs":{}}],"attrs":{}},{"type":"text","text":" 之中","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":8,"align":null,"origin":null},"content":[{"type":"text","text":"使用 localStorage 記錄我們的底盤數據","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"代碼實現","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 定義 100 x 100 的底盤數據\n// 使用 localStorage 獲取,如果沒有就新建一個\nlet map = localStorage['map'] ? JSON.parse(localStorage['map']) : Array(10000).fill(0);\n\n// 獲取 container 元素對象\nlet container = document.getElementById('container');\n\n// 遍歷所有格子\nfor (let y = 0; y < 100; y++) {\n for (let x = 0; x < 100; x++) {\n // 創建地圖方格\n let cell = document.createElement('div');\n cell.classList.add('cell');\n // 遇到格子的狀態是 1 的,就賦予背景顏色\n if (map[100 * y + x] == 1) cell.style.backgroundColor = 'aqua';\n // 添加鼠標移動監聽事件\n cell.addEventListener('mousemove', () => {\n // 只有在鼠標點擊狀態下執行\n if (mousedown) {\n if (clear) {\n // 1. 右鍵點擊時,就是清楚格子的狀態\n cell.style.backgroundColor = '';\n map[100 * y + x] = 0;\n } else {\n // 2. 左鍵點擊時,就是畫入格子的狀態\n cell.style.backgroundColor = 'aqua';\n map[100 * y + x] = 1;\n }\n }\n });\n\t// 加入到 container 之中\n container.appendChild(cell);\n }\n}\n\nlet mousedown = false;\nlet clear = false;\n\n// 鼠標按鍵點擊時,把鼠標點擊狀態變爲 true\ndocument.addEventListener('mousedown', e => {\n mousedown = true;\n clear = e.which === 3;\n});\n// 離開點擊鼠標按鍵後,把狀態更變成 false\ndocument.addEventListener('mouseup', () => (mousedown = false));\n// 因爲我們需要使用右鍵,所以要把右鍵默認打開菜單禁用\ndocument.addEventListener('contextmenu', e => e.preventDefault());","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們這裏添加一個保存按鈕,讓我們刷新頁面的時候也會保存着我們編輯過的地圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"
\n\n ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後呈現的結果就是這樣的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/87/8757228fcd808efcf5d60de3b97d4b19.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://tridiamond.tech/frontend-tutorials/exercises/path-finder/1-map-editor.html","title":""},"content":[{"type":"text","text":"查看效果","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/TriDiamond/frontend-tutorials/blob/master/exercises/path-finder/1-map-editor.html","title":""},"content":[{"type":"text","text":"查看代碼","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"喜歡的同學 🌟star 一下謝謝!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"實現廣度優先搜索","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們來深入的解決尋路的問題,上面我們已經定義過尋路問題,就是 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"找到一個起點和終點,然後我們需要找一條路徑,可以從起點到達終點,並且不能越過我們的邊界和牆","attrs":{}}],"attrs":{}},{"type":"text","text":"”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們一眼看過去這個 100 x 100 的網格,確實會覺得這個問題不是特別地好解決。在算這個路徑的過程,中間有大量的數據需要我們去運算。但是我們可以把這個問題變得更簡單一點。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們會到起點,從起點這個位置開展我們的思考。問一下我們自己一個問題:”從這裏我們可以走到哪裏呢?“","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們問題還是不簡單,可以走的地方可多了。因爲起點的位置可能沒有任何障礙物,也不再任何邊緣上。那就是說哪裏都可以呀?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/11/1193bc583f261339d6205170541d3c11.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,那麼我們再把問題的範圍再縮窄一點,“我們從起點開始,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"第一步","attrs":{}}],"attrs":{}},{"type":"text","text":"能走到哪裏呢?”。好這個問題的關鍵就是","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"第一步","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們忽略斜着走的情況(這個我們後面再考慮進去,這裏的終點是簡化問題),我們可以走往","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"上","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"下","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"左","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"右","attrs":{}}],"attrs":{}},{"type":"text","text":" 4 個格子。如果我們逆時針的給這些位置標記一下就會得出一下的路徑標記。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3c146da50d8937ed5732d6ca3d18a764.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就是這樣,我們就完成了尋路的第一本步驟了。接下來我們看看下一步我們又能走到哪裏。如果上面標記的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"3","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"4","attrs":{}}],"attrs":{}},{"type":"text","text":" 都是可以走的,按照我們剛剛的思路,我們現在看 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 號格子又能走哪裏呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bd/bd3cb170561b2bb956cc4639cbc98c94.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒有錯,按照逆時針的方式,我們可以走的格子有 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"5","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"6","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"7","attrs":{}}],"attrs":{}},{"type":"text","text":" 三個格子。所以我們可以按照這樣的走法,把","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"3","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"4","attrs":{}}],"attrs":{}},{"type":"text","text":" 都走一遍。最後我們就會發現下面這樣的路徑:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a3/a3dadfa9cff63dc8c7f8d996cb7ba5db.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們看看下面的動畫效果,看看整個過程是怎麼樣的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/0215f598bfeac2c41a59df21f7f952a3.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們就是這樣,一直往外尋找我們可以走到哪些格子,直到我們找到我們的終點。這樣即可幫助我們找到從起點到終點的路線。當然在尋找的時候如果我們遇到邊界或者障礙的時候就會跳過,因爲這些都是走不過去的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個思路的問題,很容易讓大家想到遞歸,但是這個方法並不適合使用遞歸來表達。如果我們用遞歸來表達,我們肯定是從找到 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 這個格子之後,就會開始展開找 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 周圍的格子了。那麼 5,6,7 就會在 2,3,4 之前被執行了(因爲是遞歸,我們會逐層展開的)。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以說,如果我們默認用遞歸的方式來表達,這個尋路的方式就會變成 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"深度優先搜索","attrs":{}}],"attrs":{}},{"type":"text","text":"”,但是對尋路問題來說深度優先搜索是不好的,“","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"廣度優先搜索","attrs":{}}],"attrs":{}},{"type":"text","text":"”纔是好的。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"爲什麼尋路廣度優先搜索比深度優先搜索好呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來舉個例子,就更好理解了!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a9/a919a6f8f37b0acd29894aa82f2f13b8.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們現在走入了一個迷宮,前方有成千上萬個分叉口,這個時候我們有兩種搜索出路的方法","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"方案一","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獨自一人,選擇左邊或者右邊一直走那邊的每一條分支,並且都一直走到底直到走到有死衚衕了,然後就回頭走另外那邊的分支。就這樣一直走一直切換分支,直到我們找到出口的分支爲止。運氣好的話,很快就找到出口了,運氣不好的話,所有分支都基本走個遍才能找到出口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"方案二","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們是獨自一個人,但是我們有 “分身術”,當我們遇到分叉口的時候我們是可以每一個分叉口都去嘗試一遍。比如 A,B,C 三個分叉口,我們可以 A 路線走一步,然後 B 路線也走一步,最後 C 路線也走一步,這裏我們步伐整齊統一,就有點像我們上面的動態圖中一樣,一直往所有分叉口往外擴展尋找路線。這樣我們就可以在 N 個分叉口依次一步一步往前走,直到找到出口。這樣的方式等同於我們依次的在尋找每一個分叉口,這樣的搜索顯然是比第一個好的。(","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"我不知道你們,但是就有分身術這個技能我就覺得已經在尋路這個問題中贏了!","attrs":{}},{"type":"text","text":")","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/db/dbc5f72ec5ef682ce8c4fc7e823faddf.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"迴歸到算法的話,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"方案一","attrs":{}},{"type":"text","text":"其實就是“","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"深度優先搜索","attrs":{}}],"attrs":{}},{"type":"text","text":"”,而**方案二**就是\"","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"廣度優先搜索","attrs":{}}],"attrs":{}},{"type":"text","text":"\"!顯然在尋路這種問題是廣度優先搜索更爲高效!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現廣度優先搜索代碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"玩過走迷宮的同學肯定都會想到,在走迷宮的時候,我們都會給我們走過的路徑標記,這樣我們才知道我們走過哪裏,最後通過這些記錄找到可以到達終點的路徑。我們這個尋路的問題也不例外,根據我們上面的分析,我們從起點就開始往外擴展尋找可以走的格子,每當我們找到一個可走的格子,我們都需要記錄起來。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在算法當中我們都會把數據加入一個 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"集合","attrs":{}}],"attrs":{}},{"type":"text","text":"” 裏面,而這個集合就是所有搜索算法的 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"靈魂","attrs":{}}],"attrs":{}},{"type":"text","text":"”。所有搜索算法的差異,完全就是在於這個 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"集合 (queue)","attrs":{}}],"attrs":{}},{"type":"text","text":"” 裏面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":" 是一種數據結構,我們也叫它爲 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"隊列","attrs":{}}],"attrs":{}},{"type":"text","text":"”,它的特性就是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"先進先出","attrs":{}}],"attrs":{}},{"type":"text","text":" ,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"一邊進另外一邊出","attrs":{}}],"attrs":{}},{"type":"text","text":"。實際效果與下圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/84/843eb20499f13be573965d347d8ff062.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼 JavaScript 中有沒有隊列這樣的數據結構呢?有!","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"JavaScript 中的數組就是天然的隊列 (Queue)","attrs":{}}],"attrs":{}},{"type":"text","text":",同時 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"JavaScript 中的數組也是天然的棧 (Stack)","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/11/1193bc583f261339d6205170541d3c11.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript 的數組有 2 組常用的處理方法 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"shif","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"unshift","attrs":{}}],"attrs":{}},{"type":"text","text":",以及 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"push","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pop","attrs":{}}],"attrs":{}},{"type":"text","text":"。 但是如果我們混搭來使用他們的話,就會讓我們的數組變成不一樣的數據結構。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"push","attrs":{}}],"attrs":{}},{"type":"text","text":" 與 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"shift","attrs":{}}],"attrs":{}},{"type":"text","text":" 或者 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pop","attrs":{}}],"attrs":{}},{"type":"text","text":" 與 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"unshift","attrs":{}}],"attrs":{}},{"type":"text","text":" 結合使用,那麼數組就是一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"隊列 (Queue)","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"push","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"pop","attrs":{}}],"attrs":{}},{"type":"text","text":" 結合使用那麼,數組就是一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"棧 (Stack)","attrs":{}}],"attrs":{}},{"type":"text","text":" (當然 shif 和 unshif 也是可以的,但是我們一般不會用這個組合來做棧,因爲我們考慮到 JavaScript 的數組的實現,這樣使用性能會變低。)","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些理論知識都搞懂了,我們就可以開始整理一下編寫代碼的思路了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要聲明一個隊列,也就是聲明一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":",並且把我們的起點默認放入隊列中。(JavaScript 中我們使用數組即可)","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"編寫一個 “入隊” 的方法,條件是如果遇到邊緣或者障礙就直接跳出方法,這些都是不可走的格子所以不會加入我們的隊列。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有遇到以上情況,我們就可以先把可以走的格子在我們的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"map","attrs":{}}],"attrs":{}},{"type":"text","text":"(在實現我們的地圖數據的時候聲明的一個數組)中記錄一個狀態,這裏我們可以使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":", 代表這個格子我們已經走過了,這裏我們加入一個標記。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"循環我們隊列中可以走的格子,這裏的主要目標就是把所有記錄了可以走的格子都找到它的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"上","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"下","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"左","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"右","attrs":{}}],"attrs":{}},{"type":"text","text":",並且把這些可走的格子都入隊列,然後進入下一個循環時就會去找這些新入隊列的格子可以走到哪裏,然後把後面找到的格子再次入隊列,以此類推。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"這個循環有一個截止條件,那就是如果我們在循環每個格子的時候,找到了終點的格子,我們就可以直接返回 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"true","attrs":{}}],"attrs":{}},{"type":"text","text":",代表我們已經找到終點了。","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好思路又了,我們馬上來看看代碼實現:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 上一部分的代碼,這裏就忽略了...\n// 只要在上部分的代碼後面改造這部分即可\n\n// 離開點擊鼠標按鍵後,把狀態更變成 false\ndocument.addEventListener('mouseup', () => (mousedown = false));\n// 因爲我們需要使用右鍵,所以要把右鍵默認打開菜單禁用\ndocument.addEventListener('contextmenu', e => e.preventDefault());\n\n/**\n * 尋路方法\n * @param {Array} map 地圖數據\n * @param {Array} start 起點 例如:[0, 0]\n * @param {Array} end 終點 例如:[50, 50]\n * @return Boolean\n */\nfunction path(map, start, end) {\n var queue = [start];\n \n function insert(x, y) {\n // 到達底盤邊緣,直接停止\n if (x < 0 || x >= 100 || y < 0 || y >= 100) return;\n // 遇到地圖的牆,也停止\n if (map[y * 100 + x]) return;\n // 標記可以走的路的格子的狀態爲 2\n map[y * 100 + x] = 2;\n // 把可走的路推入隊列\n queue.push([x, y]);\n }\n\n // 循環格子 4 邊的格子\n while (queue.length) {\n let [x, y] = queue.shift();\n console.log(x, y);\n\n // 遇到了終點位置就可以返回了\n if (x === end[0] && y === end[1]) return true;\n\n // 把上下左右推入隊列\n insert(x - 1, y);\n insert(x, y - 1);\n insert(x + 1, y);\n insert(x, y + 1);\n }\n\n return false;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了可以調試看看,我們的代碼是否正確,我們來加一個按鈕,並且讓它可以執行我們這個尋路的方法 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"path","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"
\n\n
\n \n \n
","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們的按鈕會執行一個尋路方法,設置了起點是 x = 0,y = 0 的位置,終點是 x = 50,y = 50 的位置。點擊這個按鈕之後,我們就可以在瀏覽器的調試工具中的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"console","attrs":{}}],"attrs":{}},{"type":"text","text":" 之中看到尋路過程中走過的 x 和 y。如果我們的代碼是對的話,最後我們會看到在 50, 50 的時候就會停止運行了。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://tridiamond.tech/frontend-tutorials/exercises/path-finder/2-map-path.html","title":""},"content":[{"type":"text","text":"查看效果","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/TriDiamond/frontend-tutorials/blob/master/exercises/path-finder/2-map-path.html","title":""},"content":[{"type":"text","text":"查看代碼","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"喜歡的同學 🌟star 一下謝謝!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"加入 Async 和 Await 來方便調試和展示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一個代碼中,其實已經實現了尋路算法的主體部分了。但是裏面還是有一些問題的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"算法雖然最終返回了我們終點的位置,並且返回了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"true","attrs":{}}],"attrs":{}},{"type":"text","text":",看起來是符合了我們的預期的。但是它的正確性我們不太好保證。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"所以我們希望有一個可視化的效果來觀察這個尋路算法的過程","attrs":{}},{"type":"text","text":"。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"我們是找到一條路徑,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"這個最終的路徑我們還沒有把它找出來","attrs":{}},{"type":"text","text":"。","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些問題我們下來幾個步驟中會一步一步來解決。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何有看我們的 《TicTacToe 三子棋》的編程與算法練習的文章的話,我們裏面有講到使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"async","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"await","attrs":{}}],"attrs":{}},{"type":"text","text":" ,來讓函數中間可插入一些異步的操作。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這部分的代碼我們做了一些代碼的改造:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"把 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"path()","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數改爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"async","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"把 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"insert()","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數改爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"async","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"因爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"insert()","attrs":{}}],"attrs":{}},{"type":"text","text":" 編程了異步函數,所以我們 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while()","attrs":{}}],"attrs":{}},{"type":"text","text":" 循環中的 insert 調用都需要在前面加入 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"await","attrs":{}}],"attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"同時我們需要加入一個等待函數","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"sleep()","attrs":{}}],"attrs":{}},{"type":"text","text":",它必須返回一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"promise","attrs":{}}],"attrs":{}},{"type":"text","text":" ","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"在我們入隊列之後,在改變當前格子狀態爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":" 之前,我們會對 DOM 元素中的格子的背景顏色進行改變,這樣我們就可以看到尋路的過程","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"因爲我們需要看到這個過程,所以每一次入隊列的時候我們需要給一個 1 秒的等待時間,這個就是使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"async","attrs":{}}],"attrs":{}},{"type":"text","text":" 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"await","attrs":{}}],"attrs":{}},{"type":"text","text":" 的好處。在加入這個背景顏色之前,我們就可以加入一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"await sleep(1)","attrs":{}}],"attrs":{}},{"type":"text","text":",這樣入隊列和改變格子背景顏色之前就會有 1 秒的延遲。","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好廢話少說,我們來看看代碼有什麼變化:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 上一部分的代碼,這裏就忽略了...\n// 只要在上部分的代碼後面改造這部分即可\n\n// 離開點擊鼠標按鍵後,把狀態更變成 false\ndocument.addEventListener('mouseup', () => (mousedown = false));\n// 因爲我們需要使用右鍵,所以要把右鍵默認打開菜單禁用\ndocument.addEventListener('contextmenu', e => e.preventDefault());\n\n/**\n * 等待函數\n * @param {Integer} t 時間 (秒)\n * @return Promise\n */\nfunction sleep(t) {\n return new Promise(function (resolve) {\n setTimeout(resolve, t);\n });\n}\n\n/**\n * 尋路方法 (異步)\n * @param {Array} map 地圖數據\n * @param {Array} start 起點 例如:[0, 0]\n * @param {Array} end 終點 例如:[50, 50]\n * @return Boolean\n */\nasync function path(map, start, end) {\n var queue = [start];\n\n /**\n * 入隊方法 (異步)\n * @param {Integer} x\n * @param {Integer} y\n */\n async function insert(x, y) {\n // 到達底盤邊緣,直接停止\n if (x < 0 || x >= 100 || y < 0 || y >= 100) return;\n // 遇到地圖的牆,也停止\n if (map[y * 100 + x]) return;\n // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化\n await sleep(1);\n // 給搜索到的路徑的格子加上背景顏色\n container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';\n // 標記可以走的路的格子的狀態爲 2\n map[y * 100 + x] = 2;\n // 把可走的路推入隊列\n queue.push([x, y]);\n }\n\n // 循環格子 4 邊的格子\n while (queue.length) {\n let [x, y] = queue.shift();\n // console.log(x, y);\n\n // 遇到了終點位置就可以返回了\n if (x === end[0] && y === end[1]) return true;\n\n // 把上下左右推入隊列\n await insert(x - 1, y);\n await insert(x, y - 1);\n await insert(x + 1, y);\n await insert(x, y + 1);\n }\n\n return false;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們執行後的結果:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5a/5aab27190ac1ab42e994009159e7bf37.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://tridiamond.tech/frontend-tutorials/exercises/path-finder/3-map-search-async.html","title":""},"content":[{"type":"text","text":"查看效果","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/TriDiamond/frontend-tutorials/blob/master/exercises/path-finder/3-map-search-async.html","title":""},"content":[{"type":"text","text":"查看代碼","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"喜歡的同學 🌟star 一下謝謝!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"處理路徑問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一步我們用一個動畫,讓我們特別清晰的去理解整個尋路算法的過程。但是上一步提到的第二個問題,我們目前是還沒有解決的。所以這裏我們就是要找到最終的路徑是什麼,我們最終通過尋路算法之後,我們怎麼獲得一個路徑是可以從起點到達終點的呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實處理這個路徑的問題也是非常的簡單的,我們先看看看我們之前講過的尋路的思路:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a3/a3dadfa9cff63dc8c7f8d996cb7ba5db.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上面這個圖,我們已經都還記得,在我們訪問每一個格子的時候,我們都往外擴展找到可走的格子有哪些。這個過程當中,我們都是知道每一個被擴展到的格子都是由前一個給自擴展過來的。換句話說,每一個格子都是知道我們是從那個給自擴展過來的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如說 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"5","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"6","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"7","attrs":{}}],"attrs":{}},{"type":"text","text":" 這三個格子都是從 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 這個格子擴展過來的。既然我們知道每個給自上一步的來源,是不是我們可以在每一個給自記錄上一個來源的格子呢?所以在代碼中我們是不是可以在入隊的時候,就記錄上一個給自的 x,y軸呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們就產生一個想法,通過這個記錄,我們怎麼尋找最終的路徑呢?","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們假設終點是在 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"8","attrs":{}}],"attrs":{}},{"type":"text","text":" 這個位置,我們過來 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"8","attrs":{}}],"attrs":{}},{"type":"text","text":" 這個點的時候是從我們的起點一步一步擴展出來的,那我們反過來通過記錄了每一個格子的前驅格子,就可以一步一步收縮回到起點呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那就是說,從 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"8","attrs":{}}],"attrs":{}},{"type":"text","text":" 開始收,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"8","attrs":{}}],"attrs":{}},{"type":"text","text":" 是從 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":" 走過來的,而 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"2","attrs":{}}],"attrs":{}},{"type":"text","text":" 就是從","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"起點","attrs":{}}],"attrs":{}},{"type":"text","text":" 擴展過來的,最終我們就找到起點了。如果我們在收縮的過程記錄着收縮時候訪問到的格子,最終這些格子就是從起點到終點的整條路徑了!是不是很神奇?!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實也可以理解爲一個 “原路返回” 的效果!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bd/bd95a0f527518180b8a254451db42abe.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先不要雞凍,穩住!我們接下來看看代碼是如何處理的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"其實基本上我們的代碼沒有太多的改變","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"首先就是在 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while","attrs":{}}],"attrs":{}},{"type":"text","text":" 循環當中的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"insert()","attrs":{}}],"attrs":{}},{"type":"text","text":" 調用的時候添加了上一個座標的傳參","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們順便也把橫向的可走的格子也加入到隊列中","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"這裏因爲我們需要記錄所有格子的前驅座標,所以我們需要聲明一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"table","attrs":{}}],"attrs":{}},{"type":"text","text":" 的變量存放這個數據","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"在我們進入隊列之前,我們就把當前入隊列的格子的值存爲上一個格子的座標(這個爲了我們後面方便收縮是找到整個路徑)","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"最後在 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while","attrs":{}}],"attrs":{}},{"type":"text","text":" 循環中,當我們遇到終點的 x 和 y 的時候,我們加入一段 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while","attrs":{}}],"attrs":{}},{"type":"text","text":" 循環","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"這個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while","attrs":{}}],"attrs":{}},{"type":"text","text":" 就是往回一直走,知道我們找到起點位置,在往回走的同時,把每一個經過的格子的背景改爲另外一個背景顏色,這樣就可以在我們的地圖上畫出一個路徑了!","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好思路清晰,我們來上一波代碼吧!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 上一部分的代碼,這裏就忽略了...\n// 只要在上部分的代碼後面改造這部分即可\n\n/**\n * 尋路方法 (異步)\n * @param {Array} map 地圖數據\n * @param {Array} start 起點 例如:[0, 0]\n * @param {Array} end 終點 例如:[50, 50]\n * @return Boolean\n */\nfunction sleep(t) {\n return new Promise(function (resolve) {\n setTimeout(resolve, t);\n });\n}\n\n/**\n * 入隊方法 (異步)\n * @param {Integer} x\n * @param {Integer} y\n */\nasync function findPath(map, start, end) {\n // 創建一個記錄表格\n let table = Object.create(map);\n let queue = [start];\n\n /**\n * 入隊方法 (異步)\n * @param {Integer} x\n * @param {Integer} y\n * @param {Array} pre 上一個格子的座標:[x,y]\n */\n async function insert(x, y, pre) {\n // 到達底盤邊緣,直接停止\n if (x < 0 || x >= 100 || y < 0 || y >= 100) return;\n // 遇到地圖的牆,也停止\n if (table[y * 100 + x]) return;\n // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化\n // await sleep(1);\n // 給搜索到的路徑的格子加上背景顏色\n container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';\n // 標記走過的格子的值,標記爲上一個格子的 x,y 位置\n table[y * 100 + x] = pre;\n // 把可走的路推入隊列\n queue.push([x, y]);\n }\n\n // 循環格子 4 邊的格子\n while (queue.length) {\n let [x, y] = queue.shift();\n // console.log(x, y);\n\n // 遇到了終點位置就可以返回了\n if (x === end[0] && y === end[1]) {\n let path = [];\n\n // 往回走,直到走到起點\n // 這樣就能畫出最佳路徑了\n while (x != start[0] || y != start[1]) {\n path.push(map[y * 100 + x]);\n [x, y] = table[y * 100 + x];\n await sleep(1);\n container.children[y * 100 + x].style.backgroundColor = 'fuchsia';\n }\n\n return path;\n }\n\n // 把上下左右推入隊列\n await insert(x - 1, y, [x, y]);\n await insert(x, y - 1, [x, y]);\n await insert(x + 1, y, [x, y]);\n await insert(x, y + 1, [x, y]);\n\n // 把 4 個 斜邊推入隊列\n await insert(x - 1, y - 1, [x, y]);\n await insert(x + 1, y - 1, [x, y]);\n await insert(x - 1, y + 1, [x, y]);\n await insert(x + 1, y + 1, [x, y]);\n }\n\n return null;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:這裏我們把原來的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"path","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數名改爲了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"findPath","attrs":{}}],"attrs":{}},{"type":"text","text":",因爲在尋找路徑的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"while","attrs":{}}],"attrs":{}},{"type":"text","text":" 循環裏面我們用了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"path","attrs":{}}],"attrs":{}},{"type":"text","text":" 做爲記錄路徑的變量。這裏還需要注意的就是,我們的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"find","attrs":{}}],"attrs":{}},{"type":"text","text":" 按鈕裏面的函數調用也需要一併修改哦。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n
\n \n \n
","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行最終效果如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/eb/eb81f224a7a560665193edbe218d5829.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://tridiamond.tech/frontend-tutorials/exercises/path-finder/4-map-find-path.html","title":""},"content":[{"type":"text","text":"查看效果","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/TriDiamond/frontend-tutorials/blob/master/exercises/path-finder/4-map-find-path.html","title":""},"content":[{"type":"text","text":"查看代碼","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"喜歡的同學 🌟star 一下謝謝!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"啓發式尋路(A*)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到這裏我們已經完成了整個廣度優先尋路的算法。但是廣搜式尋路是不是最好的尋路方案呢?","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"其實並不是的","attrs":{}}],"attrs":{}},{"type":"text","text":"!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bc/bc8b68ef9eb75f70203c94c48089c90b.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過各位數學科學家的努力下,他們證明了一件事情。我們是可以有一種方法能夠加速尋路的,通過用這個方法我們不需要使用一個非常傻的方式來挨個去找。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種尋路的方式叫做 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"啓發式尋路","attrs":{}}],"attrs":{}},{"type":"text","text":"”","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓發式尋路就是用一個函數去判斷這些點擴展的優先級。只要我們判斷好了優先級,我們就可以有目的的去驗者格子的方向做優先找路。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“但是這個找到的路徑是不是最佳路徑呀?說的那麼神奇的。“ 說實話,這個我們普通人的思路就排不上用場了。但是數學家證明了一件事情,只要我們使用啓發式函數來估計值,並且這些值能夠一定小於當前點到終點的路徑的長度,這樣就一定能找到最優路徑。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種能找到最優路徑的啓發式尋路,在計算機裏面我們叫它做 “","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"A*","attrs":{}}],"attrs":{}},{"type":"text","text":"”。這裏面的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"A","attrs":{}}],"attrs":{}},{"type":"text","text":" 代表着一種不一定能找到最優路徑的啓發式尋路。所以 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"A*","attrs":{}}],"attrs":{}},{"type":"text","text":" 就是 A 尋路的一個特例,是一種可以找到最佳路徑的一種算法。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要實現這個啓發式尋路,其實我們是不需要改過多我們 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"findPath()","attrs":{}}],"attrs":{}},{"type":"text","text":"函數裏面的代碼的。我們要改的是我們存儲的數據結構,也就是我們的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":" 隊列。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們要把 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":" 的先進先出變成一個","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"優先隊列 (Prioritized Queue)","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們需要構建一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Sorted","attrs":{}}],"attrs":{}},{"type":"text","text":" 類,這個類有幾個工作:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"這個類可以存儲我們之前 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":" 隊列裏面的數據","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"可以支持傳入排序函數(也就是和我們 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"array sort","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數一樣的功能,可以傳入一個排序規則函數,也叫 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"compare","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數)","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"在我們通過這個類獲取值的時候給我們數據裏面的最小值","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在我們插入數據的時候不需要排序,只是單純的保存","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"每次去除數據的時候,可以把輸出的數據在類的數據裏面刪除掉","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"這樣我們就可以一直在裏面獲取數據,知道沒有數據爲止","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"最後加入一個可以獲取當前數據的長度(這個在我們的尋路算法中需要用到)","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們用一個非常 “土鱉” 的數組來實現這個 Sorted 類,但是在計算機當中,我們還有很多其他方式可以實現這一種類。比如說 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"winner tree","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"堆 (heap)","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"排序二叉樹","attrs":{}}],"attrs":{}},{"type":"text","text":" 等很多的不同思路來實現有序的數據結構。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好廢話少說,我們來看看怎麼實現這個數據結構:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/** 排序數據類 */\nclass Sorted {\n constructor(data, compare) {\n this.data = data.slice();\n this.compare = compare || ((a, b) => a - b);\n }\n take() {\n // 考慮到 null 也是可以參與比較的,所以這裏返回 null 是不合適的\n if (!this.data.length) return;\n // 記錄最小的值\n // 默認第一個位置爲最小值\n let min = this.data[0];\n // 記錄最小值的位置\n let minIndex = 0;\n\n // 開始比較數組裏面的所有值,找到更小的值,就記錄爲 min\n // 同時記錄最小值,和最小值的位置\n for (let i = 1; i < this.data.length; i++) {\n if (this.compare(this.data[i], min) < 0) {\n min = this.data[i];\n minIndex = i;\n }\n }\n\n // 現在我們要把最小值拿出去了,所以要在我們當前的數據中移除\n // 這裏我們不考慮使用 splice,因爲 splice 移除會涉及數組內的元素都要往前挪動\n // 這樣 splice 就會有一個 O(N) 的時間複雜度\n // 這裏我們用一個小技巧,把數組裏面最後一位的值挪動到當前發現最小值的位置\n // 最後使用 pop 把最後一位數據移除\n this.data[minIndex] = this.data[this.data.length - 1];\n this.data.pop();\n // 最後把最小值輸出\n return min;\n }\n give(value) {\n this.data.push(value);\n }\n get length() {\n return this.data.length;\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了這個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Sorted","attrs":{}}],"attrs":{}},{"type":"text","text":" 的數據結構之後,我們就可以用來解決我們的尋路問題,讓我們可以找到最佳的路徑了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"加入這個最佳路徑的邏輯,我們需要改到一下幾個點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"首先改寫我們的存儲數據的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"queue","attrs":{}}],"attrs":{}},{"type":"text","text":",使用我們 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Sorted","attrs":{}}],"attrs":{}},{"type":"text","text":" 的排序數據結構","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"編寫一個 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"distance()","attrs":{}}],"attrs":{}},{"type":"text","text":" 函數來運算任何一個格子與終點格子的直線距離","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"把所有入隊列和出隊列的調用改爲使用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Sorted","attrs":{}}],"attrs":{}},{"type":"text","text":" 類裏面的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"take","attrs":{}}],"attrs":{}},{"type":"text","text":" 取值 和 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"give","attrs":{}}],"attrs":{}},{"type":"text","text":" 插入值函數","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"其他地方基本就沒有什麼改變了","attrs":{}}]}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們來看看代碼是怎麼實現的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 上一部分的代碼,這裏就忽略了...\n// 只要在上部分的代碼後面改造這部分即可\n\n/**\n * 尋路方法 (異步)\n * @param {Array} map 地圖數據\n * @param {Array} start 起點 例如:[0, 0]\n * @param {Array} end 終點 例如:[50, 50]\n * @return Boolean\n */\nasync function findPath(map, start, end) {\n // 創建一個記錄表格\n let table = Object.create(map);\n let queue = new Sorted([start], (a, b) => distance(a) - distance(b));\n\n async function insert(x, y, pre) {\n // 到達底盤邊緣,直接停止\n if (x < 0 || x >= 100 || y < 0 || y >= 100) return;\n // 遇到地圖的牆,也停止\n if (table[y * 100 + x]) return;\n // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化\n await sleep(1);\n // 給搜索到的路徑的格子加上背景顏色\n container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';\n // 標記走過的格子的值,標記爲上一個格子的 x,y 位置\n table[y * 100 + x] = pre;\n // 把可走的路推入隊列\n queue.give([x, y]);\n }\n\n /**\n * 獲取格與格之前的距離\n * @param {Array} point 當前格子的座標:[x,y]\n */\n function distance(point) {\n // 使用三角形 x^2 + y^2 = z^2 的方式來計算距離\n return (point[0] - end[0]) ** 2 + (point[1] - end[1]) ** 2;\n }\n\n // 循環格子 4 邊的格子\n while (queue.length) {\n let [x, y] = queue.take();\n // console.log(x, y);\n\n // 遇到了終點位置就可以返回了\n if (x === end[0] && y === end[1]) {\n let path = [];\n\n // 往回走,直到走到起點\n // 這樣就能畫出最佳路徑了\n while (x != start[0] || y != start[1]) {\n path.push(map[y * 100 + x]);\n [x, y] = table[y * 100 + x];\n container.children[y * 100 + x].style.backgroundColor = 'fuchsia';\n }\n\n return path;\n }\n\n // 把上下左右推入隊列\n await insert(x - 1, y, [x, y]);\n await insert(x, y - 1, [x, y]);\n await insert(x + 1, y, [x, y]);\n await insert(x, y + 1, [x, y]);\n\n // 把 4 個 斜邊推入隊列\n await insert(x - 1, y - 1, [x, y]);\n await insert(x + 1, y - 1, [x, y]);\n await insert(x - 1, y + 1, [x, y]);\n await insert(x + 1, y + 1, [x, y]);\n }\n\n return null;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個最終的運行效果如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cd7dfa17b40260f1210e225cfb5b1c5c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://tridiamond.tech/frontend-tutorials/exercises/path-finder/6-map-best-path.html","title":""},"content":[{"type":"text","text":"查看效果","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/TriDiamond/frontend-tutorials/blob/master/exercises/path-finder/6-map-best-path.html","title":""},"content":[{"type":"text","text":"查看代碼","attrs":{}}]},{"type":"text","text":" | ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"喜歡的同學 🌟star 一下謝謝!","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我是來自《","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"技術銀河","attrs":{}},{"type":"text","text":"》的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"三鑽","attrs":{}},{"type":"text","text":":\"學習是爲了成長,成長是爲了不退步。堅持才能成功,失敗只是因爲沒有堅持。同學們加油哦!下期見!\"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f7/f7db8645b6fdfa85351bc5f5aeb79aeb.gif","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":1}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章