算法小技巧 -- 鏈表


一、快慢指針

1、核心思想

【核心思想:】
    采用雙指針完成,一個指針永遠比另一個指針稍快一點。

【常見案例:】
    找到單鏈表的中間節點
    環形鏈表 【單鏈表結構:】
class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }

 

2、案例實現(找到單鏈表的中間節點)

【LeetCode 題目:】
標題:
    876. 鏈表的中間結點
    
題目描述:
    給定一個頭結點為 head 的非空單鏈表,返回鏈表的中間結點。
    如果有兩個中間結點,則返回第二個中間結點。

限制:
    給定鏈表的結點數介於 1 和 100 之間。

測試用例:
    輸入:[1,2,3,4,5]
    輸出:3 

    輸入:[1,2,3,4,5,6]
    輸出:4

【案例分析:】
    假設兩個指針 slow、fast。
    每次指向下一個節點時,fast 都比 slow 快一個節點,
    即 fast 每次跳過兩個節點,slow 每次跳過一個節點。
    當 fast 遍歷到鏈表末尾時,slow 恰好處於鏈表中間節點處。
即
    slow = slow.next;
    fast = fast.next.next;
注:
    鏈表節點為偶數時,存在兩個中間節點,此處以第二個節點作為中間節點。
    
【偽代碼實現:】
public ListNode middleNode(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

 

 

3、案例實現(環形鏈表)

(1)LeetCode 題目

【LeetCode 題目:】
標題:
    141. 環形鏈表
    
題目描述:
    給定一個鏈表,判斷鏈表中是否有環。
    如果鏈表中有某個節點,可以通過連續跟蹤 next 指針再次到達,則鏈表中存在環。 
    如果鏈表中存在環,則返回 true。 否則,返回 false 。
注:
    為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。
    如果 pos 是 -1,則在該鏈表中沒有環。
    pos 不作為參數進行傳遞,僅僅是為了標識鏈表的實際情況(用於表示哪個位置開始會出現環)。

限制:
    鏈表中節點的數目范圍是 [0, 104]
    -105 <= Node.val <= 105
    pos 為 -1 或者鏈表中的一個 有效索引 。

測試用例:
    輸入:head = [3,2,0,-4], pos = 1
    輸出:true
    解釋:鏈表中有一個環,其尾部連接到第二個節點。
    
    輸入:head = [1,2], pos = 0
    輸出:true
    解釋:鏈表中有一個環,其尾部連接到第一個節點。
    
    輸入:head = [1], pos = -1
    輸出:false
    解釋:鏈表中沒有環。

 

(2)案例分析

【案例分析:】
    鏈表存在環,則進行鏈表遍歷時將會進入 死循環,需要設置結束遍歷的條件。
    若不存在環,即使鏈表足夠長,總能遍歷結束,無需設置結束遍歷的條件。
聯想到一個場景:
    晚上寂寞無聊,約哥們去體育館操場跑圈。假設兩個人為 A、B。
    A、B 同時出發,由於身體素質問題,A 跑的比 B 快點。
    經過一段時間后,A 甩了 B 半圈,再經過一段時間后,A 追上了 B。
    只要操場跑道是個圈,A 總能追上 B,只是時間長短的問題。
即:
    A、B 的跑步頻率只要設置的不太離譜,總有重合的一天。
    此處設置 A 為快指針,B 為慢指針。
    A 跑兩米,B 跑一米,當 A 追上了 B,則表示是個環。    

 

(3)偽代碼

【偽代碼:】
public boolean hasCycle(ListNode head) {
    if (head == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

 

 

二、單鏈表的遍歷

1、核心思想

【核心思想:】
    head 與 head.next 一定要區分開。
    每次經過一個節點后,需要跳到下一個節點,
即 
    head = head.next。

【常見實現:】
    使用 while 進行遍歷。
    使用 遞歸 進行遍歷。(類似於樹的 前序、后序 遍歷的實現)
    
【常見案例:】
    正序輸出鏈表(可使用 while、遞歸 實現)
    反序輸出鏈表(可使用 遞歸 實現)

 

2、案例實現(正序輸出鏈表 -- while 實現)

【案例分析:】
    temp 與 temp.next 一定要區分開。
    temp 表示當前所在鏈表節點位置,temp.next 表示當前鏈表節點的下一個位置。
注:
    判斷條件可以為 temp != null 或者 temp.next != null,靈活使用。

【偽代碼:】
public void printList(ListNode head) {
    ListNode temp = head;
    if (temp == null) {
        System.out.println("Empty");
        return;
    }
    while(temp != null) {
        System.out.println(temp.val);
        temp = temp.next;
    }
}

 

 

3、案例實現(遍歷輸出鏈表 -- 遞歸實現)

【案例分析:】
    遍歷輸出鏈表,正序、反序 的代碼非常相似,可等同於 樹結構的 前序、后序遍歷進行理解。
    遞歸 類似於 棧結構,先進后出。入棧的順序就是 正序的,出棧的順序就是 反序的。

【偽代碼:】
public static void printList(ListNode head) {
    if (head == null) {
        System.out.println("Empty");
        return;
    }
    nextNode(head);
}

public static void nextNode(ListNode head) {
    if (head == null) {
        return;
    }
    // System.out.println(head.val); // 正序遍歷輸出
    nextNode(head.next);
    System.out.println(head.val); // 反序遍歷輸出
}

 

 

三、單鏈表反轉

1、核心思想

【核心思想:】
    head 與 head.next 一定要區分開。
    每經過一個節點,確保當前節點的下一個節點指向自己。
即:
    head.next.next = head
    若從頭開始反轉,可以使用 多指針 實現。
    若從尾開始反轉,可以使用 遞歸 實現。

【常見實現:】
    使用 棧 作為中轉站,遍歷鏈表,將值入棧,出棧時構建為一個新的鏈表。
    使用多個指針,從頭開始,逐個節點進行反轉。
    使用 遞歸 進行反轉,從尾開始,逐個節點進行反轉。
    
【常見案例:】
    反轉鏈表
    反轉部分鏈表(指定范圍進行反轉)
    K 個一組翻轉鏈表
    回文鏈表

 

 

2、案例實現(反轉鏈表 -- 棧實現)

【LeetCode 題目:】
標題:
    206. 反轉鏈表

題目描述:
    給你單鏈表的頭節點 head ,請你反轉鏈表,並返回反轉后的鏈表。

限制:
    鏈表中節點的數目范圍是 [0, 5000]
    -5000 <= Node.val <= 5000

測試用例:
    輸入:head = [1,2,3,4,5]
    輸出:[5,4,3,2,1]
    
    輸入:head = [1,2]
    輸出:[2,1]
    
    輸入:head = []
    輸出:[]

【案例分析:】
    使用 棧 作為中轉站,遍歷鏈表,將值入棧,出棧時構建為一個新的鏈表。

【偽代碼:】
public ListNode reverseList(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode temp = head;
    Stack<Integer> stack = new Stack<>();
    while(temp != null) {
        stack.push(temp.val);
        temp = temp.next;
    }
    temp = head;
    while(!stack.isEmpty()) {
        temp.next = new ListNode(stack.pop());
        temp = temp.next;
    }
    return head.next;
}

 

3、案例實現(反轉鏈表 -- 多指針實現)

【LeetCode 題目:】
標題:
    206. 反轉鏈表

【案例分析:】
    此方式從頭向尾執行。
    使用多個指針,假設當前指針為 current,其上一個指針為 pre,下一個指針為 next。
其中:
    pre 用於存儲 當前指針 需要指向的 上一個節點。
    next 用於存儲 當前指針 需要跳到的 下一個節點。
    每次經過一個節點時,保證當前節點指向上一個節點,然后跳轉到下一個節點繼續操作。
即:
    next = current.next;
    current.next = pre;
    pre = current;
    current = next;
    
【偽代碼:】
public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode next = null;
    ListNode current = head;
    while(current != null) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
    }
    return pre;
}

 

 

4、案例實現(反轉鏈表 -- 遞歸實現)

【LeetCode 題目:】
標題:
    206. 反轉鏈表

【案例分析:】
    此方式從尾向頭進行。
    每次經過一個節點時,保證當前節點的下一個節點指向當前節點,移除當前節點指向的下一個節點。
    然后跳轉到上一個節點繼續操作(此過程由遞歸的邏輯實現,返回上一層)。
即
    head.next.next = head;
    head.next = null;

【偽代碼:】
public static ListNode reverseList(ListNode head) {
    if (head == null) {
        return head;
    }
    return nextNode(head);
}

public static ListNode nextNode(ListNode head) {
    if (head.next == null) {
        return head;
    }
    // lastNode 表示最后一個節點位置,沒有被修改,不斷的向遞歸的上層傳遞
    ListNode lastNode = nextNode(head.next);
    head.next.next = head;
    head.next = null;
    return lastNode;
}

 

 

5、案例實現(反轉部分鏈表 -- 多指針)

【LeetCode 題目:】
標題:
    92. 反轉鏈表 II

題目描述:
    給你單鏈表的頭指針 head 和兩個整數 left 和 right ,其中 left <= right 。
    請你反轉從位置 left 到位置 right 的鏈表節點,返回 反轉后的鏈表 。

限制:
    鏈表中節點數目為 n
    1 <= n <= 500
    -500 <= Node.val <= 500
    1 <= left <= right <= n

測試用例:
    輸入:head = [1,2,3,4,5], left = 2, right = 4
    輸出:[1,4,3,2,5]
    
    輸入:head = [5], left = 1, right = 1
    輸出:[5]

【案例分析:】
    反轉指定范圍的鏈表。關鍵就在於 反轉范圍邊界 的四個節點是如何連接的。
    假設反轉范圍邊界節點分別為 a、b、c、d,需要反轉的是 b -> ... -> c 這部分節點。
    開始順序為 head -> ... a -> b -> ... -> c -> d -> ...
    最終需要得到的順序為 head -> ... a -> c -> ... -> b -> d -> ...
    這么看上去就很直觀了,先令 b、c 之間的鏈表節點反轉,然后讓 a 指向 c,b 指向 d 即可完成。
注:
    范圍為 0 時,此時只反轉一個節點,即 b、c 為同一個節點,無需反轉。    
    若從頭開始反轉,即沒有 a 節點,此時 c 直接作為新的頭結點即可。

【偽代碼:】
public ListNode reverseBetween(ListNode head, int left, int right) {
    ListNode first, temp;
    first = temp = head;
    int count = right - left;
    if (count != 0) {
        if (left == 1) {
            // 從頭開始反轉,沒有 a 節點,c 節點即為新的頭結點
            return reverseList(head, count);
        }
        while (temp != null && left != 1) {
            left--;
            // 找到開始反轉的節點的上一個節點,即 a 節點
            first = temp;
            // 找到開始反轉的節點,即 b 節點
            temp = temp.next;
        }
        // 從中間位置開始反轉,需要連接上反轉范圍的鏈表的新頭節點,即 a -> c
        first.next = reverseList(temp, count);
    }
    return head;
}

public ListNode reverseList(ListNode head, int count) {
    ListNode first, pre, next, current;
    pre = next = null;
    first = current = head;
    // 反轉指定范圍的鏈表
    while(current != null && count != 0) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
        count--;
    }
    // 指向反轉范圍的新尾節點的下一個節點,即 b -> d
    first.next = current.next;
    // 返回反轉范圍鏈表的新頭結點,即 c 節點
    current.next = pre;
    return current;
}

 

 

6、案例實現(K 個一組翻轉鏈表)

【LeetCode 題目:】
標題:
    25. K 個一組翻轉鏈表

題目描述:
    給你一個鏈表,每 k 個節點一組進行翻轉,請你返回翻轉后的鏈表。
    k 是一個正整數,它的值小於或等於鏈表的長度。
    如果節點總數不是 k 的整數倍,那么請將最后剩余的節點保持原有順序。
    你不能只是單純的改變節點內部的值,而是需要實際進行節點交換。

限制:
    列表中節點的數量在范圍 sz 內
    1 <= sz <= 5000
    0 <= Node.val <= 1000
    1 <= k <= sz

測試用例:
    輸入:head = [1,2,3,4,5], k = 2
    輸出:[2,1,4,3,5]
    
    輸入:head = [1,2,3,4,5], k = 3
    輸出:[3,2,1,4,5]
    
    輸入:head = [1,2,3,4,5], k = 1
    輸出:[1,2,3,4,5]
    
    輸入:head = [1], k = 1
    輸出:[1]
    
【案例分析:】
    k 個節點一組去反轉,原理與 部分節點反轉 類似,只是多了重復調用的過程。
    關注點同樣是 a、b、c、d 四個節點指向的問題。
注:
    此處從頭開始反轉,即第一次沒有 a 節點,直接使用 c 節點作為新的頭節點。

【偽代碼:】
public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) {
        return head;
    }
    ListNode temp = head;
    // 找到需要反轉的節點范圍
    for (int i = 0; i < k; i++) {
        // 若反轉節點范圍不足,則不進行反轉
        if (temp == null) {
            return head;
        }
        temp = temp.next;
    }
    // 獲取反轉節點后的新頭結點
    ListNode newHead = reverseList(head, k - 1);
    // 進行下一次反轉,用於獲取下一部分的新頭節點,並令當前尾節點指向下一次獲取的頭結點
    head.next = reverseKGroup(temp, k);
    return newHead;
}

public ListNode reverseList(ListNode head, int count) {
    //ListNode first, pre, next, current;
    ListNode pre, next, current;
    pre = next = null;
    //first = current = head;
    current = head;
    while (current != null && count != 0) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
        count--;
    }
    //first.next = current.next;
    current.next = pre;
    return current;
}

 

 

7、案例實現(回文鏈表)

【LeetCode 題目:】
標題:
    劍指 Offer II 027. 回文鏈表
    
題目描述:
    給定一個鏈表的 頭節點 head ,請判斷其是否為回文鏈表。
    如果一個鏈表是回文,那么鏈表節點序列從前往后看和從后往前看是相同的。
    能否用 O(n) 時間復雜度和 O(1) 空間復雜度解決此題?
    
限制:
    鏈表 L 的長度范圍為 [1, 105]
    0 <= node.val <= 9

測試用例:
    輸入: head = [1,2,3,3,2,1]
    輸出: true
    
    輸入: head = [1,2]
    輸出: fasle
    
【案例分析:】
    判斷回文,常用方法就是:從兩側向中間逼近 或者 從中間向兩側展開 或者 暴力反轉構建新的鏈表,然后逐個比較。
從兩側向中間逼近(效率低):
    由於鏈表的結構特殊,無法直接從 尾節點向頭結點遍歷,
    但可以使用 遞歸的方式,模擬 棧 的使用,先遞歸到最后一個節點,然后逐級回退到上層節點。
從中間向兩側逼近(效率稍高):
    先找到鏈表的中點,可以使用快慢指針。
    然后對 前半段鏈表進行 反轉,這樣就可以從 中間節點 向兩側展開並比較。
    當然,也可以反轉后半段鏈表,然后從頭結點開始比較。
暴力反轉構建新鏈表(需要額外空間):
    直接對鏈表進行反轉,構建一個新鏈表。
    然后 新鏈表、舊鏈表 逐個進行值比較。

【偽代碼:(從兩側向中間逼近)】
public static ListNode first = null;
public static boolean isPalindrome(ListNode head) {
    first = head;
    return nextNode(head);
}

public static boolean nextNode(ListNode head) {
    if (head == null) {
        return true;
    }
    boolean flag = nextNode(head.next);
    if (flag && head.val == first.val) {
        first = first.next;
        return flag;
    }
    return false;
} 

【偽代碼:(從中間向兩側展開,反轉后半段鏈表)】
public static boolean isPalindrome(ListNode head) {
    // 找中間節點
    ListNode middleNode = middleNode(head);
    // 反轉后半段鏈表
    ListNode newNode = reverseList(middleNode);
    // 從頭結點 開始比較 新的后半段鏈表
    while(newNode != null && head != null) {
        if (newNode.val != head.val) {
            return false;
        }
        newNode = newNode.next;
        head = head.next;
    }
    return true;
}

public static ListNode middleNode(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM