<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          寫給零基礎(chǔ)的前端算法入門指南,摸魚刷一刷【遞歸與回溯篇】

          共 42897字,需瀏覽 86分鐘

           ·

          2021-07-16 17:42

          前言

          原本打算通過一篇文章介紹一下,推薦一下自己的刷題方式和刷題路線,得到一些伙伴的反饋:最好還是更加詳細(xì),面向零基礎(chǔ),小白這些,還有github訪問速度也是一方面問題,可能圖片都加載不出來。

          因此,我打算分模塊出幾期文章,這樣你只用通過文章即可了解 Chocolate 同學(xué)整體刷題匯總啦。

          希望能夠幫助你的春秋招。打算出的內(nèi)容計劃安排如下:


          算法這一塊到底如何準(zhǔn)備

          首先,我來簡單介紹一下自己,在校打過 ACM(如果沒聽過,當(dāng)我沒說,因為沒有很大價值的牌牌,鐵牌,參賽證以及證書倒是一堆)

          如果你知道 acm,并且參與過,對于國內(nèi)前端(注意是說前端)面試的話,應(yīng)該不需要花費很長的刷題時間,如果大家有想法了解我的 acm 經(jīng)歷的話,這個后續(xù)我會考慮在 「B 站發(fā)布一期視頻」

          那么對于零基礎(chǔ)的小白來說,可能需要花 10-20 天左右時間來準(zhǔn)備算法,而對于非科班來說這個周期可能會更長一點。那么,現(xiàn)在我準(zhǔn)備來分享我是如何帶你零基礎(chǔ)刷題的。

          • 第一點,不要畏懼算法,理解了其實就那會事,或許你還會喜歡上做題,當(dāng)然,對于 acm 大佬做的題就另當(dāng)別論了,這篇文章主體與面試水平為準(zhǔn)
          • 第二點,前端對于算法這一塊的考察相對來說會偏簡單一點,我在春秋招過程中遇到的筆試題都是一些常見的題目,比如搜索,貪心,簡單動態(tài)規(guī)劃,經(jīng)典排序算法,都是以 leetcode 一些簡單以及中等難度的居多,而這些算法對于科班來說的話,應(yīng)該在學(xué)校都學(xué)習(xí)過,比如算法分析與設(shè)計,數(shù)據(jù)結(jié)構(gòu)與算法這一類課程,那么有這個基礎(chǔ),你的刷題時間又可以進(jìn)行縮短了
          • 第三點,既然說到要刷題,該如何刷,我在掘金參考了幾個大佬(文末有參考處),大家都會推薦分專題來刷,在這里,我也是非常推薦的,在這里,我希望的是將刷算法題的數(shù)量再減少一點,帶你入門,當(dāng)你刷完這些專題之后,你就有相關(guān)思維能力主動去刷題了,而不是很被動的去刷,這樣也很方便自己總結(jié)歸納~
          • 其它,可以參考大佬的文章,這里不再贅述...

          一份思維導(dǎo)圖,讓你的刷題路線更簡單

          開門見山地說,首先提供一份思維導(dǎo)圖,讓知識由繁到簡。

          獲取高清PDF,請在微信公眾號「小獅子前端」回復(fù)「LeetCode」,一起刷題或者交流可在后臺回復(fù)「交流」

          本倉庫刷題路線參考 ssh  (給大佬點贊)

          感謝大佬的歸納總結(jié),原本打算在大佬那里打卡學(xué)習(xí),后面考慮不太友好,還是自己新建了一個倉庫打卡學(xué)習(xí)。

          其次,本倉庫解題代碼大部分是自己的代碼風(fēng)格,題量也進(jìn)行了拓展,將會持續(xù)更新下去,何不 star 收藏一下?

          倉庫介紹

          倉庫地址:https://github.com/Chocolate1999/leetcode-javascript

          本倉庫將全程使用的語言是 JavaScript,是一個純前端刷題路線,對于前端刷題沒有方向的小伙伴簡直是福音。解題代碼會記錄在本倉庫的 Issues 中,會按照 label進(jìn)行分類。比如想查看 「遞歸與回溯」 分類下的問題,那么選擇標(biāo)簽進(jìn)行篩選即可。

          同時,小伙伴們可以在 Issues 中提交自己的解題代碼,?? 歡迎 Contributing ,可打卡刷題,堅持下來的人最酷!Give a ?? if this project helped you !

          刷題路線

          下面正式開始我們的刷題之路,給本篇文章來個「點贊+在看」,拿出自己心儀的鍵盤,開始!

          以下專題順序僅個人以及面試高頻點來總結(jié)的刷題方式,大家可以根據(jù)自己的想法來組合。更多題集請參考本倉庫哈~


          遞歸與回溯

          784. 字母大小寫全排列

          「題目描述」

          給定一個字符串S,通過將字符串S中的每個字母轉(zhuǎn)變大小寫,我們可以獲得一個新的字符串。返回所有可能得到的字符串集合。

          示例:

          輸入:S = "a1b2"
          輸出:["a1b2""a1B2""A1b2""A1B2"]

          輸入:S = "3z4"
          輸出:["3z4""3Z4"]

          輸入:S = "12345"
          輸出:["12345"]
           

          提示:

          S 的長度不超過12
          S 僅由數(shù)字和字母組成。

          「解題思路」

          這道題就是遞歸操作,沒有回溯,是一個挺有意思的題目,在講解思路之前,我先搬運一下大佬的圖解,方便我后續(xù)補充。

          參考大佬 liweiwei1419 圖解

          第一步第二步第三步第四步

          第五步第六步好了,有了上述圖解之后(還是感謝大佬的圖解,萬分感謝orz),我相信明白的已經(jīng)明白了,如果不明白我繼續(xù)解釋。

          此題我們只需要從頭往后遍歷一遍即可,對于非字母節(jié)點,我們只會產(chǎn)生一個分支,而對于字母節(jié)點,我們可以產(chǎn)生兩個分支,即大寫字母和小寫字母。(詳細(xì)請參見下述代碼)

          于是,我們只要簡單搜一遍就可以了。

          /**
           * @param {string} S
           * @return {string[]}
           */

          var letterCasePermutation = function(S{
              let res = []
              let dfs = (t,str) => {
                  if(t.length === S.length)
                      return res.push(t)
                  let ch = str[0]
                  let nextStr = str.substr(1)
                  // 當(dāng)前位置為數(shù)字,只有一個分支
                  if(!isNaN(Number(ch))){
                      dfs(t+ch,nextStr)
                  }else{
                      //當(dāng)前位置為字母,會產(chǎn)生兩個分支
                      let tmp = ch.toUpperCase()
                      if(tmp === ch) tmp = ch.toLowerCase()
                      dfs(t+ch,nextStr)
                      dfs(t+tmp,nextStr)
                  }
              }
              dfs('',S)
              return res
          };

          面試題 08.08. 有重復(fù)字符串的排列組合

          「題目描述」

          有重復(fù)字符串的排列組合。編寫一種方法,計算某字符串的所有排列組合。

          示例1:

           輸入:S = "qqe"
           輸出:["eqq","qeq","qqe"]

          示例2:

           輸入:S = "ab"
           輸出:["ab""ba"]

          提示:

          字符都是英文字母。
          字符串長度在[19]之間。

          「解題思路」

          全排列,直接用回溯法即可,數(shù)據(jù)量比較小,暴力完事~

          var permutation = function (S{
            let res = new Set()
            let vis = []
            let dfs = (t) => {
              if (t.length === S.length) return res.add(t)
              for (let i = 0; i < S.length; i++) {
                if (vis[i]) continue
                vis[i] = true
                dfs(t + S[i])
                vis[i] = false
              }
            }
            dfs('')
            return [...res]
          }

          980. 不同路徑 III

          「題目描述」

          在二維網(wǎng)格 grid 上,有 4 種類型的方格:

          1 表示起始方格。且只有一個起始方格。2 表示結(jié)束方格,且只有一個結(jié)束方格。0 表示我們可以走過的空方格。-1 表示我們無法跨越的障礙。返回在四個方向(上、下、左、右)上行走時,從起始方格到結(jié)束方格的不同路徑的數(shù)目。

          每一個無障礙方格都要通過一次,但是一條路徑中不能重復(fù)通過同一個方格。

          示例 1:

          輸入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
          輸出:2
          解釋:我們有以下兩條路徑:
          1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
          2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)

          示例 2:

          輸入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]]
          輸出:4
          解釋:我們有以下四條路徑: 
          1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
          2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
          3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
          4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)

          示例 3:

          輸入:[[0,1],[2,0]]
          輸出:0
          解釋:
          沒有一條路能完全穿過每一個空的方格一次。
          請注意,起始和結(jié)束方格可以位于網(wǎng)格中的任意位置。

          提示:

          1 <= grid.length * grid[0].length <= 20

          「解題思路」

          回溯算法,不過這道題需要我們走完所有空格,所以我們起初遍歷的時候需要統(tǒng)計一下空格的數(shù)目,然后還有一個注意點就是重點也算是可走的路徑的一個點,也需要統(tǒng)計進(jìn)去,所以代碼 cnt 值 初始化為 1

          接下來就是回溯過程了,寫了一個 check 函數(shù),進(jìn)行簡單判斷剪枝,然后就是往四個方向搜,每走一個格子就將當(dāng)前格子設(shè)置為障礙(即 -1),然后搜索完后,回溯時,需要將障礙重設(shè)置為空格。

          /**
           * @param {number[][]} grid
           * @return {number}
           */

          var uniquePathsIII = function(grid{
              let cnt = 1 // 統(tǒng)計地圖中可走的方格個數(shù),包括終點,故初始值為1
              let sx,sy // 記錄起點坐標(biāo)
              for(let i=0;i<grid.length;i++){
                  for(let j=0;j<grid[0].length;j++){
                      if(grid[i][j] === 1){
                          sx = i
                          sy = j
                      }
                      else if(grid[i][j] === 0){
                          cnt++
                      }
                  }
              }
              return dfs(sx,sy,cnt,grid)
          };
          // 剪枝條件
          let check = (sx,sy,grid) => {
              if(sx<0 || sx>=grid.length || sy<0 || sy>=grid[0].length || grid[sx][sy] == -1return false
              return true
          }

          let dfs = (sx,sy,cnt,grid) => {
              if(!check(sx,sy,grid)) return 0
              if(grid[sx][sy] === 2){ // 走到終點時,也要判斷一下當(dāng)前所有空格是否走完
                  return cnt === 0 ? 1:0
              }
              let res = 0
              grid[sx][sy] = -1  //走過的空格進(jìn)行標(biāo)記,設(shè)置為障礙即可
              res += dfs(sx+1,sy,cnt-1,grid)  // 四個方向進(jìn)行搜索
              res += dfs(sx,sy+1,cnt-1,grid)
              res += dfs(sx-1,sy,cnt-1,grid)
              res += dfs(sx,sy-1,cnt-1,grid)
              grid[sx][sy] = 0  // 回溯過程,不影響后續(xù)dfs
              return res
          }

          1219. 黃金礦工

          「題目描述」

          你要開發(fā)一座金礦,地質(zhì)勘測學(xué)家已經(jīng)探明了這座金礦中的資源分布,并用大小為 m * n 的網(wǎng)格 grid 進(jìn)行了標(biāo)注。每個單元格中的整數(shù)就表示這一單元格中的黃金數(shù)量;如果該單元格是空的,那么就是 0

          為了使收益最大化,礦工需要按以下規(guī)則來開采黃金:

          每當(dāng)?shù)V工進(jìn)入一個單元,就會收集該單元格中的所有黃金。礦工每次可以從當(dāng)前位置向上下左右四個方向走。每個單元格只能被開采(進(jìn)入)一次。不得開采(進(jìn)入)黃金數(shù)目為 0 的單元格。礦工可以從網(wǎng)格中 任意一個 有黃金的單元格出發(fā)或者是停止。

          示例 1:

          輸入:grid = [[0,6,0],[5,8,7],[0,9,0]]
          輸出:24
          解釋:
          [[0,6,0],
           [5,8,7],
           [0,9,0]]
          一種收集最多黃金的路線是:9 -> 8 -> 7

          示例 2:

          輸入:grid = [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]]
          輸出:28
          解釋:
          [[1,0,7],
           [2,0,6],
           [3,4,5],
           [0,3,0],
           [9,0,20]]
          一種收集最多黃金的路線是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7

          提示:

          1 <= grid.length, grid[i].length <= 15
          0 <= grid[i][j] <= 100
          最多 25 個單元格中有黃金。

          「解題思路」

          這題也是搜索相關(guān),四個方向,不允許重復(fù),不過這次我們需要從不同起點搜索,而且為了減少搜索次數(shù),我們得從黃金數(shù)量不為0的點開始搜。然后每當(dāng)走不下去的時候,就比較一下當(dāng)前黃金數(shù)量,求出最大值即可。

          /**
           * @param {number[][]} grid
           * @return {number}
           */

          var getMaximumGold = function(grid{
              if(!grid || !grid.length) return 0
              let vis = []
              // 最終收集的最多黃金數(shù)量
              let maxGold = 0
              for(let i=0;i<grid.length;i++) vis[i] = []
              // 剪枝條件
              let check = (x,y) => {
                  if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || vis[x][y] === 1 || !grid[x][y]) return false
                  return true
              }
              let dfs = (x,y,total) => {
                  if(check(x,y)){
                      vis[x][y] = 1 //防止重復(fù)
                      dfs(x+1,y,total+grid[x][y]) // 四個方向搜索
                      dfs(x,y+1,total+grid[x][y])
                      dfs(x-1,y,total+grid[x][y])
                      dfs(x,y-1,total+grid[x][y])
                      vis[x][y] = 0
                  }else{
                      // 走到底了,就比較一下當(dāng)前黃金數(shù)量
                      maxGold = Math.max(maxGold,total)
                  }
              }
              // 起點從非0單元格開始
              for(let i=0;i<grid.length;i++){
                  for(let j=0;j<grid[0].length;j++){
                      if(grid[i][j]){
                          dfs(i,j,0)
                      }
                  }
              }
              return maxGold
          };

          79. 單詞搜索

          「題目描述」

          給定一個二維網(wǎng)格和一個單詞,找出該單詞是否存在于網(wǎng)格中。

          單詞必須按照字母順序,通過相鄰的單元格內(nèi)的字母構(gòu)成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內(nèi)的字母不允許被重復(fù)使用。

          示例:

          board =
          [
            ['A','B','C','E'],
            ['S','F','C','S'],
            ['A','D','E','E']
          ]

          給定 word = "ABCCED", 返回 true
          給定 word = "SEE", 返回 true
          給定 word = "ABCB", 返回 false

          提示:

          board 和 word 中只包含大寫和小寫英文字母。
          1 <= board.length <= 200
          1 <= board[i].length <= 200
          1 <= word.length <= 10^3

          「解題思路」

          上一期做了單詞搜索2 hard 版本之后,這道題也想用字典樹玩一玩,沒想到超時了,后面一想,數(shù)據(jù)確實有點大,而且對于一個單詞來說,建立一顆字典樹豈不是很浪費,還要花時間碼代碼...

          本題也是回溯的思路,不過期間做了一點小優(yōu)化,還是通過動態(tài)更改當(dāng)前所走的格子,省去了那份 開辟vis 數(shù)組的空間。

          對于遞歸層次,由于最后一次計算時,層次多算了一次(即多加了一次),所以條件為 >

          var exist = function(grid, word{
            let dfs = (x,y,t) => {
              // 最后一次還會 +1 因此,條件是大于
              if(t > word.length-1){
                return true
              }
              // 剪枝條件
              if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || grid[x][y]!= word[t] || grid[x][y] == '#'return false
              let tmp = grid[x][y]
              // 開始走
              grid[x][y] = '#'
              // 從四個方向搜索,只要一個方向搜索有結(jié)果,那么直接返回 true即可
              let res = dfs(x+1,y,t+1) || dfs(x,y+1,t+1) || dfs(x-1,y,t+1) || dfs(x,y-1,t+1)
              if(res) return true
              // 回溯(重置)
              grid[x][y] = tmp
              return false
            }
            for(let i=0;i<grid.length;i++){
              for(let j=0;j<grid[0].length;j++){
                if(grid[i][j] == word[0]){
                  let res = dfs(i,j,0)
                  if(res) return true
                }
              }
            }
            return false
          };

          212. 單詞搜索 II

          「題目描述」

          給定一個二維網(wǎng)格 board 和一個字典中的單詞列表 words,找出所有同時在二維網(wǎng)格和字典中出現(xiàn)的單詞。

          單詞必須按照字母順序,通過相鄰的單元格內(nèi)的字母構(gòu)成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內(nèi)的字母在一個單詞中不允許被重復(fù)使用。

          示例:

          輸入: 
          words = ["oath","pea","eat","rain"] and board =
          [
            ['o','a','a','n'],
            ['e','t','a','e'],
            ['i','h','k','r'],
            ['i','f','l','v']
          ]

          輸出: ["eat","oath"]
          說明:
          你可以假設(shè)所有輸入都由小寫字母 a-z 組成。

          提示:

          你需要優(yōu)化回溯算法以通過更大數(shù)據(jù)量的測試。你能否早點停止回溯?
          如果當(dāng)前單詞不存在于所有單詞的前綴中,則可以立即停止回溯。什么樣的數(shù)據(jù)結(jié)構(gòu)可以有效地執(zhí)行這樣的操作?散列表是否可行?為什么? 前綴樹如何?如果你想學(xué)習(xí)如何實現(xiàn)一個基本的前綴樹,請先查看這個問題: 實現(xiàn)Trie(前綴樹)。

          「解題思路」

          「參考力扣官網(wǎng)分析:實現(xiàn) Trie (前綴樹)」

          • 判斷是否找到了,通過傳遞節(jié)點的END來判斷

          • 判斷是否重復(fù)訪問,通過動態(tài)更改走過的網(wǎng)格點來判斷,就不需要再定義一個vis數(shù)組了

          「參考大佬:秦時明月字典樹建樹解法(二)」

          var findWords = function(grid, words{
            // 存放最終結(jié)果集
            let res = []
            // 字典樹節(jié)點
            class TrieNode {
              constructor(){
                this.end = false
                this.child = {}
              }
            }
            // 最終形成的字典樹根節(jié)點
            let root = null
            let Trie = function(){
              root = new TrieNode()
            }
            // 建立字典樹
            Trie.prototype.insert = (word) => {
              let cur = root
              for(let i=0;i<word.length;i++){
                if(!cur.child[word[i]]){
                  cur.child[word[i]] = new TrieNode()
                }
                cur = cur.child[word[i]]
              }
              cur.end = true
            }
            // 創(chuàng)建根節(jié)點
            let trie = new Trie()
            // 進(jìn)行建樹操作
            for(let i=0;i<words.length;i++){
              trie.insert(words[i])
            }
            let dfs = (x,y,t,cur) => {
              if(cur.end){
                res.push(t)
                cur.end = false // 避免重復(fù)計算
              }
              // 剪枝條件:1.邊界處理 2.下一步是否可走 3.下一步字典樹是否可走
              if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || grid[x][y] == '#' || !cur.child[grid[x][y]]) return
              let tmp = grid[x][y]
              grid[x][y] = '#'  // 走
              cur = cur.child[tmp]
              dfs(x+1,y,t+tmp,cur)  // 上下左右四個方向遍歷
              dfs(x,y+1,t+tmp,cur)
              dfs(x-1,y,t+tmp,cur)
              dfs(x,y-1,t+tmp,cur)
              grid[x][y] = tmp // 回溯(還原)
            }
            // 對單詞表進(jìn)行全局搜索
            for(let i=0;i<grid.length;i++){
              for(let j=0;j<grid[0].length;j++){
                dfs(i,j,'',root)
              }
            }
            return res
          };

          附上完整字典樹(前綴樹)模板,日后可用~

          「在 Trie 樹中查找鍵」

          每個鍵在 trie 中表示為從根到內(nèi)部節(jié)點或葉的路徑。我們用第一個鍵字符從根開始,。檢查當(dāng)前節(jié)點中與鍵字符對應(yīng)的鏈接。有兩種情況:

          • 存在鏈接。我們移動到該鏈接后面路徑中的下一個節(jié)點,并繼續(xù)搜索下一個鍵字符。
          • 不存在鏈接。若已無鍵字符,且當(dāng)前結(jié)點標(biāo)記為 isEnd,則返回 true。否則有兩種可能,均返回 false: 還有鍵字符剩余,但無法跟隨 Trie 樹的鍵路徑,找不到鍵。沒有鍵字符剩余,但當(dāng)前結(jié)點沒有標(biāo)記為 isEnd。也就是說,待查找鍵只是Trie樹中另一個鍵的前綴。

          「查找 Trie 樹中的鍵前綴」

          該方法與在 Trie 樹中搜索鍵時使用的方法非常相似。我們從根遍歷 Trie 樹,直到鍵前綴中沒有字符,或者無法用當(dāng)前的鍵字符繼續(xù) Trie 中的路徑。與上面提到的“搜索鍵”算法唯一的區(qū)別是,到達(dá)鍵前綴的末尾時,總是返回 true。我們不需要考慮當(dāng)前 Trie 節(jié)點是否用 “isend” 標(biāo)記,因為我們搜索的是鍵的前綴,而不是整個鍵。

          var findWords = function(grid, words{
            // 存放最終結(jié)果集
            let res = []
            // 字典樹節(jié)點
            class TrieNode {
              constructor(){
                this.end = false
                this.child = {}
              }
            }
            // 最終形成的字典樹根節(jié)點
            let root = null
            let Trie = function(){
              root = new TrieNode()
            }
            // 建立字典樹
            Trie.prototype.insert = (word) => {
              let cur = root
              for(let i=0;i<word.length;i++){
                if(!cur.child[word[i]]){
                  cur.child[word[i]] = new TrieNode()
                }
                cur = cur.child[word[i]]
              }
              cur.end = true
            }
            // 在 Trie 樹中查找鍵
            let searchPrefix = (word) => {
              let cur = root
              for(let i=0;i<word.length;i++){
                if(cur.child[word[i]]){
                  cur = cur.child[word[i]]
                }else{
                  return null
                }
              }
              return cur
            }
            Trie.prototype.search = (word) => {
              let cur = searchPrefix(word)
              return cur !== null && cur.end
            }
            // 查找 Trie 樹中的鍵前綴
            Trie.prototype.startsWith = (pre) => {
              return searchPrefix(pre) != null
            }
            // 創(chuàng)建根節(jié)點
            let trie = new Trie()
            // 進(jìn)行建樹操作
            for(let i=0;i<words.length;i++){
              trie.insert(words[i])
            }
            let dfs = (x,y,t,cur) => {
              if(cur.end){
                res.push(t)
                cur.end = false // 避免重復(fù)計算
              }
              // 剪枝條件:1.邊界處理 2.下一步是否可走 3.下一步字典樹是否可走
              if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || grid[x][y] == '#' || !cur.child[grid[x][y]]) return
              let tmp = grid[x][y]
              grid[x][y] = '#'  // 走
              cur = cur.child[tmp]
              dfs(x+1,y,t+tmp,cur)  // 上下左右四個方向遍歷
              dfs(x,y+1,t+tmp,cur)
              dfs(x-1,y,t+tmp,cur)
              dfs(x,y-1,t+tmp,cur)
              grid[x][y] = tmp // 回溯(還原)
            }
            // 對單詞表進(jìn)行全局搜索
            for(let i=0;i<grid.length;i++){
              for(let j=0;j<grid[0].length;j++){
                dfs(i,j,'',root)
              }
            }
            return res
          };

          77. 組合

          「題目描述」

          給定兩個整數(shù) n 和 k,返回 1 ... n 中所有可能的 k 個數(shù)的組合。

          示例:

          輸入: n = 4, k = 2
          輸出:
          [
            [2,4],
            [3,4],
            [2,3],
            [1,2],
            [1,3],
            [1,4],
          ]

          「解題思路」

          直接套用組合題解題模板即可

          var combine = function (n, k{
            let res = [];
            let dfs = (t, start) => {
              if (t.length === k) {
                res.push(t);
                return;
              }
              for (let i = start; i <= n; i++) {
                t.push(i);
                dfs(t.slice(), i + 1);
                t.pop();
              }
            }
            dfs([], 1);
            return res;
          };

          39. 組合總和

          「題目描述」

          給定一個無重復(fù)元素的數(shù)組 candidates 和一個目標(biāo)數(shù) target ,找出 candidates 中所有可以使數(shù)字和為 target 的組合。

          candidates 中的數(shù)字可以無限制重復(fù)被選取。

          說明:

          所有數(shù)字(包括 target)都是正整數(shù)。
          解集不能包含重復(fù)的組合。 

          示例 1:

          輸入:candidates = [2,3,6,7], target = 7,
          所求解集為:
          [
            [7],
            [2,2,3]
          ]

          示例 2:

          輸入:candidates = [2,3,5], target = 8,
          所求解集為:
          [
            [2,2,2,2],
            [2,3,3],
            [3,5]
          ]

          提示:

          1 <= candidates.length <= 30
          1 <= candidates[i] <= 200
          candidate 中的每個元素都是獨一無二的。
          1 <= target <= 500

          「解題思路」

          這道題是組合題,但是這道題有意思的是當(dāng)前元素可以重復(fù)無限制選取,那么我們可以改一下另外一道組合題的思路,下一層也從 i開始即可,然后本題元素重復(fù),那么我們不需要進(jìn)行排序然后剪枝了。

          // 當(dāng)前元素可以無限制選取,下一層也從i開始取
          dfs(t.slice(),i,sum+candidates[i]); 

          「參考xiao_ben_zhu圖解」

          var combinationSum = function(candidates, target{
            let res = [];
            let dfs = (t,start,sum) => {
              if(sum >= target){ // 防止爆掉
                if(sum === target){
                  res.push(t);
                }
                return;
              }
              for(let i=start;i<candidates.length;i++){
                t.push(candidates[i]);
                // 當(dāng)前元素可以無限制選取,下一層也從i開始取
                dfs(t.slice(),i,sum+candidates[i]); 
                t.pop();
              }
            }
            dfs([],0,0);
            return res;
          };

          40. 組合總和 II

          「題目描述」

          給定一個數(shù)組 candidates和一個目標(biāo)數(shù) target ,找出candidates 中所有可以使數(shù)字和為target的組合。

          candidates中的每個數(shù)字在每個組合中只能使用一次。

          說明:

          所有數(shù)字(包括目標(biāo)數(shù))都是正整數(shù)。解集不能包含重復(fù)的組合。示例 1:

          輸入: candidates = [10,1,2,7,6,1,5], target = 8,
          所求解集為:
          [
            [17],
            [125],
            [26],
            [116]
          ]

          示例 2:

          輸入: candidates = [2,5,2,1,2], target = 5,
          所求解集為:
          [
            [1,2,2],
            [5]
          ]

          「解題思路」

          這道題也是一道組合題,但是這道題數(shù)組里面是存在重復(fù)元素的,組合題的話,為了更好地去重,我們可以先對數(shù)組進(jìn)行排序,然后對于每一層如果相鄰元素相同,就剪掉該分支即可。

          「參考xiao_ben_zhu大佬圖解」

          注意求和那里,如果只判斷是否相等的話,可能會出現(xiàn)爆掉情況。

          var combinationSum2 = function (candidates, target{
            let res = [];
            candidates.sort((a, b) => a - b);
            let dfs = (t, start, sum) => {
              if (sum >= target) { // 加這外層,超出范圍了也終止,防爆棧
                if (sum === target) {
                  res.push(t);
                }
                return;
              }
              // 組合
              for (let i = start; i < candidates.length; i++) {
                // 組合元素不能重復(fù),去掉同一層重復(fù)的元素
                if (i > start && candidates[i] == candidates[i - 1]) continue;
                t.push(candidates[i]);
                // 組合元素去重,即當(dāng)前選擇和下一層的不能重復(fù)
                dfs(t.slice(), i + 1, sum + candidates[i]);
                t.pop();
              }
            }
            dfs([], 00);
            return res;
          };

          216. 組合總和 III

          「題目描述」

          找出所有相加之和為 nk 個數(shù)的組合。組合中只允許含有 1 - 9 的正整數(shù),并且每種組合中不存在重復(fù)的數(shù)字。

          說明:

          所有數(shù)字都是正整數(shù)。解集不能包含重復(fù)的組合。示例 1:

          輸入: k = 3, n = 7
          輸出: [[1,2,4]]

          示例 2:

          輸入: k = 3, n = 9
          輸出: [[1,2,6], [1,3,5], [2,3,4]]

          「解題思路」

          首先,還是搬運一下大佬的圖解,然后我再來解釋一番。

          「參考xiao_ben_zhu大佬圖解」

          本題需要一層一層來,第一層我們可以有 i(1-9)個選擇,而第二層的每一個值只有 i+1個選擇了,因為不能重復(fù)。比如你第一次拿了 2,在下一次,你只能從 3開始拿了,如果還是 1的話就會有重復(fù)的組合了。這樣我們也不用維護 vis數(shù)組來去重,因為每一層取的值是不一樣的。

          var combinationSum3 = function (k, n{
            let res = [];
            let dfs = (t, start, sum) => {
              if (t.length === k && sum === n) {
                res.push(t);
              }
              for (let i = start; i < 10; i++) {
                t.push(i);
                dfs(t.slice(), i + 1, sum + i);
                t.pop();
              }
            }
            dfs([], 10);
            return res;
          };

          401. 二進(jìn)制手表

          「題目描述」

          二進(jìn)制手表頂部有 4 個 LED 代表 「小時(0-11)」,底部的 6 個 LED 代表 「分鐘(0-59)」

          每個 LED 代表一個 0 或 1,最低位在右側(cè)。

          例如,上面的二進(jìn)制手表讀取 “3:25”。

          給定一個非負(fù)整數(shù) n 代表當(dāng)前 LED 亮著的數(shù)量,返回所有可能的時間。

          示例:

          輸入: n = 1
          返回: ["1:00""2:00""4:00""8:00""0:01""0:02""0:04""0:08""0:16""0:32"]
           

          提示:

          輸出的順序沒有要求。
          小時不會以零開頭,比如 “01:00” 是不允許的,應(yīng)為 “1:00”。
          分鐘必須由兩位數(shù)組成,可能會以零開頭,比如 “10:2” 是無效的,應(yīng)為 “10:02”。
          超過表示范圍(小時 0-11,分鐘 0-59)的數(shù)據(jù)將會被舍棄,也就是說不會出現(xiàn) "13:00""0:61" 等時間。

          「解題思路」

          回溯算法,我的解法類似于全排列做法,將10個小燈泡進(jìn)行排列組合,然后根據(jù) 01 來判斷燈泡是否亮,如果亮了,加上對應(yīng)二進(jìn)制,然后將 0-3分給小時來計算,將 4-9分給分鐘來計算,但是要考慮一下,就是可能會出現(xiàn)重復(fù)情況,于是用 Set數(shù)據(jù)結(jié)構(gòu)維護一下就好了。

          var readBinaryWatch = function(num{
              let res = new Set();
              let vis = new Array(10).fill(0)
              let check = (hour,minutes) => {
                if(hour>=0 && hour<=11 && minutes>=0 && minutes<=59return true;
                return false;
              }
              let dfs = (t,vis) => {
                if(t==0){
                  let hour = vis[0]*1 + vis[1]*2 + vis[2]*4 + vis[3]*8;
                  let minutes = vis[4]*1 + vis[5]*2 + vis[6]*4 + vis[7]*8 + vis[8]*16 + vis[9]*32;
                  if(check(hour,minutes)){
                    let tmp = `${hour}:${minutes >= 10? minutes: '0'+minutes}`;
                    res.add(tmp);
                  }
                }
                for(let i=0;i<10;i++){
                  if(vis[i]) continue;
                  vis[i] = 1;
                  dfs(t-1,vis);
                  vis[i] = 0;
                }
              }
              dfs(num,vis);
              return [...res];
          };

          補充,后面看到有大佬這樣做,進(jìn)行了去重操作,關(guān)鍵點在回溯 for循環(huán)那里。其實這個相當(dāng)于全排列了。

          var readBinaryWatch = function(num{
              let res = [];
              let vis = new Array(10).fill(0)
              let check = (hour,minutes) => {
                if(hour>=0 && hour<=11 && minutes>=0 && minutes<=59return true;
                return false;
              }
              let dfs = (t,cnt,vis) => {
                if(t==0){
                  let hour = vis[0]*1 + vis[1]*2 + vis[2]*4 + vis[3]*8;
                  let minutes = vis[4]*1 + vis[5]*2 + vis[6]*4 + vis[7]*8 + vis[8]*16 + vis[9]*32;
                  if(check(hour,minutes)){
                    let tmp = `${hour}:${minutes >= 10? minutes: '0'+minutes}`;
                    res.push(tmp);
                  }
                  return;
                }
                for(let i=cnt;i<=10-t;i++){
                  if(vis[i]) continue;
                  vis[i] = 1;
                  dfs(t-1,i+1,vis);
                  vis[i] = 0;
                }
              }
              dfs(num,0,vis);
              return res;
          };

          37. 解數(shù)獨

          「題目描述」

          編寫一個程序,通過填充空格來解決數(shù)獨問題。

          一個數(shù)獨的解法需「遵循如下規(guī)則」

          數(shù)字 1-9 在每一行只能出現(xiàn)一次。數(shù)字 1-9 在每一列只能出現(xiàn)一次。數(shù)字 1-9 在每一個以粗實線分隔的 3x3 宮內(nèi)只能出現(xiàn)一次。空白格用 '.' 表示。

          一個數(shù)獨。

          答案被標(biāo)成紅色。

          提示:

          給定的數(shù)獨序列只包含數(shù)字 1-9 和字符 '.' 。
          你可以假設(shè)給定的數(shù)獨只有唯一解。
          給定數(shù)獨永遠(yuǎn)是 9x9 形式的。

          「解題思路」

          我們一行一行的放,如果能得到一個解,直接返回 true,然后剪枝條件如下述 check函數(shù)。

          「參考xiao_ben_zhu大佬圖解」

          var solveSudoku = function (board{
            let check = (x, y, val) => {
              // 一行或者一列有重復(fù)元素,剪掉
              for (let i = 0; i < 9; i++) {
                if (board[x][i] == val || board[i][y] == val) return true;
              }
              let xx = Math.floor(x / 3) * 3;
              let yy = Math.floor(y / 3) * 3;
              // 3x3宮格內(nèi)重復(fù)的情況,剪掉
              for (let i = 0; i < 3; i++) {
                for (let j = 0; j < 3; j++) {
                  if (board[xx + i][yy + j] == val) return true;
                }
              }
              return false// 沒有沖突情況
            }
            let dfs = (x, y) => {
              if (y == 9) {
                x++;
                y = 0;
                if (x == 9return true// 都填完了,直接返回 true
              }
              if (board[x][y] != '.'return dfs(x, y + 1);
              for (let i = 1; i < 10; i++) {
                if (check(x, y, String(i))) continue;
                board[x][y] = String(i);
                if (dfs(x, y + 1)) return true// 如果往下走,能夠解出數(shù)獨,直接返回 true
                board[x][y] = '.'// 回溯,因為往下走得不到一個解
              }
              return false;
            }
            dfs(00);
            return board;
          };

          51. N 皇后

          「題目描述」

          n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,并且使皇后彼此之間不能相互攻擊。

          上圖為 8 皇后問題的一種解法。

          給定一個整數(shù) n,返回所有不同的 n 皇后問題的解決方案。

          每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 'Q' 和 '.' 分別代表了皇后和空位。

          示例:

          輸入:4
          輸出:[
           [".Q..",  // 解法 1
            "...Q",
            "Q...",
            "..Q."],

           ["..Q.",  // 解法 2
            "Q...",
            "...Q",
            ".Q.."]
          ]
          解釋: 4 皇后問題存在兩個不同的解法。

          提示:

          皇后彼此不能相互攻擊,也就是說:任何兩個皇后都不能處于同一條橫行、縱行或斜線上。

          「解題思路」

          對于 n 皇后問題,經(jīng)典的回溯算法,我們采用一行放一個,然后逐行來放,這樣我們就不用在剪枝的時候判斷是否同行了。只需要判斷是否同列 或者 同一斜線就好了。

          參考「xiao_ben_zhu」大佬圖解

          var solveNQueens = function(n{
            let res = [];
            let grid = new Array(n); // 初始化一個地圖
            for(let i=0;i<n;i++){
              grid[i] = new Array(n).fill('.');
            }
            // 剪枝條件 
            let check = (x,y)=>{
              for(let i=0;i<x;i++){
                for(let j=0;j<n;j++){
                  // 判斷同列 或者 同一斜線即可(不需要判斷同行是因為一行一行放的,一定不同行)
                  if(grid[i][j] == 'Q' && (j == y || i+j == x+y || i-j == x-y) ){
                    return true;
                  }
                }
              }
              return false;
            }
            let dfs = (t) => {
              if(t === n ){
                let ans = grid.slice(); // 拷貝一份,對輸出做處理
                for(let i=0;i<n;i++){
                  ans[i] = ans[i].join('');
                }
                res.push(ans);
                return;
              }
              for(let i=0;i<n;i++){
                if(check(t,i)) continue;
                grid[t][i] = 'Q';
                dfs(t+1);
                grid[t][i] = '.';
              }
            }
            dfs(0);
            return res;
          };

          131. 分割回文串

          「題目描述」

          給定一個字符串 s,將 s 分割成一些子串,使每個子串都是回文串。

          返回 s 所有可能的分割方案。

          示例:

          輸入: "aab"
          輸出:
          [
            ["aa","b"],
            ["a","a","b"]
          ]

          「解題思路」

          借鑒「zesong-wang-c」 大佬的圖解

          本題采用回溯思想,看上圖基本已經(jīng)明白,每次進(jìn)行一次切割,直到切到最后一個元素,然后壓入結(jié)果集合里,期間對于每次切割的字符串,我們判斷一下是否是回文,如果不是,直接減掉即可。

          和組合的思想有點類似。

          // 判斷是否是回文
          function isPal(str{
            let len = Math.floor(str.length / 2);
            if (len === 0) {
              return true;
            }
            let add = str.length % 2 === 0 ? 0 : 1;
            let subStr = str.slice(0, len);
            for (let i = 0; i < len; i++) {
              if (subStr[len - i - 1] !== str[len + add + i]) {
                return false;
              }
            }
            return true;
          }
          var partition = function (s{
            let res = [];
            let dfs = (cur, start) => {
              // 當(dāng)前已經(jīng)到達(dá)了最后一個元素
              if (start >= s.length) {
                res.push(cur.slice());
                return;
              }
              for (let i = start; i < s.length; i++) {
                // 字符串切割
                let str = s.slice(start, i + 1);
                if (str && isPal(str) ) {
                  cur.push(str);
                  dfs(cur, i + 1);
                  // 回溯
                  cur.pop();
                }
              }
            }
            dfs([], 0);
            return res;
          };

          93. 復(fù)原IP地址

          「題目描述」

          給定一個只包含數(shù)字的字符串,復(fù)原它并返回所有可能的 IP 地址格式。

          有效的 IP 地址 正好由四個整數(shù)(每個整數(shù)位于 0 到 255 之間組成,且不能含有前導(dǎo) 0),整數(shù)之間用 '.' 分隔。

          例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "[email protected]" 是 無效的 IP 地址。

          示例 1:

          輸入:s = "25525511135"
          輸出:["255.255.11.135","255.255.111.35"]

          示例 2:

          輸入:s = "0000"
          輸出:["0.0.0.0"]

          示例 3:

          輸入:s = "1111"
          輸出:["1.1.1.1"]

          示例 4:

          輸入:s = "010010"
          輸出:["0.10.0.10","0.100.1.0"]

          示例 5:

          輸入:s = "101023"
          輸出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
           

          提示:

          0 <= s.length <= 3000
          s 僅由數(shù)字組成

          「解題思路」

          直接看圖解,顯然要用回溯來做,我的做法是對于當(dāng)前位置,我們可以有三種選擇,選一個,選兩個,還有選三個。此時就需要判斷一下是不是會出現(xiàn)選出邊界的情況。

          然后對于我們選擇的數(shù)字,要判斷是否出現(xiàn)前導(dǎo) 0 ,同時也要看一下如果是三位數(shù)字的話,是不是會超過 255 。題目不能重復(fù)選擇,于是用組合思想,免去 vis 數(shù)組。借助大佬 xiao_ben_zhu 圖解

          var restoreIpAddresses = function (s{
            let res = [];
            let dfs = (cur, start) => {
              if (cur.length == 4 && start>=s.length) {
                res.push(cur.join('.'));
                return;
              }
              if(cur.length == 4 && start != s.length) return;
              for(let k=1;k<=3;k++){
                // 如果取的范圍超過了字符串長度,直接剪掉
                if(start+k-1>=s.length) return;
                // 切割字符串
                let str = s.substring(start,start+k);
                if(str.length>=2 && str[0] == 0return;
                if(str.length>=3 && +str > 255return;
                cur.push(str);
                dfs(cur.slice(),start+k);
                // 回溯
                cur.pop();
              }
            }
            dfs([], 0);
            return res;
          };

          22. 括號生成

          「題目描述」

          數(shù)字 n 代表生成括號的對數(shù),請你設(shè)計一個函數(shù),用于能夠生成所有可能的并且 有效的 括號組合。

          示例:

          輸入:n = 3
          輸出:[
                 "((()))",
                 "(()())",
                 "(())()",
                 "()(())",
                 "()()()"
               ]

          「解題思路」

          這道題,看了大佬的題解,發(fā)現(xiàn)真是有意思,現(xiàn)在來解釋一下。

          我們可以直接走可行的情況,對于不可行的情況,自然就剪掉了。

          關(guān)鍵在于左右括號如何選擇,首先,對于左括號,起初我們必然是要選的,然后我們也可以全部選完,因此,只要有左括號我們必須選,而對于右括號而言,它的剩余數(shù)量必須大于剩余左括號數(shù)量,我們才能選右括號。

          舉個反例,假如我們現(xiàn)在已經(jīng)有了 (())n = 3,然后左右括號都還剩一個,如果理解選 ),豈不是就 (()))了,顯示不是有效的括號,應(yīng)該被剪掉才是,因此,我們必須嚴(yán)格右括號剩余數(shù)量必須大于剩余左括號數(shù)量,我們才能選右括號。參考 笨豬爆破組 大佬圖解

          var generateParenthesis = function (n{
            let res = [];
            let dfs = (cur, left, right) => {
              if (cur.length === 2 * n) {
                res.push(cur);
                return;
              }
              // 左括號還存在,就可以選左括號
              if (left > 0) dfs(cur + '(', left - 1, right);
              // 右括號數(shù)量要大于左括號,才可以選右括號
              if (right > left) dfs(cur + ')', left, right - 1);
            }
            dfs('', n, n);
            return res;
          };


          本文參考

          • 「前端該如何準(zhǔn)備數(shù)據(jù)結(jié)構(gòu)和算法?」
          • 「寫給前端的算法進(jìn)階指南,我是如何兩個月零基礎(chǔ)刷200題」
          • 「(1.8w字)負(fù)重前行,前端工程師如何系統(tǒng)練習(xí)數(shù)據(jù)結(jié)構(gòu)和算法?【上】」

          參考鏈接:

          結(jié)語

          ??關(guān)注+點贊+收藏+評論+轉(zhuǎn)發(fā)??,原創(chuàng)不易,您的支持將會是我最大的動力~

          祝愿在準(zhǔn)備春秋招の你,能夠早點結(jié)束,offer 拿到手軟,希望我的文章能夠幫助到你,我們很快會在下期相遇,給個贊及時催更呀~

          學(xué)如逆水行舟,不進(jìn)則退

          你若喜歡,為小獅子點個【在看】哦~


          瀏覽 98
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美性爱超碰在线 | 五月婷婷黄色片 | 欧美色图亚洲激情 | 91成人一区二区三区 | 狂野欧美大鸡巴操逼 |