主頁 > 區塊鏈 > 用 JavaScript 實作尋路演算法 —— 編程訓練

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

2020-11-07 07:35:32 區塊鏈

同學們好,我是來自 《技術銀河》的 💎 三鉆

尋路演算法練習

學習尋路演算法有什么好處?

  • 尋路是廣度優先搜索演算法
  • 所有的搜索的演算法的思路的非常相似
  • 所以在講廣度優先的演算法的程序中也可以把深度優先搜索類的都講一遍
  • 搜索是演算法里面特別重要,通用型也是特別好的一類演算法
  • 這里可以幫助大家在演算法方面有一定的提升

通過可視化來理解演算法

  • 演算法里面也是有 UI 相關的部分的
  • 并且有一些 JavaScript 特有的部分
  • 學習這部分可以讓大家對 JavaScript 語言提高熟悉程度
  • 并且對語言里面的應用方式獲得一個更深的了解
  • 還有最重要的是通過異步編程的特性,來講解一些可視化相關的知識
  • 通過把演算法的步驟可視化后,我們就可以非常直觀地看到演算法的運轉狀況

尋路問題的定義

尋路的問題 —— 就是在一張地圖上指定一個起點和一個終點,從起點通過橫豎斜各個方向去找到它通往終點的一個路徑,

在我們的這個練習里面我們會制造一張 100 x 100 個格子的地圖,并且在上面繪制我們的從起點到終點的路徑,

在一開始我們會先用簡單的橫豎兩個方向來尋找我們的終點,因為斜向的路徑會有一定的差異,尤其是在地圖比較空曠的情況下,后面我們會逐漸地增加各種各樣的功能,直到我們把搜素全部解決完,

地圖編輯器

在這么大規模的尋路問題當中,我們首選需要做一個地圖編輯器,它的功能如下:

  1. 可以通過滑鼠左鍵點擊,或者點擊并拖動就可以繪制地圖
  2. 可以通過滑鼠右鍵點擊,或者點擊并拖動就可以清楚地圖
  3. 可以保存地圖的資料,重繪地圖后,還可以重新繪制出來

首先我們需要繪制我們地圖的底盤,在繪制之前我們就需要給我們的 HTML 加入 CSS,

我們的底盤是一個 100 x 100 格的,所以我們需要給每個格子一個樣式,這里我們只需要用 flex 來布局即可,假設我們每個格子都是 6px 寬高,然后依次每行排列 100 個,所以我們的 HTML 布局代碼就是如下:

<div id="container">
  <div class="cell"></div>
  <div class="cell"></div>
  <div class="cell"></div>
  <div class="cell"></div>
  ...
</div>
  • container 就是外包框
  • cell 就是里面的格子

布局的 CSS 就是如下:

body {
  background: #0f0e18;
  /* 讓整個內容 */
  display: flex;
  flex-direction: column;
  align-items: center;
}
.cell {
  display: inline-block;
  line-height: 7px;
  width: 6px;
  height: 6px;
  background-color: #2d2f42;
  border-bottom: solid 1px #0f0e18;
  border-right: solid 1px #0f0e18;
  vertical-align: bottom;
  transition: all 400ms ease;
}
#container {
  padding-bottom: 1rem;
  display: flex;
  flex-wrap: wrap;
  width: 701px;
}

/* 為了更好看,加入了 button 的樣式,可有可無!*/
button {
  background: transparent;
  /* border: none; */
  color: var(--blue-color);
  border: 1px solid aqua;
  padding: 0.5rem 1rem;
  cursor: pointer;
  transition: all 400ms ease;
}
button:hover {
  background: var(--blue-color);
  color: #333;
}

以上是布局的 CSS,但是整個底盤是需要我們用資料來構建的,這樣我們后面才能使用它來做我們尋路的問題,所以這里我們需要加入 JavaScript 來做整個渲染的程序,

HTML 的部分我們只需要加入一個 div,并且擁有一個 ID 為 container 即可:

<div id="container"></div>

實作思路

  1. 創建一個 10000 個資料的陣列,并且給里面都放入 0
  2. 從左到右,從上到下,回圈遍歷所有底盤的格子
  3. 在遍歷的同時在創建 div 元素,classcell
  4. 遍歷的程序中遇到值為 1 的就給予背景顏色 #7ceefc
  5. 添加 mousemove (滑鼠移動) 監聽
  6. 滑鼠移動監聽中有兩種情況,如果是滑鼠左鍵點擊狀態下就加入背景顏色,如果是右鍵點擊的話就是清楚當前背景顏色
  7. 最后把使用 appendChildcell 加入到 container 之中
  8. 使用 localStorage 記錄我們的底盤資料

代碼實作

// 定義 100 x 100 的底盤資料
// 使用 localStorage 獲取,如果沒有就新建一個
let map = localStorage['map'] ? JSON.parse(localStorage['map']) : Array(10000).fill(0);

// 獲取 container 元素物件
let container = document.getElementById('container');

// 遍歷所有格子
for (let y = 0; y < 100; y++) {
  for (let x = 0; x < 100; x++) {
    // 創建地圖方格
    let cell = document.createElement('div');
    cell.classList.add('cell');
    // 遇到格子的狀態是 1 的,就賦予背景顏色
    if (map[100 * y + x] == 1) cell.style.backgroundColor = 'aqua';
    // 添加滑鼠移動監聽事件
    cell.addEventListener('mousemove', () => {
      // 只有在滑鼠點擊狀態下執行
      if (mousedown) {
        if (clear) {
           // 1. 右鍵點擊時,就是清楚格子的狀態
          cell.style.backgroundColor = '';
          map[100 * y + x] = 0;
        } else {
          // 2. 左鍵點擊時,就是畫入格子的狀態
          cell.style.backgroundColor = 'aqua';
          map[100 * y + x] = 1;
        }
      }
    });
	// 加入到 container 之中
    container.appendChild(cell);
  }
}

let mousedown = false;
let clear = false;

// 滑鼠按鍵點擊時,把滑鼠點擊狀態變為 true
document.addEventListener('mousedown', e => {
  mousedown = true;
  clear = e.which === 3;
});
// 離開點擊滑鼠按鍵后,把狀態更變成 false
document.addEventListener('mouseup', () => (mousedown = false));
// 因為我們需要使用右鍵,所以要把右鍵默認打開選單禁用
document.addEventListener('contextmenu', e => e.preventDefault());

最后我們這里添加一個保存按鈕,讓我們重繪頁面的時候也會保存著我們編輯過的地圖:

<div id="container"></div>
<!-- 保存地圖資料到 localStorage -->
 <button onclick="localStorage['map'] = JSON.stringify(map)">save</button>

最后呈現的結果就是這樣的:

查看效果 | 查看代碼 | 喜歡的同學 🌟star 一下謝謝!

實作廣度優先搜索

現在我們來深入的解決尋路的問題,上面我們已經定義過尋路問題,就是 “找到一個起點和終點,然后我們需要找一條路徑,可以從起點到達終點,并且不能越過我們的邊界和墻”,

我們一眼看過去這個 100 x 100 的網格,確實會覺得這個問題不是特別地好解決,在算這個路徑的程序,中間有大量的資料需要我們去運算,但是我們可以把這個問題變得更簡單一點,

我們會到起點,從起點這個位置開展我們的思考,問一下我們自己一個問題:”從這里我們可以走到哪里呢?“

這里我們問題還是不簡單,可以走的地方可多了,因為起點的位置可能沒有任何障礙物,也不再任何邊緣上,那就是說哪里都可以呀?

好吧,那么我們再把問題的范圍再縮窄一點,“我們從起點開始,第一步能走到哪里呢?”,好這個問題的關鍵就是第一步

假設我們忽略斜著走的情況(這個我們后面再考慮進去,這里的終點是簡化問題),我們可以走往 4 個格子,如果我們逆時針的給這些位置標記一下就會得出一下的路徑標記,

就是這樣,我們就完成了尋路的第一本步驟了,接下來我們看看下一步我們又能走到哪里,如果上面標記的1234 都是可以走的,按照我們剛剛的思路,我們現在看 1 號格子又能走哪里呢?

沒有錯,按照逆時針的方式,我們可以走的格子有 567 三個格子,所以我們可以按照這樣的走法,把234 都走一遍,最后我們就會發現下面這樣的路徑:

最后我們看看下面的影片效果,看看整個程序是怎么樣的,

所以我們就是這樣,一直往外尋找我們可以走到哪些格子,直到我們找到我們的終點,這樣即可幫助我們找到從起點到終點的路線,當然在尋找的時候如果我們遇到邊界或者障礙的時候就會跳過,因為這些都是走不過去的,

這個思路的問題,很容易讓大家想到遞回,但是這個方法并不適合使用遞回來表達,如果我們用遞回來表達,我們肯定是從找到 1 這個格子之后,就會開始展開找 1 周圍的格子了,那么 5,6,7 就會在 2,3,4 之前被執行了(因為是遞回,我們會逐層展開的),

所以說,如果我們默認用遞回的方式來表達,這個尋路的方式就會變成 “深度優先搜索”,但是對尋路問題來說深度優先搜索是不好的,“廣度優先搜索”才是好的,

為什么尋路廣度優先搜索比深度優先搜索好呢?

我們來舉個例子,就更好理解了!

假設我們現在走入了一個迷宮,前方有成千上萬個分叉口,這個時候我們有兩種搜索出路的方法

方案一

獨自一人,選擇左邊或者右邊一直走那邊的每一條分支,并且都一直走到底直到走到有死胡同了,然后就回頭走另外那邊的分支,就這樣一直走一直切換分支,直到我們找到出口的分支為止,運氣好的話,很快就找到出口了,運氣不好的話,所有分支都基本走個遍才能找到出口,

方案二

我們是獨自一個人,但是我們有 “分身術”,當我們遇到分叉口的時候我們是可以每一個分叉口都去嘗試一遍,比如 A,B,C 三個分叉口,我們可以 A 路線走一步,然后 B 路線也走一步,最后 C 路線也走一步,這里我們步伐整齊統一,就有點像我們上面的動態圖中一樣,一直往所有分叉口往外擴展尋找路線,這樣我們就可以在 N 個分叉口依次一步一步往前走,直到找到出口,這樣的方式等同于我們依次的在尋找每一個分叉口,這樣的搜索顯然是比第一個好的,(我不知道你們,但是就有分身術這個技能我就覺得已經在尋路這個問題中贏了!

回歸到演算法的話,方案一其實就是“深度優先搜索”,而方案二就是"廣度優先搜索"!顯然在尋路這種問題是廣度優先搜索更為高效!

實作廣度優先搜索代碼

玩過走迷宮的同學肯定都會想到,在走迷宮的時候,我們都會給我們走過的路徑標記,這樣我們才知道我們走過哪里,最后通過這些記錄找到可以到達終點的路徑,我們這個尋路的問題也不例外,根據我們上面的分析,我們從起點就開始往外擴展尋找可以走的格子,每當我們找到一個可走的格子,我們都需要記錄起來,

在演算法當中我們都會把資料加入一個 “集合” 里面,而這個集合就是所有搜索演算法的 “靈魂”,所有搜索演算法的差異,完全就是在于這個 “集合 (queue)” 里面,

queue 是一種資料結構,我們也叫它為 “佇列”,它的特性就是 先進先出一邊進另外一邊出,實際效果與下圖:

那么 JavaScript 中有沒有佇列這樣的資料結構呢?有!JavaScript 中的陣列就是天然的佇列 (Queue),同時 JavaScript 中的陣列也是天然的堆疊 (Stack)

JavaScript 的陣列有 2 組常用的處理方法 shifunshift,以及 pushpop, 但是如果我們混搭來使用他們的話,就會讓我們的陣列變成不一樣的資料結構,

  • pushshift 或者 popunshift 結合使用,那么陣列就是一個佇列 (Queue)
  • pushpop 結合使用那么,陣列就是一個堆疊 (Stack) (當然 shif 和 unshif 也是可以的,但是我們一般不會用這個組合來做堆疊,因為我們考慮到 JavaScript 的陣列的實作,這樣使用性能會變低,)

這些理論知識都搞懂了,我們就可以開始整理一下撰寫代碼的思路了:

  1. 我們需要宣告一個佇列,也就是宣告一個 queue,并且把我們的起點默認放入佇列中,(JavaScript 中我們使用陣列即可)
  2. 撰寫一個 “入隊” 的方法,條件是如果遇到邊緣或者障礙就直接跳出方法,這些都是不可走的格子所以不會加入我們的佇列,
  3. 如果沒有遇到以上情況,我們就可以先把可以走的格子在我們的 map(在實作我們的地圖資料的時候宣告的一個陣列)中記錄一個狀態,這里我們可以使用 2, 代表這個格子我們已經走過了,這里我們加入一個標記,
  4. 回圈我們佇列中可以走的格子,這里的主要目標就是把所有記錄了可以走的格子都找到它的 ,并且把這些可走的格子都入佇列,然后進入下一個回圈時就會去找這些新入佇列的格子可以走到哪里,然后把后面找到的格子再次入佇列,以此類推,
  5. 這個回圈有一個截止條件,那就是如果我們在回圈每個格子的時候,找到了終點的格子,我們就可以直接回傳 true,代表我們已經找到終點了,

好思路又了,我們馬上來看看代碼實作:

// 上一部分的代碼,這里就忽略了...
// 只要在上部分的代碼后面改造這部分即可

// 離開點擊滑鼠按鍵后,把狀態更變成 false
document.addEventListener('mouseup', () => (mousedown = false));
// 因為我們需要使用右鍵,所以要把右鍵默認打開選單禁用
document.addEventListener('contextmenu', e => e.preventDefault());

/**
  * 尋路方法
  * @param {Array} map 地圖資料
  * @param {Array} start 起點 例如:[0, 0]
  * @param {Array} end 終點 例如:[50, 50]
  * @return Boolean
  */
function path(map, start, end) {
  var queue = [start];
  
  function insert(x, y) {
    // 到達底盤邊緣,直接停止
    if (x < 0 || x >= 100 || y < 0 || y >= 100) return;
    // 遇到地圖的墻,也停止
    if (map[y * 100 + x]) return;
    // 標記可以走的路的格子的狀態為 2
    map[y * 100 + x] = 2;
    // 把可走的路推入佇列
    queue.push([x, y]);
  }

  // 回圈格子 4 邊的格子
  while (queue.length) {
    let [x, y] = queue.shift();
    console.log(x, y);

    // 遇到了終點位置就可以回傳了
    if (x === end[0] && y === end[1]) return true;

    // 把上下左右推入佇列
    insert(x - 1, y);
    insert(x, y - 1);
    insert(x + 1, y);
    insert(x, y + 1);
  }

  return false;
}

為了可以除錯看看,我們的代碼是否正確,我們來加一個按鈕,并且讓它可以執行我們這個尋路的方法 path

<div id="container"></div>
<!-- 保存地圖資料到 localStorage -->
<div>
  <button onclick="localStorage['map'] = JSON.stringify(map)">save</button>
  <button onclick="path(map, [0,0], [50,50])">find</button>
</div>

這里我們的按鈕會執行一個尋路方法,設定了起點是 x = 0,y = 0 的位置,終點是 x = 50,y = 50 的位置,點擊這個按鈕之后,我們就可以在瀏覽器的除錯工具中的 console 之中看到尋路程序中走過的 x 和 y,如果我們的代碼是對的話,最后我們會看到在 50, 50 的時候就會停止運行了,

查看效果 | 查看代碼 | 喜歡的同學 🌟star 一下謝謝!

加入 Async 和 Await 來方便除錯和展示

上一個代碼中,其實已經實作了尋路演算法的主體部分了,但是里面還是有一些問題的:

  1. 演算法雖然最侄訓傳了我們終點的位置,并且回傳了 true,看起來是符合了我們的預期的,但是它的正確性我們不太好保證,所以我們希望有一個可視化的效果來觀察這個尋路演算法的程序
  2. 我們是找到一條路徑,這個最終的路徑我們還沒有把它找出來

這些問題我們下來幾個步驟中會一步一步來解決,

如何有看我們的 《TicTacToe 三子棋》的編程與演算法練習的文章的話,我們里面有講到使用 asyncawait ,來讓函式中間可插入一些異步的操作,

這部分的代碼我們做了一些代碼的改造:

  1. path() 函式改為 async 函式
  2. insert() 函式改為 async 函式
  3. 因為 insert() 編程了異步函式,所以我們 while() 回圈中的 insert 呼叫都需要在前面加入 await
  4. 同時我們需要加入一個等待函式sleep(),它必須回傳一個 promise
  5. 在我們入佇列之后,在改變當前格子狀態為 2 之前,我們會對 DOM 元素中的格子的背景顏色進行改變,這樣我們就可以看到尋路的程序
  6. 因為我們需要看到這個程序,所以每一次入佇列的時候我們需要給一個 1 秒的等待時間,這個就是使用 asyncawait 的好處,在加入這個背景顏色之前,我們就可以加入一個 await sleep(1),這樣入佇列和改變格子背景顏色之前就會有 1 秒的延遲,

好廢話少說,我們來看看代碼有什么變化:

// 上一部分的代碼,這里就忽略了...
// 只要在上部分的代碼后面改造這部分即可

// 離開點擊滑鼠按鍵后,把狀態更變成 false
document.addEventListener('mouseup', () => (mousedown = false));
// 因為我們需要使用右鍵,所以要把右鍵默認打開選單禁用
document.addEventListener('contextmenu', e => e.preventDefault());

/**
     * 等待函式
     * @param {Integer} t 時間 (秒)
     * @return Promise
     */
function sleep(t) {
  return new Promise(function (resolve) {
    setTimeout(resolve, t);
  });
}

/**
     * 尋路方法 (異步)
     * @param {Array} map 地圖資料
     * @param {Array} start 起點 例如:[0, 0]
     * @param {Array} end 終點 例如:[50, 50]
     * @return Boolean
     */
async function path(map, start, end) {
  var queue = [start];

  /**
       * 入隊方法 (異步)
       * @param {Integer} x
       * @param {Integer} y
       */
  async function insert(x, y) {
    // 到達底盤邊緣,直接停止
    if (x < 0 || x >= 100 || y < 0 || y >= 100) return;
    // 遇到地圖的墻,也停止
    if (map[y * 100 + x]) return;
    // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化
    await sleep(1);
    // 給搜索到的路徑的格子加上背景顏色
    container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';
    // 標記可以走的路的格子的狀態為 2
    map[y * 100 + x] = 2;
    // 把可走的路推入佇列
    queue.push([x, y]);
  }

  // 回圈格子 4 邊的格子
  while (queue.length) {
    let [x, y] = queue.shift();
    // console.log(x, y);

    // 遇到了終點位置就可以回傳了
    if (x === end[0] && y === end[1]) return true;

    // 把上下左右推入佇列
    await insert(x - 1, y);
    await insert(x, y - 1);
    await insert(x + 1, y);
    await insert(x, y + 1);
  }

  return false;
}

最后我們執行后的結果:

查看效果 | 查看代碼 | 喜歡的同學 🌟star 一下謝謝!

處理路徑問題

上一步我們用一個影片,讓我們特別清晰的去理解整個尋路演算法的程序,但是上一步提到的第二個問題,我們目前是還沒有解決的,所以這里我們就是要找到最終的路徑是什么,我們最終通過尋路演算法之后,我們怎么獲得一個路徑是可以從起點到達終點的呢?

其實處理這個路徑的問題也是非常的簡單的,我們先看看看我們之前講過的尋路的思路:

通過上面這個圖,我們已經都還記得,在我們訪問每一個格子的時候,我們都往外擴展找到可走的格子有哪些,這個程序當中,我們都是知道每一個被擴展到的格子都是由前一個給自擴展過來的,換句話說,每一個格子都是知道我們是從那個給自擴展過來的,

比如說 567 這三個格子都是從 1 這個格子擴展過來的,既然我們知道每個給自上一步的來源,是不是我們可以在每一個給自記錄上一個來源的格子呢?所以在代碼中我們是不是可以在入隊的時候,就記錄上一個給自的 x,y軸呢?

那么我們就產生一個想法,通過這個記錄,我們怎么尋找最終的路徑呢?

我們假設終點是在 8 這個位置,我們過來 8 這個點的時候是從我們的起點一步一步擴展出來的,那我們反過來通過記錄了每一個格子的前驅格子,就可以一步一步收碩訓到起點呢?

那就是說,從 8 開始收,8 是從 2 走過來的,而 2 就是從起點 擴展過來的,最終我們就找到起點了,如果我們在收縮的程序記錄著收縮時候訪問到的格子,最終這些格子就是從起點到終點的整條路徑了!是不是很神奇?!

其實也可以理解為一個 “原路回傳” 的效果!

先不要雞凍,穩住!我們接下來看看代碼是如何處理的:

  1. 其實基本上我們的代碼沒有太多的改變
  2. 首先就是在 while 回圈當中的 insert() 呼叫的時候添加了上一個坐標的傳參
  3. 這里我們順便也把橫向的可走的格子也加入到佇列中
  4. 這里因為我們需要記錄所有格子的前驅坐標,所以我們需要宣告一個 table 的變數存放這個資料
  5. 在我們進入佇列之前,我們就把當前入佇列的格子的值存為上一個格子的坐標(這個為了我們后面方便收縮是找到整個路徑)
  6. 最后在 while 回圈中,當我們遇到終點的 x 和 y 的時候,我們加入一段 while 回圈
  7. 這個 while 就是往回一直走,知道我們找到起點位置,在往回走的同時,把每一個經過的格子的背景改為另外一個背景顏色,這樣就可以在我們的地圖上畫出一個路徑了!

好思路清晰,我們來上一波代碼吧!

// 上一部分的代碼,這里就忽略了...
// 只要在上部分的代碼后面改造這部分即可

/**
     * 尋路方法 (異步)
     * @param {Array} map 地圖資料
     * @param {Array} start 起點 例如:[0, 0]
     * @param {Array} end 終點 例如:[50, 50]
     * @return Boolean
     */
function sleep(t) {
  return new Promise(function (resolve) {
    setTimeout(resolve, t);
  });
}

/**
     * 入隊方法 (異步)
     * @param {Integer} x
     * @param {Integer} y
     */
async function findPath(map, start, end) {
  // 創建一個記錄表格
  let table = Object.create(map);
  let queue = [start];

  /**
       * 入隊方法 (異步)
       * @param {Integer} x
       * @param {Integer} y
       * @param {Array} pre 上一個格子的坐標:[x,y]
       */
  async function insert(x, y, pre) {
    // 到達底盤邊緣,直接停止
    if (x < 0 || x >= 100 || y < 0 || y >= 100) return;
    // 遇到地圖的墻,也停止
    if (table[y * 100 + x]) return;
    // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化
    // await sleep(1);
    // 給搜索到的路徑的格子加上背景顏色
    container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';
    // 標記走過的格子的值,標記為上一個格子的 x,y 位置
    table[y * 100 + x] = pre;
    // 把可走的路推入佇列
    queue.push([x, y]);
  }

  // 回圈格子 4 邊的格子
  while (queue.length) {
    let [x, y] = queue.shift();
    // console.log(x, y);

    // 遇到了終點位置就可以回傳了
    if (x === end[0] && y === end[1]) {
      let path = [];

      // 往回走,直到走到起點
      // 這樣就能畫出最佳路徑了
      while (x != start[0] || y != start[1]) {
        path.push(map[y * 100 + x]);
        [x, y] = table[y * 100 + x];
        await sleep(1);
        container.children[y * 100 + x].style.backgroundColor = 'fuchsia';
      }

      return path;
    }

    // 把上下左右推入佇列
    await insert(x - 1, y, [x, y]);
    await insert(x, y - 1, [x, y]);
    await insert(x + 1, y, [x, y]);
    await insert(x, y + 1, [x, y]);

    // 把 4 個 斜邊推入佇列
    await insert(x - 1, y - 1, [x, y]);
    await insert(x + 1, y - 1, [x, y]);
    await insert(x - 1, y + 1, [x, y]);
    await insert(x + 1, y + 1, [x, y]);
  }

  return null;
}

注意:這里我們把原來的 path 函式名改為了 findPath,因為在尋找路徑的 while 回圈里面我們用了 path 做為記錄路徑的變數,這里還需要注意的就是,我們的 find 按鈕里面的函式呼叫也需要一并修改哦,

<!-- 保存地圖資料到 localStorage -->
<div>
  <button onclick="localStorage['map'] = JSON.stringify(map)">save</button>
  <button onclick="findPath(map, [0,0], [50,50])">find</button>
</div>

運行最終效果如下:

查看效果 | 查看代碼 | 喜歡的同學 🌟star 一下謝謝!

啟發式尋路(A*)

到這里我們已經完成了整個廣度優先尋路的演算法,但是廣搜式尋路是不是最好的尋路方案呢?其實并不是的!

通過各位數學科學家的努力下,他們證明了一件事情,我們是可以有一種方法能夠加速尋路的,通過用這個方法我們不需要使用一個非常傻的方式來挨個去找,

這種尋路的方式叫做 “啟發式尋路

啟發式尋路就是用一個函式去判斷這些點擴展的優先級,只要我們判斷好了優先級,我們就可以有目的的去驗者格子的方向做優先找路,

“但是這個找到的路徑是不是最佳路徑呀?說的那么神奇的,“ 說實話,這個我們普通人的思路就排不上用場了,但是數學家證明了一件事情,只要我們使用啟發式函式來估計值,并且這些值能夠一定小于當前點到終點的路徑的長度,這樣就一定能找到最優路徑,

這種能找到最優路徑的啟發式尋路,在計算機里面我們叫它做 “A*”,這里面的 A 代表著一種不一定能找到最優路徑的啟發式尋路,所以 A* 就是 A 尋路的一個特例,是一種可以找到最佳路徑的一種演算法,

要實作這個啟發式尋路,其實我們是不需要改過多我們 findPath()函式里面的代碼的,我們要改的是我們存盤的資料結構,也就是我們的 queue 佇列,

我們要把 queue 的先進先出變成一個優先佇列 (Prioritized Queue)

所以我們需要構建一個 Sorted 類,這個類有幾個作業:

  1. 這個類可以存盤我們之前 queue 佇列里面的資料
  2. 可以支持傳入排序函式(也就是和我們 array sort 函式一樣的功能,可以傳入一個排序規則函式,也叫 compare 函式)
  3. 在我們通過這個類獲取值的時候給我們資料里面的最小值
  4. 在我們插入資料的時候不需要排序,只是單純的保存
  5. 每次去除資料的時候,可以把輸出的資料在類的資料里面洗掉掉
  6. 這樣我們就可以一直在里面獲取資料,知道沒有資料為止
  7. 最后加入一個可以獲取當前資料的長度(這個在我們的尋路演算法中需要用到)

這里我們用一個非常 “土鱉” 的陣列來實作這個 Sorted 類,但是在計算機當中,我們還有很多其他方式可以實作這一種類,比如說 winner tree堆 (heap)排序二叉樹 等很多的不同思路來實作有序的資料結構,

好廢話少說,我們來看看怎么實作這個資料結構:

/** 排序資料類 */
class Sorted {
  constructor(data, compare) {
    this.data = data.slice();
    this.compare = compare || ((a, b) => a - b);
  }
  take() {
    // 考慮到 null 也是可以參與比較的,所以這里回傳 null 是不合適的
    if (!this.data.length) return;
    // 記錄最小的值
    // 默認第一個位置為最小值
    let min = this.data[0];
    // 記錄最小值的位置
    let minIndex = 0;

    // 開始比較陣列里面的所有值,找到更小的值,就記錄為 min
    // 同時記錄最小值,和最小值的位置
    for (let i = 1; i < this.data.length; i++) {
      if (this.compare(this.data[i], min) < 0) {
        min = this.data[i];
        minIndex = i;
      }
    }

    // 現在我們要把最小值拿出去了,所以要在我們當前的資料中移除
    // 這里我們不考慮使用 splice,因為 splice 移除會涉及陣列內的元素都要往前挪動
    // 這樣 splice 就會有一個 O(N) 的時間復雜度
    // 這里我們用一個小技巧,把陣列里面最后一位的值挪動到當前發現最小值的位置
    // 最后使用 pop 把最后一位資料移除
    this.data[minIndex] = this.data[this.data.length - 1];
    this.data.pop();
    // 最后把最小值輸出
    return min;
  }
  give(value) {
    this.data.push(value);
  }
  get length() {
    return this.data.length;
  }
}

有了這個 Sorted 的資料結構之后,我們就可以用來解決我們的尋路問題,讓我們可以找到最佳的路徑了,

加入這個最佳路徑的邏輯,我們需要改到一下幾個點:

  1. 首先改寫我們的存盤資料的 queue,使用我們 Sorted 的排序資料結構
  2. 撰寫一個 distance() 函式來運算任何一個格子與終點格子的直線距離
  3. 把所有入佇列和出佇列的呼叫改為使用 Sorted 類里面的 take 取值 和 give 插入值函式
  4. 其他地方基本就沒有什么改變了

接下來我們來看看代碼是怎么實作的:

// 上一部分的代碼,這里就忽略了...
// 只要在上部分的代碼后面改造這部分即可

/**
     * 尋路方法 (異步)
     * @param {Array} map 地圖資料
     * @param {Array} start 起點 例如:[0, 0]
     * @param {Array} end 終點 例如:[50, 50]
     * @return Boolean
     */
async function findPath(map, start, end) {
  // 創建一個記錄表格
  let table = Object.create(map);
  let queue = new Sorted([start], (a, b) => distance(a) - distance(b));

  async function insert(x, y, pre) {
    // 到達底盤邊緣,直接停止
    if (x < 0 || x >= 100 || y < 0 || y >= 100) return;
    // 遇到地圖的墻,也停止
    if (table[y * 100 + x]) return;
    // 加入 30 毫秒的停頓,讓我們可以看到 UI 上面的變化
    await sleep(1);
    // 給搜索到的路徑的格子加上背景顏色
    container.children[y * 100 + x].style.backgroundColor = 'DARKSLATEBLUE';
    // 標記走過的格子的值,標記為上一個格子的 x,y 位置
    table[y * 100 + x] = pre;
    // 把可走的路推入佇列
    queue.give([x, y]);
  }

  /**
       * 獲取格與格之前的距離
       * @param {Array} point 當前格子的坐標:[x,y]
       */
  function distance(point) {
    // 使用三角形 x^2 + y^2 = z^2 的方式來計算距離
    return (point[0] - end[0]) ** 2 + (point[1] - end[1]) ** 2;
  }

  // 回圈格子 4 邊的格子
  while (queue.length) {
    let [x, y] = queue.take();
    // console.log(x, y);

    // 遇到了終點位置就可以回傳了
    if (x === end[0] && y === end[1]) {
      let path = [];

      // 往回走,直到走到起點
      // 這樣就能畫出最佳路徑了
      while (x != start[0] || y != start[1]) {
        path.push(map[y * 100 + x]);
        [x, y] = table[y * 100 + x];
        container.children[y * 100 + x].style.backgroundColor = 'fuchsia';
      }

      return path;
    }

    // 把上下左右推入佇列
    await insert(x - 1, y, [x, y]);
    await insert(x, y - 1, [x, y]);
    await insert(x + 1, y, [x, y]);
    await insert(x, y + 1, [x, y]);

    // 把 4 個 斜邊推入佇列
    await insert(x - 1, y - 1, [x, y]);
    await insert(x + 1, y - 1, [x, y]);
    await insert(x - 1, y + 1, [x, y]);
    await insert(x + 1, y + 1, [x, y]);
  }

  return null;
}

這個最終的運行效果如下:

查看效果 | 查看代碼 | 喜歡的同學 🌟star 一下謝謝!


我是來自《技術銀河》的三鉆:“學習是為了成長,成長是為了不退步,堅持才能成功,失敗只是因為沒有堅持,同學們加油哦!下期見!”


推薦專欄

小伙伴們可以查看或者訂閱相關的專欄,從而集中閱讀相關知識的文章哦,

  • 📖 《前端進階》 — 這里包含的文章學習內容需要我們擁有 1-2 年前端開發經驗后,選擇讓自己升級到高級前端工程師的學習內容(這里學習的內容是對應阿里 P6 級別的內容),

  • 📖 《資料結構與演算法》 — 到了如今,如果想成為一個高級開發工程師或者進入大廠,不論崗位是前端、后端還是AI,演算法都是重中之重,也無論我們需要進入的公司的崗位是否最后是做演算法工程師,前提面試就需要考演算法,

  • 📖 《FCC前端集訓營》 — 根據FreeCodeCamp的學習課程,一起深入淺出學習前端,穩固前端知識,一起在FreeCodeCamp獲得證書

  • 📖 《前端星球》 — 以實戰為線索,深入淺出前端多維度的知識點,內含有多方面的前端知識文章,帶領不懂前端的童鞋一起學習前端,在前端開發路上童鞋一起燃起心中那團火🔥

三鉆 CSDN認證博客專家 前端 Vue React
—— 起步于PHP,一入前端深似海,最后愛上了前端,Vue、React使用者,專于Web、移動端開發,特別關注產品和UI設計,專心、專注、專研,與同學們一起終身學習,關注我的微信公眾號《技術銀河》有更多最新知識文章與同學們分享,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/205258.html

標籤:區塊鏈

上一篇:Unity 3D 完成地形凹陷、水面、成品樹

下一篇:C語言水果忍者修改器(入門版)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • JAVA使用 web3j 進行token轉賬

    最近新學習了下區塊鏈這方面的知識,所學不多,給大家分享下。 # 1. 關于web3j web3j是一個高度模塊化,反應性,型別安全的Java和Android庫,用于與智能合約配合并與以太坊網路上的客戶端(節點)集成。 # 2. 準備作業 jdk版本1.8 引入maven <dependency> < ......

    uj5u.com 2020-09-10 03:03:06 more
  • 以太坊智能合約開發框架Truffle

    前言 部署智能合約有多種方式,命令列的瀏覽器的渠道都有,但往往跟我們程式員的風格不太相符,因為我們習慣了在IDE里寫了代碼然后打包運行看效果。 雖然現在IDE中已經存在了Solidity插件,可以撰寫智能合約,但是部署智能合約卻要另走他路,沒辦法進行一個快捷的部署與測驗。 如果團隊管理的區塊節點多、 ......

    uj5u.com 2020-09-10 03:03:12 more
  • 谷歌二次驗證碼成為區塊鏈專用安全碼,你怎么看?

    前言 谷歌身份驗證器,前些年大家都比較陌生,但隨著國內互聯網安全的加強,它越來越多地出現在大家的視野中。 比較廣泛接觸的人群是國際3A游戲愛好者,游戲盜號現象嚴重+國外賬號安全應用廣泛,這類游戲一般都會要求用戶系結名為“兩步驗證”、“雙重驗證”等,平臺一般都推薦用谷歌身份驗證器。 后來區塊鏈業務風靡 ......

    uj5u.com 2020-09-10 03:03:17 more
  • 密碼學DAY1

    目錄 ##1.1 密碼學基本概念 密碼在我們的生活中有著重要的作用,那么密碼究竟來自何方,為何會產生呢? 密碼學是網路安全、資訊安全、區塊鏈等產品的基礎,常見的非對稱加密、對稱加密、散列函式等,都屬于密碼學范疇。 密碼學有數千年的歷史,從最開始的替換法到如今的非對稱加密演算法,經歷了古典密碼學,近代密 ......

    uj5u.com 2020-09-10 03:03:50 more
  • 密碼學DAY1_02

    目錄 ##1.1 ASCII編碼 ASCII(American Standard Code for Information Interchange,美國資訊交換標準代碼)是基于拉丁字母的一套電腦編碼系統,主要用于顯示現代英語和其他西歐語言。它是現今最通用的單位元組編碼系統,并等同于國際標準ISO/IE ......

    uj5u.com 2020-09-10 03:04:50 more
  • 密碼學DAY2

    ##1.1 加密模式 加密模式:https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html ECB ECB : Electronic codebook, 電子密碼本. 需要加密的訊息按照塊密碼的塊大小被分為數個塊,并對每個塊進 ......

    uj5u.com 2020-09-10 03:05:42 more
  • NTP時鐘服務器的特點(京準電子)

    NTP時鐘服務器的特點(京準電子) NTP時鐘服務器的特點(京準電子) 京準電子官V——ahjzsz 首先對時間同步進行了背景介紹,然后討論了不同的時間同步網路技術,最后指出了建立全球或區域時間同步網存在的問題。 一、概 述 在通信領域,“同步”概念是指頻率的同步,即網路各個節點的時鐘頻率和相位同步 ......

    uj5u.com 2020-09-10 03:05:47 more
  • 標準化考場時鐘同步系統推進智能化校園建設

    標準化考場時鐘同步系統推進智能化校園建設 標準化考場時鐘同步系統推進智能化校園建設 安徽京準電子科技官微——ahjzsz 一、背景概述隨著教育事業的快速發展,學校建設如雨后春筍,隨之而來的學校教育、管理、安全方面的問題成了學校管理人員面臨的最大的挑戰,這些問題同時也是學生家長所擔心的。為了讓學生有更 ......

    uj5u.com 2020-09-10 03:05:51 more
  • 位元幣入門

    引言 位元幣基本結構 位元幣基礎知識 1)哈希演算法 2)非對稱加密技術 3)數字簽名 4)MerkleTree 5)哪有位元幣,有的是UTXO 6)位元幣挖礦與共識 7)區塊驗證(共識) 總結 引言 上一篇我們已經知道了什么是區塊鏈,此篇說一下區塊鏈的第一個應用——位元幣。其實先有位元幣,后有的區塊 ......

    uj5u.com 2020-09-10 03:06:15 more
  • 北斗對時服務器(北斗對時設備)電力系統應用

    北斗對時服務器(北斗對時設備)電力系統應用 北斗對時服務器(北斗對時設備)電力系統應用 京準電子科技官微(ahjzsz) 中國北斗衛星導航系統(英文名稱:BeiDou Navigation Satellite System,簡稱BDS),因為是目前世界范圍內唯一可以大面積提供免費定位服務的系統,所以 ......

    uj5u.com 2020-09-10 03:06:20 more
最新发布
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:46:47 more
  • Hyperledger Fabric 使用 CouchDB 和復雜智能合約開發

    在上個實驗中,我們已經實作了簡單智能合約實作及客戶端開發,但該實驗中智能合約只有基礎的增刪改查功能,且其中的資料管理功能與傳統 MySQL 比相差甚遠。本文將在前面實驗的基礎上,將 Hyperledger Fabric 的默認資料庫支持 LevelDB 改為 CouchDB 模式,以實作更復雜的資料... ......

    uj5u.com 2023-04-16 07:28:31 more
  • .NET Core 波場鏈離線簽名、廣播交易(發送 TRX和USDT)筆記

    Get Started NuGet You can run the following command to install the Tron.Wallet.Net in your project. PM> Install-Package Tron.Wallet.Net 配置 public reco ......

    uj5u.com 2023-04-14 08:08:00 more
  • DKP 黑客分析——不正確的代幣對比率計算

    概述: 2023 年 2 月 8 日,針對 DKP 協議的閃電貸攻擊導致該協議的用戶損失了 8 萬美元,因為 execute() 函式取決于 USDT-DKP 對中兩種代幣的余額比率。 智能合約黑客概述: 攻擊者的交易:0x0c850f,0x2d31 攻擊者地址:0xF38 利用合同:0xf34ad ......

    uj5u.com 2023-04-07 07:46:09 more
  • Defi開發簡介

    Defi開發簡介 介紹 Defi是去中心化金融的縮寫, 是一項旨在利用區塊鏈技術和智能合約創建更加開放,可訪問和透明的金融體系的運動. 這與傳統金融形成鮮明對比,傳統金融通常由少數大型銀行和金融機構控制 在Defi的世界里,用戶可以直接從他們的電腦或移動設備上訪問廣泛的金融服務,而不需要像銀行或者信 ......

    uj5u.com 2023-04-05 08:01:34 more
  • solidity簡單的ERC20代幣實作

    // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; import "hardhat/console.sol"; //ERC20 同質化代幣,每個代幣的本質或性質都是相同 //ETH 是原生代幣,它不是ERC20代幣, ......

    uj5u.com 2023-03-21 07:56:29 more
  • solidity 參考型別修飾符memory、calldata與storage 常量修飾符C

    在solidity語言中 參考型別修飾符(參考型別為存盤空間不固定的數值型別) memory、calldata與storage,它們只能修飾參考型別變數,比如字串、陣列、位元組等... memory 適用于方法傳參、返參或在方法體內使用,使用完就會清除掉,釋放記憶體 calldata 僅適用于方法傳參 ......

    uj5u.com 2023-03-08 07:57:54 more
  • solidity注解標簽

    在solidity語言中 注釋符為// 注解符為/* 內容*/ 或者 是 ///內容 注解中含有這幾個標簽給予我們使用 @title 一個應該描述合約/介面的標題 contract, library, interface @author 作者的名字 contract, library, interf ......

    uj5u.com 2023-03-08 07:57:49 more
  • 評價指標:相似度、GAS消耗

    【代碼注釋自動生成方法綜述】 這些評測指標主要來自機器翻譯和文本總結等研究領域,可以評估候選文本(即基于代碼注釋自動方法而生成)和參考文本(即基于手工方式而生成)的相似度. BLEU指標^[^?88^^?^]^:其全稱是bilingual evaluation understudy.該指標是最早用于 ......

    uj5u.com 2023-02-23 07:27:39 more
  • 基于NOSTR協議的“公有制”版本的Twitter,去中心化社交軟體Damus

    最近,一個幽靈,Web3的幽靈,在網路游蕩,它叫Damus,這玩意詮釋了什么叫做病毒式營銷,滑稽的是,一個Web3產品卻在Web2的產品鏈上瘋狂傳銷,各方大佬紛紛為其背書,到底發生了什么?Damus的葫蘆里,賣的是什么藥? 注冊和簡單實用 很少有什么產品在用戶注冊環節會有什么噱頭,但Damus確實出 ......

    uj5u.com 2023-02-05 06:48:39 more