Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 題解。git
這裏是第 169 期的第 3 題,也是題目列表中的第 1306 題 -- 『Jump Game III』github
Given an array of non-negative integers arr
, you are initially positioned at start
index of the array. When you are at index i
, you can jump to i + arr[i]
or i - arr[i]
, check if you can reach to any index with value 0.shell
Notice that you can not jump outside of the array at any time.數組
Example 1:數據結構
Input: arr = [4,2,3,0,3,1,2], start = 5 Output: true Explanation: All possible ways to reach at index 3 with value 0 are: index 5 -> index 4 -> index 1 -> index 3 index 5 -> index 6 -> index 4 -> index 1 -> index 3
Example 2:ide
Input: arr = [4,2,3,0,3,1,2], start = 0 Output: true Explanation: One possible way to reach at index 3 with value 0 is: index 0 -> index 4 -> index 1 -> index 3
Example 3:post
Input: arr = [3,0,2,1,2], start = 2 Output: false Explanation: There is no way to reach at index 1 with value 0.
Constraints:優化
1 <= arr.length <= 5 * 10^4 0 <= arr[i] < arr.length 0 <= start < arr.length
MEDIUMspa
題目的內容是一個小遊戲,能夠想象這樣一個場景:code
那麼回到題目中,紙牌和背後的數字是一個給定的由非負整數組成的數組,起始位置是給定的一個下標,咱們須要返回 true
或者 false
。
咱們先想想,若是是玩這個遊戲的話,咱們會怎麼玩呢?因爲出發點是肯定的,而結束的點不肯定,由於可能會有多個 0 的存在,因此咱們能夠從出發點開始不斷的作嘗試。基於此咱們能夠獲得兩種思路:
其實上述兩種思路也就對應了兩種很常見的遍歷思路,即廣度優先遍歷和深度優先遍歷。這部份內容我會在數據結構的新坑中詳細介紹。
基於上面的第一種思路,咱們用到了一個 visited
集合來判斷是否已經訪問過,從而規避循環。同時咱們用一個 queue
來保存全部記錄節點,方便基於延伸。具體代碼以下:
const canReach = (arr, start) => { const visited = new Set(); const queue = [start]; for (let len = 0, max = arr.length; len < queue.length; ++len) { const idx = queue[len]; if (visited.has(idx)) continue; if (arr[idx] === 0) return true; visited.add(idx); idx + arr[idx] < max && queue.push(idx + arr[idx]); idx - arr[idx] >= 0 && queue.push(idx - arr[idx]); } return false; };
這裏算是一個很是常見的廣度優先遍歷的實現模板了,不過具體到這道題其實還能夠再優化一下。咱們能夠注意到題目的限制條件裏,arr
的每一個值取值範圍是 [0, arr.length]
。基於此,咱們能夠經過賦值爲一個範圍外的特殊值來標識已經訪問過,從而去掉 visited
集合的使用。我這裏直接使用了 -1
做爲特殊值,具體代碼以下:
const canReach = (arr, start) => { const queue = [start]; for (let len = 0, max = arr.length; len < queue.length; ++len) { const idx = queue[len]; if (arr[idx] === -1) continue; if (arr[idx] === 0) return true; idx + arr[idx] < max && queue.push(idx + arr[idx]); idx - arr[idx] >= 0 && queue.push(idx - arr[idx]); arr[idx] = -1; } return false; };
基於上面的第二種思路,咱們能夠經過基於遞歸的方式來實現不斷的層層深刻,以及遇到循環後回退。具體代碼以下:
const canReach = (arr, start) => { const val = arr[start]; if (val === 0) return true; if (val === -1) return false; arr[start] = -1; return (start - val >= 0 && canReach(arr, start - val)) || (start + val < arr.length && canReach(arr, start + val)); };
遞歸實現的一個很明顯的好處就是,看起來代碼量更少了。特別適合懶懶的張小豬本豬,哈哈哈哈 >.<
另外值得注意的一點是,咱們這裏沒有用到那個額外的 queue
去記錄。那麼是否咱們的額外空間複雜度就是 O(1) 了呢?其實並非的。由於咱們實際上是經過遞歸的調用棧來變相的記錄了路徑和回退過程。因此,在考慮額外的空間複雜度的時候,咱們須要把遞歸的調用棧考慮進去,也就是說這裏的空間複雜度其實仍是 O(n)。
最後,利用到參數的默認值能夠進行運算、邏輯運算符、以及逗號表達式,咱們能夠把上述代碼變成一行實現,具體以下:
const canReach = (arr, start, val = arr[start]) => val === 0 || (arr[start] = -1, val !== -1) && ((start - val >= 0 && canReach(arr, start - val)) || (start + val < arr.length && canReach(arr, start + val)));
友情提示:生產環境中不要這樣寫哈,被打死我不負責,哈哈哈哈。
這道題中咱們能夠了解到深度優先遍歷和廣度優先遍歷這兩種遍歷思路,以及在具體的場景中咱們還能夠作的一些小優化。這兩種遍歷方式在之後應該會挺常見的。最後再強調一下,一行的那個寫法真的會被同事打死的,哈哈哈哈嗝 >.<