視頻面試超高頻在線編程題,搞懂這些足以應對大部分公司

引言

如今大廠面試幾乎都會問到算法,回答不上來會讓你在面試官前大打折扣。前端怎麼進階算法喃?javascript

本週是瓶子君前端進階算法的第三週🎉🎉🎉,這裏,會帶你 從 0 到 1 構建完整的前端數據結構與算法體系。前端

本週已經不單是簡單的鏈表操做(通常鏈表的問題能夠考慮使用快慢指針),開始涉及五大經常使用算法策略、二叉樹、Trie樹、隊列等,這裏僅做爲入門,後面會詳細介紹,發散思惟,你會發現面試中的算法、開發中的算法真的很 easy。java

往期精彩系列node

以及題目:git

本節是第三週的總結與回顧,下面開始進入正題吧!👇👇👇github

1、圖解字節&leetcode14:最長公共前綴

1. 題目

編寫一個函數來查找字符串數組中的最長公共前綴。面試

若是不存在公共前綴,返回空字符串 ""算法

示例 1:數組

輸入: ["flower","flow","flight"]
輸出: "fl"

示例 2:瀏覽器

輸入: ["dog","racecar","car"]
輸出: ""
解釋: 輸入不存在公共前綴。

2. 答案

解法一:逐個比較

解題思路: 從前日後一次比較字符串,獲取公共前綴

畫圖幫助理解一下:



代碼實現:

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)

解法二:僅需最大、最小字符串的最長公共前綴

解題思路: 獲取數組中的最大值及最小值字符串,最小字符串與最大字符串的最長公共前綴也爲其餘字符串的公共前綴,即爲字符串數組的最長公共前綴

例如 abcabcdabac ,最小 ab 與最大 ac 的最長公共前綴必定也是 abcabcd 的公共前綴

畫圖幫助理解一下:

代碼實現:

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))

畫圖幫助理解一下:

abcabcdabac 爲例:

代碼實現:

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 樹,字符串數組的最長公共序列就爲從根節點開始遍歷樹,直到:

  • 遍歷節點存在超過一個子節點的節點
  • 或遍歷節點爲一個字符串的結束字符

爲止,走過的字符爲字符串數組的最長公共前綴

畫圖幫助理解一下:

構建一個 Trie 樹,以 abcabcdabac 爲例:

代碼實現:

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

3. 更多解法請看 圖解字節&leetcode14:最長公共前綴

2、圖解字節&leetcode151:翻轉字符串裏的單詞

1. 題目

給定一個字符串,逐個翻轉字符串中的每一個單詞。

示例 1:

輸入: "the sky is blue"
輸出: "blue is sky the"

示例 2:

輸入: "  hello world!  "
輸出: "world! hello"
解釋: 輸入字符串能夠在前面或者後面包含多餘的空格,可是反轉後的字符不能包括。

示例 3:

輸入: "a good   example"
輸出: "example good a"
解釋: 若是兩個單詞間有多餘的空格,將反轉後單詞間的空格減小到只含一個。

說明:

  • 無空格字符構成一個單詞。
  • 輸入字符串能夠在前面或者後面包含多餘的空格,可是反轉後的字符不能包括。
  • 若是兩個單詞間有多餘的空格,將反轉後單詞間的空格減小到只含一個。

2. 答案

解法一:正則 + JS API
var reverseWords = function(s) {
    return s.trim().replace(/\s+/g, ' ').split(' ').reverse().join(' ')
};
解法二:雙端隊列(不使用 API)

雙端隊列,故名思義就是兩端均可以進隊的隊列

解題思路:

  • 首先去除字符串左右空格
  • 逐個讀取字符串中的每一個單詞,依次放入雙端隊列的對頭
  • 再將隊列轉換成字符串輸出(已空格爲分隔符)

畫圖理解:

代碼實現:

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(' ')
};

leetcode

3. 更多解法請看 圖解字節&leetcode151:翻轉字符串裏的單詞

3、圖解字節&leetcode160:編寫一個程序,找到兩個單鏈表相交的起始節點

1. 題目

編寫一個程序,找到兩個單鏈表相交的起始節點。

以下面的兩個鏈表:

在節點 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.
  • 在返回結果後,兩個鏈表仍須保持原有的結構。
  • 可假定整個鏈表結構中沒有循環。
  • 程序儘可能知足 O(n) 時間複雜度,且僅用 O(1) 內存。

2. 答案

解法一:標記法(簡單但空間複雜度爲O(n),不符合,僅作參考)

解題思路: 兩次遍歷,先遍歷一個鏈表,給鏈表中的每一個節點都增長一個標誌位,而後遍歷另一個鏈表,遍歷到第一個已被標誌過的節點爲兩鏈表相交的起始節點。

若遍歷完都沒有發現已被標誌過的節點,則兩鏈表不相交,返回 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 ,兩鏈表不相交。

解題步驟:

  • 同步遍歷 A、B 鏈表 pApB ,直到遍歷完其中一個鏈表(短鏈表),如上圖,設A爲長鏈表
  • 那麼此時 A、B 兩遍表的長度差就爲 pA 到鏈尾的長度,此時能夠把 pB 指向長鏈表的表頭 headA ,繼續同步遍歷,直到遍歷完長鏈表
  • 此時,headApB 的長度就爲兩鏈表的長度差,pB 到鏈表的長度與 headB 到鏈尾的長度一致
  • 此時,可將 pA 指向 headB ,而後同步遍歷 pBpA ,直到有相交節點,返回相交節點,不然返回 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)

leetcode

3. 更多解法請看 圖解字節&leetcode160:編寫一個程序,找到兩個單鏈表相交的起始節點

4、leetcode19:刪除鏈表倒數第 n 個結點

1. 題目

給定一個鏈表,刪除鏈表的倒數第 n 個節點,而且返回鏈表的頭結點。

示例:

給定一個鏈表: 1->2->3->4->5, 和 n = 2.
當刪除了倒數第二個節點後,鏈表變爲 1->2->3->5.

說明:

給定的 n 保證是有效的。

進階:

你能嘗試使用一趟掃描實現嗎?

2. 解法:快慢指針

解題思路: 須要刪除鏈表中的倒數第 n 個節點,咱們須要知道的就是倒數第 n+1 個節點,而後刪除刪除倒數第 n+1 節點的後繼節點便可

步驟:

使用 2 個指針:

  • fast 快指針提早走 n+1
  • slow 指針指向當前距離 fast 倒數第 n 個節點, 初始爲 head

而後, fastslow 同步向前走,直到 fast.nextnull

此時,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.nextfast 再前進一步,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)

leetcode

3. 更多解法請看 leetcode19:刪除鏈表倒數第 n 個結點

5、leetcode876:求鏈表的中間結點

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 之間。

2. 解法:快慢指針

解題思路: 快指針一次走兩步,慢指針一次走一步,當快指針走到終點時,慢指針恰好走到中間

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)

leetcode

3. 更多解法請看 leetcode876:求鏈表的中間結點

6、圖解leetcode206:反轉鏈表

1. 題目

示例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL

進階:
你能夠迭代或遞歸地反轉鏈表。你可否用兩種方法解決這道題?

2. 答案

解法一:迭代法

解題思路: 將單鏈表中的每一個節點的後繼指針指向它的前驅節點便可

畫圖實現: 畫圖幫助理解一下

肯定邊界條件: 當鏈表爲 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)

leetcode

3. 更多解法請看 leetcode206:反轉鏈表

7、前端算法集訓營第一期免費加入啦

歡迎關注「前端瓶子君」,回覆「算法」自動加入,從0到1構建完整的數據結構與算法體系!

在這裏,瓶子君不只介紹算法,還將算法與前端各個領域進行結合,包括瀏覽器、HTTP、V八、React、Vue源碼等。

在這裏,你能夠天天學習一道大廠算法題(阿里、騰訊、百度、字節等等)或 leetcode,瓶子君都會在次日解答喲!

⬆️ 掃碼關注公衆號「前端瓶子君」,回覆「算法」便可自動加入 👍👍👍

相關文章
相關標籤/搜索