一、快慢指針
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; }