鏈表演算法題解題技巧歸納總結

  • 2022 年 6 月 21 日
  • 筆記

最近集中刷了一批鏈表的題型,在這裡總結一下解題技巧,以及對應題目的解題思路。

解題思路並不會細緻入微,主要是為了總結歸類,並且希望用幾句話來激發靈感,權當是沒思路時的指引以及以後複習時的提綱了。

還有一些重要或者總會繞暈的經典題目,也在這裡記錄一下程式碼的實現邏輯。

一、解決鏈表題型的兩個技巧

遇到鏈表相關的題,無論問題是什麼,先要想想是不是可以用上以下的兩個技巧。

  1. 哨兵節點
  2. 雙指針

1、哨兵節點

哨兵節點是一個非常常用的鏈表技巧,在處理鏈表邊界問題的場景下,可以減少我們程式碼的複雜度。主要解決的問題如下:

  1. 處理完一條鏈表後,需要返回這個鏈表的頭結點。我們在一開始的時候使用哨兵節點(dummy),讓它的 next 節點指向 head 節點。最後 return 時直接返回 dummy.next 即可。
  2. 在對鏈表進行插入或刪除節點時,使用哨兵節點可以簡化刪除 head 節點或者向 head 前插入節點時的處理邏輯。
  3. 在某些遍歷鏈表的時候,可能會需要同時記錄 pre 節點。當你從 head 節點開始遍歷時,head 是沒有 pre 節點的(為null)。而此時引用哨兵節點,相當於幫助 head 節點初始化了一個 pre 節點,可以方便的解決 pre 節點為空的問題。

因為哨兵節點使用場景很廣泛,所以在這裡就不針對性的列出典型題目了。

2、雙指針

雙指針實在是太好用了,其實不止是鏈表,對於數組題來說,也是非常好用的技巧。

雙指針的主要用法有以下幾種:

  1. 兩個指針在兩條鏈表上行走
  2. 快慢指針,同時前進
  3. 前後指針,前指針先走 n 步,之後兩個指針同時前進

兩個指針在兩條鏈表上行走

有時,你需要將兩條鏈表合併成一條鏈表,或者要將一條鏈表拆分成兩條鏈表。此時,需要用兩個指針分別在兩條鏈表上行走,並處理邏輯。

典型題目:

  • 21. 合併兩個有序鏈表:兩個指針在兩條鏈表上行走,哪個指針指向的節點值大,則將該節點放到合併後的鏈表上,指針繼續向前一步。

  • 86. 分隔鏈表:根據指定的目標值,將一個鏈表先分割成兩個鏈表。因此,需要兩個指針在分割出來的兩個鏈表上行走。之後再將兩個鏈表直接頭尾合併。

  • 160. 相交鏈表:兩個指針在兩條鏈表上行走,當一個指針指向當前列表的結尾時,繼續遍歷另外一個鏈表。這樣可以保證兩個指針指向相同的節點時,這個節點就是相交節點。

快慢指針

快指針的速度是慢指針 n 倍,則當快指針走完時,慢指針停留的地方就是整個鏈表的 1/n 處(大概位置)。一般情況下都是慢指針走一步,快指針走兩步。

典型題目:

  • 876. 鏈表的中間結點:快指針走兩步,慢指針走一步。快指針走到結尾時,則慢指針在中間位置。

  • 141. 環形鏈表:快指針走兩步,慢指針走一步。當快慢指針相遇時,從頭結點再出發一個慢指針,兩個慢指針一起走,再次相遇時,則是環的入口。

  • 234. 迴文鏈表:實際上也是找到鏈表的中間位置,然後去判斷是否為迴文。判斷迴文的邏輯,藉助棧或者反轉後半段鏈表都可以。

  • 148. 排序鏈表:在使用歸併排序時,也是通過快慢指針找到每個子鏈表的中間節點,去歸併處理。

一個指針先走 n 步

因為沒有辦法獲取鏈表的長度,對於尋找倒數第 n 個節點的問題時,這種方法可以一次遍歷找到目標節點。

典型題目:

  • 19. 刪除鏈表的倒數第 N 個結點:一個指針先走 n 步,然後兩個指針同時前進,當先出髮指針遍歷完成時,後出髮指針就在倒數第 n 個節點。

  • 61. 旋轉鏈表:與上一題類似,只是先走的 n 步可能會超過鏈表的總長度,需要取模處理一下。

二、經典鏈表題的解法

反轉鏈表

206. 反轉鏈表 是最經典的鏈表題了。除了進階題 92. 反轉鏈表 II,反轉鏈表也會用於其他鏈表題型的解答中,比如上文所說的 234. 迴文鏈表。

反轉鏈表主要有遞歸法和迭代法兩種解決方法,因為指針指來指去的容易暈頭轉向,所以在這裡記錄標準的解答程式碼。

遞歸法

public ListNode reverseList(ListNode head) {
    // 這個head就是原鏈表的最後一個節點,也是反轉後鏈表的新節點
    if (head == null || head.next == null) {
        return head;
    }
    // 本質上,resultNode就是遞歸到最深處後,一層層返回的新鏈表的頭結點
    ListNode resultNode = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return resultNode;
}

迭代法

public ListNode reverseList(ListNode head) {
    ListNode current = head;
    ListNode pre = null;
    ListNode next;
    while (current != null) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
    }

    return pre;
}

LRU快取

146. LRU 快取 畢竟是個快取,肯定是鍵值對的結構,所以要用到HashMap。

而在讀寫方面,快取是有要求的:函數 get 和 put 必須以 O(1) 的平均時間複雜度運行。

為了達成這個要求,底層的數據結構需要用鏈表來實現。

所以我們實現的 LRU 底層的數據結構是:HashMap +鏈表。

程式碼如下,雖然不夠優雅,但是方便理解邏輯。

public class LRUCache {

    private int maxSize = 0;
    private Node head = new Node();
    private Node tail = head;
    HashMap<Integer, Node> map = new HashMap<>();

    public LRUCache(int capacity) {
        this.maxSize = capacity;
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            // get也是使用了該值,因此需要將該值更新到最近使用的位置
            updateNode(node);
            return node.value;
        }

        return -1;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            updateNode(node);
            return;
        }
        // 新插入的值放到鏈尾
        Node node = new Node(key, value);
        node.prev = tail;
        map.put(key, node);
        tail.next = node;
        tail = node;
        if (map.size() > maxSize) {
            // 此時需要移除最久未使用數據值
            Node removed = head.next;
            head.next = removed.next;
            head.next.prev = head;
            map.remove(removed.key);
        }

    }

    private void updateNode(Node current) {
        // 如果當前節點已經是尾節點,則不需要移動
        if (current.next == null) {
            return;
        }
        // 當前節點的前節點指向當前節點的後節點,即原鏈表中刪除該節點
        current.prev.next = current.next;
        current.next.prev = current.prev;
        // 當前節點放到尾節點
        current.prev = tail;
        current.next = null;
        tail.next = current;
        // 尾節點移動
        tail = current;
    }

    class Node {
        public int key;
        public int value;
        public Node prev;
        public Node next;
        public Node(){}
        public Node(int key,int value) {
            this.key = key;
            this.value = value;
        }
    }
}
Tags: