如今大廠面試幾乎都會問到算法,回答不上來會讓你在面試官前大打折扣。前端怎麼進階算法喃?javascript
本週是瓶子君前端進階算法的第三週🎉🎉🎉,這裏,會帶你 從 0 到 1 構建完整的前端數據結構與算法體系。前端
本週已經不單是簡單的鏈表操做(通常鏈表的問題能夠考慮使用快慢指針),開始涉及五大經常使用算法策略、二叉樹、Trie樹、隊列等,這裏僅做爲入門,後面會詳細介紹,發散思惟,你會發現面試中的算法、開發中的算法真的很 easy。java
往期精彩系列node
以及題目:git
本節是第三週的總結與回顧,下面開始進入正題吧!👇👇👇github
編寫一個函數來查找字符串數組中的最長公共前綴。面試
若是不存在公共前綴,返回空字符串 ""
。算法
示例 1:數組
輸入: ["flower","flow","flight"] 輸出: "fl"
示例 2:瀏覽器
輸入: ["dog","racecar","car"] 輸出: "" 解釋: 輸入不存在公共前綴。
解題思路: 從前日後一次比較字符串,獲取公共前綴
畫圖幫助理解一下:
代碼實現:
var longestCommonPrefix = function(strs) { if (strs === null || strs.length === 0) return ""; let prevs = strs[0] for(let i = 1; i < strs.length; i++) { let j = 0 for(; j < prevs.length && j < strs[i].length; j++) { if(prevs.charAt(j) !== strs[i].charAt(j)) break } prevs = prevs.substring(0, j) if(prevs === "") return "" } return prevs };
時間複雜度:O(s),s 是全部字符串中字符數量的總和
空間複雜度:O(1)
解題思路: 獲取數組中的最大值及最小值字符串,最小字符串與最大字符串的最長公共前綴也爲其餘字符串的公共前綴,即爲字符串數組的最長公共前綴
例如 abc
、 abcd
、ab
、ac
,最小 ab
與最大 ac
的最長公共前綴必定也是 abc
、 abcd
的公共前綴
畫圖幫助理解一下:
代碼實現:
var longestCommonPrefix = function(strs) { if (strs === null || strs.length === 0) return ""; if(strs.length === 1) return strs[0] let min = 0, max = 0 for(let i = 1; i < strs.length; i++) { if(strs[min] > strs[i]) min = i if(strs[max] < strs[i]) max = i } for(let j = 0; j < strs[min].length; j++) { if(strs[min].charAt(j) !== strs[max].charAt(j)) { return strs[min].substring(0, j) } } return strs[min] };
時間複雜度:O(n+m),n是數組的長度, m 是字符串數組中最短字符的長度
空間複雜度:O(1)
分治,顧名思義,就是分而治之,將一個複雜的問題,分紅兩個或多個類似的子問題,在把子問題分紅更小的子問題,直到更小的子問題能夠簡單求解,求解子問題,則原問題的解則爲子問題解的合併。
這道題就是一個典型的分治策略問題:
LCP(S1, S2, ..., Sn) = LCP(LCP(S1, Sk), LCP(Sk+1, Sn))
畫圖幫助理解一下:
以 abc
、 abcd
、ab
、ac
爲例:
代碼實現:
var longestCommonPrefix = function(strs) { if (strs === null || strs.length === 0) return ""; return lCPrefixRec(strs) }; // 若分裂後的兩個數組長度不爲 1,則繼續分裂 // 直到分裂後的數組長度都爲 1, // 而後比較獲取最長公共前綴 function lCPrefixRec(arr) { let length = arr.length if(length === 1) { return arr[0] } let mid = Math.floor(length / 2), left = arr.slice(0, mid), right = arr.slice(mid, length) return lCPrefixTwo(lCPrefixRec(left), lCPrefixRec(right)) } // 求 str1 與 str2 的最長公共前綴 function lCPrefixTwo(str1, str2) { let j = 0 for(; j < str1.length && j < str2.length; j++) { if(str1.charAt(j) !== str2.charAt(j)) { break } } return str1.substring(0, j) }
時間複雜度:O(s),s 是全部字符串中字符數量的總和
空間複雜度:O(m*logn),n是數組的長度,m爲字符串數組中最長字符的長度
Trie 樹,也稱爲字典樹或前綴樹,顧名思義,它是用來處理字符串匹配問題的數據結構,以及用來解決集合中查找固定前綴字符串的數據結構。
解題思路: 構建一個 Trie 樹,字符串數組的最長公共序列就爲從根節點開始遍歷樹,直到:
爲止,走過的字符爲字符串數組的最長公共前綴
畫圖幫助理解一下:
構建一個 Trie 樹,以 abc
、 abcd
、ab
、ac
爲例:
代碼實現:
var longestCommonPrefix = function(strs) { if (strs === null || strs.length === 0) return ""; // 初始化 Trie 樹 let trie = new Trie() // 構建 Trie 樹 for(let i = 0; i < strs.length; i++) { if(!trie.insert(strs[i])) return "" } // 返回最長公共前綴 return trie.searchLongestPrefix() }; // Trie 樹 var Trie = function() { this.root = new TrieNode() }; var TrieNode = function() { // next 放入當前節點的子節點 this.next = {}; // 當前是不是結束節點 this.isEnd = false; }; Trie.prototype.insert = function(word) { if (!word) return false let node = this.root for (let i = 0; i < word.length; i++) { if (!node.next[word[i]]) { node.next[word[i]] = new TrieNode() } node = node.next[word[i]] } node.isEnd = true return true }; Trie.prototype.searchLongestPrefix = function() { let node = this.root let prevs = '' while(node.next) { let keys = Object.keys(node.next) if(keys.length !== 1) break if(node.next[keys[0]].isEnd) { prevs += keys[0] break } prevs += keys[0] node = node.next[keys[0]] } return prevs }
時間複雜度:O(s+m),s 是全部字符串中字符數量的總和,m爲字符串數組中最長字符的長度,構建 Trie 樹須要 O(s) ,最長公共前綴查詢操做的複雜度爲 O(m)
空間複雜度:O(s),用於構建 Trie 樹
leetcode
給定一個字符串,逐個翻轉字符串中的每一個單詞。
示例 1:
輸入: "the sky is blue" 輸出: "blue is sky the"
示例 2:
輸入: " hello world! " 輸出: "world! hello" 解釋: 輸入字符串能夠在前面或者後面包含多餘的空格,可是反轉後的字符不能包括。
示例 3:
輸入: "a good example" 輸出: "example good a" 解釋: 若是兩個單詞間有多餘的空格,將反轉後單詞間的空格減小到只含一個。
說明:
var reverseWords = function(s) { return s.trim().replace(/\s+/g, ' ').split(' ').reverse().join(' ') };
雙端隊列,故名思義就是兩端均可以進隊的隊列
解題思路:
畫圖理解:
代碼實現:
var reverseWords = function(s) { let left = 0 let right = s.length - 1 let queue = [] let word = '' while (s.charAt(left) === ' ') left ++ while (s.charAt(right) === ' ') right -- while (left <= right) { let char = s.charAt(left) if (char === ' ' && word) { queue.unshift(word) word = '' } else if (char !== ' '){ word += char } left++ } queue.unshift(word) return queue.join(' ') };
編寫一個程序,找到兩個單鏈表相交的起始節點。
以下面的兩個鏈表:
在節點 c1 開始相交。
示例 1:
輸入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 輸出:Reference of the node with value = 8 輸入解釋:相交節點的值爲 8 (注意,若是兩個列表相交則不能爲 0)。從各自的表頭開始算起,鏈表 A 爲 [4,1,8,4,5],鏈表 B 爲 [5,0,1,8,4,5]。在 A 中,相交節點前有 2 個節點;在 B 中,相交節點前有 3 個節點。
示例 2:
輸入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 輸出:Reference of the node with value = 2 輸入解釋:相交節點的值爲 2 (注意,若是兩個列表相交則不能爲 0)。從各自的表頭開始算起,鏈表 A 爲 [0,9,1,2,4],鏈表 B 爲 [3,2,4]。在 A 中,相交節點前有 3 個節點;在 B 中,相交節點前有 1 個節點。
示例 3:
輸入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 輸出:null 輸入解釋:從各自的表頭開始算起,鏈表 A 爲 [2,6,4],鏈表 B 爲 [1,5]。因爲這兩個鏈表不相交,因此 intersectVal 必須爲 0,而 skipA 和 skipB 能夠是任意值。 解釋:這兩個鏈表不相交,所以返回 null。
注意:
解題思路: 兩次遍歷,先遍歷一個鏈表,給鏈表中的每一個節點都增長一個標誌位,而後遍歷另一個鏈表,遍歷到第一個已被標誌過的節點爲兩鏈表相交的起始節點。
若遍歷完都沒有發現已被標誌過的節點,則兩鏈表不相交,返回 null
var getIntersectionNode = function(headA, headB) { while(headA) { headA.flag = true headA = headA.next } while(headB) { if (headB.flag) return headB headB = headB.next } return null };
時間複雜度:O(n)
空間複雜度:O(n)
解題思路: 若是 A、B 兩鏈表相交,則 A 、B 自相交點日後的鏈表是一致的。
咱們能夠嘗試消除 A、B 鏈表的長度差,同步遍歷上圖中的方框裏的節點,判斷是否有相同節點,如有相同則是兩鏈表相交,返回第一個相同節點 便可。不然返回 null
,兩鏈表不相交。
解題步驟:
pA
、 pB
,直到遍歷完其中一個鏈表(短鏈表),如上圖,設A爲長鏈表pA
到鏈尾的長度,此時能夠把 pB
指向長鏈表的表頭 headA
,繼續同步遍歷,直到遍歷完長鏈表headA
到 pB
的長度就爲兩鏈表的長度差,pB
到鏈表的長度與 headB
到鏈尾的長度一致pA
指向 headB
,而後同步遍歷 pB
及 pA
,直到有相交節點,返回相交節點,不然返回 null
畫圖幫助理解:
var getIntersectionNode = function(headA, headB) { // 清除高度差 let pA = headA, pB = headB while(pA || pB) { if(pA === pB) return pA pA = pA === null ? headB : pA.next pB = pB === null ? headA : pB.next } return null };
時間複雜度:O(n)
空間複雜度:O(1)
給定一個鏈表,刪除鏈表的倒數第 n 個節點,而且返回鏈表的頭結點。
示例:
給定一個鏈表: 1->2->3->4->5, 和 n = 2. 當刪除了倒數第二個節點後,鏈表變爲 1->2->3->5.
說明:
給定的 n 保證是有效的。
進階:
你能嘗試使用一趟掃描實現嗎?
解題思路: 須要刪除鏈表中的倒數第 n
個節點,咱們須要知道的就是倒數第 n+1
個節點,而後刪除刪除倒數第 n+1
節點的後繼節點便可
步驟:
使用 2 個指針:
fast
快指針提早走 n+1
步slow
指針指向當前距離 fast
倒數第 n
個節點, 初始爲 head
而後, fast
、 slow
同步向前走,直到 fast.next
爲 null
此時,fast
爲最後一個節點,slow
就是倒數第 n+1
個節點,此時問題就變動爲刪除鏈表中的 slow
的後繼節點
但存在一個問題,當鏈表長度爲 n
時,fast
是前進不到 n+1
個節點位置的,因此此時有兩種解決思路:
preHead
,設置 preHead.next = head
,這樣就能夠解決以上問題,刪除倒數第 n
個節點後,返回的 preHead.next
便可fast
快指針提早走 n
步後,判斷 fast.next
是否爲 null
,即 fast
是不是最後一個節點,若是是,則 head
爲倒數第 n
個節點,此時問題能夠簡化爲刪除頭節點;若是不是, fast = fast.next
,fast
再前進一步,slow
爲倒數第 n+1
個節點,也解決了以上問題。preHead
節點var removeNthFromEnd = function(head, n) { let preHead = new ListNode(0) preHead.next = head let fast = preHead, slow = preHead // 快先走 n+1 步 while(n--) { fast = fast.next } // fast、slow 一塊兒前進 while(fast && fast.next) { fast = fast.next slow = slow.next } slow.next = slow.next.next return preHead.next };
n
節點var removeNthFromEnd = function(head, n) { let fast = head, slow = head // 快先走 n 步 while(--n) { fast = fast.next } if(!fast.next) return head.next fast = fast.next // fast、slow 一塊兒前進 while(fast && fast.next) { fast = fast.next slow = slow.next } slow.next = slow.next.next return head };
時間複雜度:O(n)
空間複雜度:O(1)
給定一個帶有頭結點 head
的非空單鏈表,返回鏈表的中間結點。
若是有兩個中間結點,則返回第二個中間結點。
示例 1:
輸入:[1,2,3,4,5] 輸出:此列表中的結點 3 (序列化形式:[3,4,5]) 返回的結點值爲 3 。 (測評系統對該結點序列化表述是 [3,4,5])。 注意,咱們返回了一個 ListNode 類型的對象 ans,這樣: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
輸入:[1,2,3,4,5,6] 輸出:此列表中的結點 4 (序列化形式:[4,5,6]) 因爲該列表有兩個中間結點,值分別爲 3 和 4,咱們返回第二個結點。
提示:
給定鏈表的結點數介於 1 和 100 之間。
解題思路: 快指針一次走兩步,慢指針一次走一步,當快指針走到終點時,慢指針恰好走到中間
var middleNode = function(head) { let fast = head, slow = head while(fast && fast.next) { slow = slow.next fast = fast.next.next } return slow };
時間複雜度:O(n)
空間複雜度:O(1)
示例:
輸入: 1->2->3->4->5->NULL 輸出: 5->4->3->2->1->NULL
進階:
你能夠迭代或遞歸地反轉鏈表。你可否用兩種方法解決這道題?
解題思路: 將單鏈表中的每一個節點的後繼指針指向它的前驅節點便可
畫圖實現: 畫圖幫助理解一下
肯定邊界條件: 當鏈表爲 null
或鏈表中僅有一個節點時,不須要反轉
代碼實現:
var reverseList = function(head) { if(!head || !head.next) return head var prev = null, curr = head while(curr) { // 用於臨時存儲 curr 後繼節點 var next = curr.next // 反轉 curr 的後繼指針 curr.next = prev // 變動prev、curr // 待反轉節點指向下一個節點 prev = curr curr = next } head = prev return head };
時間複雜度:O(n)
空間複雜度:O(1)
解題思路: 從頭節點開始,遞歸反轉它的每個節點,直到 null
,思路和解法一相似
代碼實現:
var reverseList = function(head) { if(!head || !head.next) return head head = reverse(null, head) return head }; var reverse = function(prev, curr) { if(!curr) return prev var next = curr.next curr.next = prev return reverse(curr, next) };
時間複雜度:O(n)
空間複雜度:O(n)
解題思路: 不斷遞歸反轉當前節點 head
的後繼節點 next
畫圖實現: 畫圖幫助理解一下
代碼實現:
var reverseList = function(head) { if(!head || !head.next) return head var next = head.next // 遞歸反轉 var reverseHead = reverseList(next) // 變動指針 next.next = head head.next = null return reverseHead };
時間複雜度:O(n)
空間複雜度:O(n)
歡迎關注「前端瓶子君」,回覆「算法」自動加入,從0到1構建完整的數據結構與算法體系!
在這裏,瓶子君不只介紹算法,還將算法與前端各個領域進行結合,包括瀏覽器、HTTP、V八、React、Vue源碼等。
在這裏,你能夠天天學習一道大廠算法題(阿里、騰訊、百度、字節等等)或 leetcode,瓶子君都會在次日解答喲!
⬆️ 掃碼關注公衆號「前端瓶子君」,回覆「算法」便可自動加入 👍👍👍