鏈表+6道前端算法面試高頻題解

數組在上一篇的專欄,中咱們進行了回顧和刷題。javascript

鏈表

趁熱打鐵,咱們來對比數組來學習鏈表。前端

首先要明確的是,鏈表和數組的底層存儲結構不一樣,數組要求存儲在一塊連續的內存中,而鏈表是經過指針將一組零散的內存塊串聯起來。java

可見鏈表對內存的要求下降了,可是隨機訪問的性能就沒有數組好了,須要 O(n) 的時間複雜度。node

下圖中展現了單鏈表及單鏈表的添加和刪除操做,其實鏈表操做的本質就是處理鏈表結點之間的指針。git

在刪除鏈表結點的操做中,咱們只須要將須要刪除結點的前驅結點的 next 指針,指向其後繼便可。這樣,當前被刪除的結點就被丟棄在內存中,等待着它的是被垃圾回收器清除。github

爲了更便於你理解,鏈表能夠類比現實生活中的火車,火車的每節車箱就是鏈表的一個個結點。車箱之間相互鏈接,能夠添加或者移除掉。春運時,客運量比較大,列車通常會加掛車箱。面試

鏈表的結點結構由數據域指針域組成,在 JavaScript 中,以嵌套的對象形式實現。數組

{
    // 數據域
    val: 1,
    // 指針域
    next: {
        val:2,
        next: ...
    }
}

名詞科普

  • 頭結點:頭結點用來記錄鏈表的基地址,是咱們遍歷鏈表的起點
  • 尾結點:尾結點的指針不是指向下一個結點,而是指向一個空地址 NULL
  • 單鏈表:單鏈表是單向的,它的結點只有一個後繼指針 next 指向後面的結點,尾結點指針指向空地址
  • 循環鏈表:循環鏈表的尾結點指針指向鏈表的頭結點
  • 雙向鏈表:雙向鏈表支持兩個方向,每一個結點不止有一個後繼指針 next 指向後面的結點,還有一個前驅指針 prev 指向前面的結點,雙向鏈表會佔用更多的內存,可是查找前驅節點的時間複雜度是 O(1) ,比單鏈表的插入和刪除操做都更高效
  • 雙向循環鏈表

循環鏈表

雙向鏈表

雙向循環鏈表

開啓刷題

年初立了一個 flag,上面這個倉庫在 2021 年寫滿 100 道前端面試高頻題解,目前進度已經完成了 50%數據結構

若是你也準備刷或者正在刷 LeetCode,不妨加入前端食堂,一塊兒並肩做戰,刷個痛快。性能

瞭解了鏈表的基礎知識後,立刻開啓咱們愉快的刷題之旅,我整理了 6 道高頻的 LeetCode 鏈表題及題解以下。

01 刪除鏈表的倒數第 N 個結點

原題連接

快慢指針

先明確,刪除倒數第 n 個結點,咱們須要找到倒數第 n+1 個結點,刪除其後繼結點便可。

  1. 添加 prev 哨兵結點,處理邊界問題。
  2. 藉助快慢指針,快指針先走 n+1 步,而後快慢指針同步往前走,直到 fast.next 爲 null。
  3. 刪除倒數第 n 個結點,返回 prev.next。
const removeNthFromEnd = function(head, n) {
    let prev = new ListNode(0), fast = prev, slow = prev;
    prev.next = head;
    while (n--) {
        fast = fast.next;
    }
    while (fast && fast.next) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return prev.next;
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

02 合併兩個有序鏈表

原題連接

思路

  1. 使用遞歸來解題
  2. 將兩個鏈表頭部較小的一個與剩下的元素合併
  3. 當兩條鏈表中的一條爲空時終止遞歸

關鍵點

  • 掌握鏈表數據結構
  • 考慮邊界狀況

複雜度分析

n + m 是兩條鏈表的長度

  • 時間複雜度:O(m + n)
  • 空間複雜度:O(m + n)
const mergeTwoLists = function (l1, l2) {
    if (l1 === null) {
        return l2;
    }
    if (l2 === null) {
        return l1;
    }
    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}

03 兩兩交換鏈表中的節點

原題連接

先明確想要交換節點共須要有三個指針進行改變。

  1. 因此咱們須要在鏈表頭部添加一個哨兵節點
  2. 循環中首先操做三個指針完成節點交換
  3. 指針右移,進行下一對節點的交換

迭代 + 哨兵節點

const swapPairs = (head) => {
  const dummy = new ListNode(0);
  dummy.next = head; // 頭部添加哨兵節點
  let prev = dummy;

  while (head && head.next) {
    const next = head.next; // 保存 head.next
    head.next = next.next;
    next.next = head;
    prev.next = next;
    // 下面兩個操做將指針更新
    prev = head;      
    head = head.next;
  }
  return dummy.next;
};
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

遞歸

若是你對遞歸還以爲掌握的不夠透徹,能夠移步個人這篇專欄

回到本題的遞歸解法:

  1. 寫遞歸解法的話,老套路,先明確終止條件,鏈表中沒有節點或只有一個節點時沒法進行交換。
  2. 接下來遞歸的進行兩兩交換節點並更新指針關係。
  3. 返回新鏈表的頭節點 newHead。
const swapPairs = function (head) {
    // 遞歸終止條件
    if (head === null || head.next === null) {
        return head;
    }
    // 得到第 2 個節點
    let newHead = head.next;
    // 將第 1 個節點指向第 3 個節點,並從第 3 個節點開始遞歸
    head.next = swapPairs(newHead.next);
    // 將第 2 個節點指向第 1 個節點
    newHead.next = head;
    return newHead;
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(n)

04 環形鏈表

原題連接

快慢指針

  1. 使用快慢不一樣的兩個指針遍歷,快指針一次走兩步,慢指針一次走一步。
  2. 若是沒有環,快指針會先到達尾部,返回 false。
  3. 若是有環,則必定會相遇。
const hasCycle = function(head) {
    if (!head || !head.next) return false;
    let fast = head.next;
    let slow = head;
    while (fast !== slow) {
        if (!fast || !fast.next) {
            return false;
        }
        fast = fast.next.next;
        slow = slow.next;
    }
    return true;
};
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

標記法

遍歷鏈表,經過 flag 標記判斷是否有環,若是標記存在則有環。(走過的地方插個旗子作標記)

const hasCycle = function(head) {
    while (head) {
        if (head.flag) {
            return true;
        } else {
            head.flag = true;
            head = head.next;
        }
    }
    return false;
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

05 反轉鏈表

原題連接

迭代

  1. 初始化哨兵節點 prev 爲 null,及當前節點 curr 指向頭節點。
  2. 開始迭代,記錄 next 指針留備後用,反轉指針。
  3. 推動指針繼續迭代,最後返回新的鏈表頭節點 prev。
const reverseList = function(head) {
    let prev = null;
    let curr = head;
    while (curr !== null) {
        // 記錄 next 節點
        let next = curr.next;
        // 反轉指針
        curr.next = prev;
        // 推動指針
        prev = curr;
        curr = next;
    }
    // 返回翻轉後的頭節點
    return prev;
};
  • 時間複雜度: O(n)
  • 空間複雜度: O(1)

遞歸

const reverseList = function(head) {
    if (!head || !head.next) return head;
    // 記錄當前節點的下一個節點
    let next = head.next;
    let reverseHead = reverseList(next);
    // 操做指針進行反轉
    head.next = null;
    next.next = head;
    return reverseHead;
};
  • 時間複雜度: O(n)
  • 空間複雜度: O(n)

06 鏈表的中間結點

原題連接

快慢指針

老套路,藉助快慢指針,fast 一次走兩步,slow 一次走一步,當 fast 到達鏈表末尾時,slow 就處於鏈表的中間點了。

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

image

相關文章
相關標籤/搜索