LeetCode入門指南 之 連結串列

WINLSR 發表於 2021-08-08
LeetCode

83. 刪除排序連結串列中的重複元素

存在一個按升序排列的連結串列,給你這個連結串列的頭節點 head ,請你刪除所有重複的元素,使每個元素 只出現一次 。返回同樣按升序排列的結果連結串列。

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }

        //刪除重複元素但保留一個的情況下頭結點不可能被刪除,故不需要啞結點
        ListNode cur = head;
        
        /**
         *      拿當前結點和當前結點後繼結點比較,
         *      若後繼結點與當前結點值相同則刪除後繼結點,
         *      否則cur後移,直到 cur.next 為 null
         */
        while (cur.next != null) {
            if (cur.next.val == cur.val) {
                //刪除後繼結點後無需後移,否則會漏過連續重複結點(3個及以上重複結點的情況)
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
        }

        return head;
    }
}

82. 刪除排序連結串列中的重複元素 II

給定一個排序連結串列,刪除所有含有重複數字的節點,只保留原始連結串列中 沒有重複出現 的數字。

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }

        //刪除所有重複元素,當頭結點為重複元素時要刪除頭結點,所以需要啞結點
        ListNode dummy = new ListNode(-1);
        //啞結點指向頭結點
        dummy.next = head;

        ListNode cur = dummy;
        while (cur.next != null && cur.next.next != null) {
            //發現重複元素
            if (cur.next.val == cur.next.next.val) {
                //重複值
                int tempVal = cur.next.val;
                
                ListNode temp = cur.next.next;
                //找到最後一個重複元素
                while(temp.next != null && temp.next.val == tempVal) {
                    temp = temp.next;
                }

                //刪除所有重複元素
                cur.next = temp.next;
            } else {
                cur = cur.next;
            }
        }

        return dummy.next;
    }
}

206. 反轉連結串列

反轉一個單連結串列。

思路一:迭代法:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        //始終指向當前結點的前一個結點
        ListNode pre = null;
        //當前結點
        ListNode cur = head;

        while (cur != null) {
            //用來儲存當前結點的下一個結點
            ListNode next = cur.next;

            cur.next = pre;
            pre = cur;
            
            cur = next;
        }

        return pre;
    }
}

思路二:遞迴(鍛鍊遞迴思維):

  • 首先給出函式定義,如此題:ListNode reverseList(ListNode head)

    1. 反轉以head為頭結點的連結串列
    2. 返回反轉後連結串列的頭結點
  • 不要用腦袋去模擬遞迴棧,根據函式的定義去處理遞迴的子問題,要具體到一個結點要做的事情

  • 確定base case

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }

        //base case, 當連結串列只有一個結點時退出遞迴
        if (head.next == null) {
            return head;
        }

        /**
         * 具體到頭結點來說,反轉當前連結串列只需要兩步:
         *  1.反轉以head.next為頭的連結串列
         *  2.將head插入head.next為頭的連結串列反轉之後的連結串列末尾
         */
        ListNode vhead = reverseList(head.next); //根據遞迴函式定義,返回反轉之後的連結串列頭

        //反轉之後head.next位於連結串列尾部,將head插入head.next之後
        head.next.next = head;
        head.next = null;

        return vhead;
    }
}

92. 反轉連結串列 II

反轉從位置 mn 的連結串列。請使用一趟掃描完成反轉。

說明:
1 ≤ mn ≤ 連結串列長度。

示例:

輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL

class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        if (head == null) {
            return head;
        }

        //可能從第一個結點開始反轉,故需要啞結點
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;

        //找到left之前和right之後的結點以便反轉後拼接
        ListNode l = dummyNode, r = dummyNode;
        for (int i = 0; i < left - 1; i++) {        //l為left的前一個結點
            l = l.next;
        }
        for (int i = 0; i < right + 1; i++) {       //r為right後一個結點
            r = r.next;
        }

        ListNode last = reverse(l.next, r);
        l.next.next = r;
        l.next = last;
        
        return dummyNode.next;
    }

    //反轉以left開頭,right結尾的連結串列(左閉右開),返回反轉後的連結串列頭結點
    private ListNode reverse(ListNode left, ListNode right) {
        ListNode pre = null, cur = left, next = left;

        while (cur != right) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }

        return pre;
    }
}

當頭節點不確定是否會被操作的時候,使用啞巴節點

25. K 個一組翻轉連結串列

給你一個連結串列,每 k 個節點一組進行翻轉,請你返回翻轉後的連結串列。

k 是一個正整數,它的值小於或等於連結串列的長度。

如果節點總數不是 k 的整數倍,那麼請將最後剩餘的節點保持原有順序。

進階:

  • 你可以設計一個只使用常數額外空間的演算法來解決此問題嗎?
  • 你不能只是單純的改變節點內部的值,而是需要實際進行節點交換。

遞迴解法(鍛鍊遞迴思維):

class Solution {
    /** 明確遞迴函式的定義:
     *      k個一組反轉以head為頭結點的連結串列,並返回反轉後的頭結點
     */
    public ListNode reverseKGroup(ListNode head, int k) {
        if (head == null) {
            return head;
        }

        /**
         *  按照遞迴函式定義,對第一組結點來說,k個一組反轉整個連結串列分為兩步:
         *      1.反轉第一組結點
         *      2.將反轉後的第一組結點與反轉後的其他組結點拼接
         */

        //找到一組共k個結點,l為第1個結點,r為第k個結點的後一個結點,因為reverse的引數為左閉右開
        ListNode l = head, r = head;
        //移動k次後,r指向第k+1個點
        for (int i = 0; i < k; i++) {
            //base case, 當結點結點不足k個時,無需反轉直接返回
            if (r == null) return l;
            r = r.next;
        }

        ListNode newHead = reverse(l, r);
        //反轉後第一組的頭結點變為尾結點
        l.next = reverseKGroup(r, k);

        return newHead;
    }

    //翻轉left和right之間的結點,左閉右開
    private ListNode reverse(ListNode left, ListNode right) {
        ListNode pre = null, cur = left;

        while (cur != right) {
            ListNode next = cur.next;
            cur.next = pre;

            pre = cur;
            cur = next;
        }
        
        return pre;
    }
}

21. 合併兩個有序連結串列

將兩個升序連結串列合併為一個新的 升序 連結串列並返回。新連結串列是通過拼接給定的兩個連結串列的所有節點組成的。

遞迴解法(鍛鍊遞迴思維):

class Solution {
    /**
     *  定義:
     *      合併連結串列l1, l2,返回合併後的連結串列 
     */
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        //base case
        //若l1或l2為空,說明l1或l2為空連結串列,直接返回另外一個連結串列即為合併後的連結串列
        if (l1 == null) return l2;
        if (l2 == null) return l1;

        
        /**
         * 按照遞迴函式定義,對當前結點l1, l2來說,合併連結串列分為兩部:
         *      1.若l1.val 小於 l2.val,只需將l1.next 和 l2合併之後的連結串列拼接在l1之後
         *      2.反之將l2.next 和 l1 合併之後的連結串列拼接在l2之後
         */

        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

迭代解法:

提示:迭代法根據程式碼在本子上畫一下很容易理解

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummyNode = new ListNode(-1);
        ListNode pre = dummyNode;
        
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                pre.next = l1;
                l1 = l1.next;
            } else {
                pre.next = l2;
                l2 = l2.next;
            }

            pre = pre.next;
        }

        //l1還有剩餘
        if (l1 != null) {
            pre.next = l1;
        } else {
            pre.next = l2;
        }
    
        return dummyNode.next;
    }
}

86. 分隔連結串列

給你一個連結串列的頭節點 head 和一個特定值 x ,請你對連結串列進行分隔,使得所有 小於 x 的節點都出現在 大於或等於 x 的節點之前。

你應當 保留 兩個分割槽中每個節點的初始相對位置。

class Solution {
    /**
     *  審題:將連結串列中小於目標值的結點全部放置在大於等於目標值的結點之前。
     *      顯然,可以遍歷原來的連結串列時將連結串列分為兩個連結串列,一個連結串列儲存所有小於目標值的結點(相對順序不變),
     *      另一個連結串列儲存所有大於等於目標值的結點(相對順序不變);
     *      最後將大的連結串列拼接在小的連結串列之後。
     */
    public ListNode partition(ListNode head, int x) {
        if (head == null) {
            return head;
        }

        //建立兩個啞結點,使用尾插法建立新連結串列 (頭插法會改變相對順序)
        ListNode smallDummy = new ListNode(-1);
        ListNode bigDummy = new ListNode(-1);

        ListNode smallPre = smallDummy;
        ListNode bigPre = bigDummy;

        //遍歷連結串列
        ListNode cur = head;
        while (cur != null) {
            if (cur.val < x) {
                smallPre.next = cur;
                smallPre = smallPre.next;
            } else {
                bigPre.next = cur;
                bigPre = bigPre.next;
            }

            cur = cur.next;
        }

        //bigPre.next可能不為null
        bigPre.next = null;
        //拼接
        smallPre.next = bigDummy.next;

        return smallDummy.next;
    }
}

147. 對連結串列進行插入排序

對連結串列進行插入排序。

class Solution {
    /**
     *      插入排序思路:
     *              用last指標標識已經有序部分的最後一個結點;
     *              遍歷剩餘無序結點,依次插入有序部分;
     *              初始第一個結點為有序結點。
     * 
     */
    public ListNode insertionSortList(ListNode head) {
        //沒有結點或只有一個結點
        if (head == null || head.next == null) {
            return head;
        }

        //方便在頭結點之前插入
        ListNode dummy = new ListNode(-1);
        dummy.next = head;

        ListNode last = head;
        ListNode cur = head.next;

        while (cur != null) {
            //若當前結點大於等於有序部分的最後一個結點則說明本身有序,直接將last後移
            if (cur.val >= last.val) {
                last = cur;
                cur = cur.next;
            } else {
                //在有序部分中找到插入的位置
                ListNode pre = dummy;
                while(pre.next.val < cur.val) {
                    pre = pre.next;
                }

                //cur插入時會修改next
                ListNode next = cur.next;
                //插入
                cur.next = pre.next;
                pre.next = cur;

                last.next = next;
                cur = next;
            }
        }

        return dummy.next;
    }
}

148. 排序連結串列

給你連結串列的頭結點 head ,請將其按 升序 排列並返回 排序後的連結串列

進階:

  • 你可以在 O(n log n) 時間複雜度和常數級空間複雜度下,對連結串列進行排序嗎?

歸併排序(空間複雜度為O(nlogn)):

class Solution {
    public ListNode sortList(ListNode head) {
        //base case 沒有或者只有一個結點,退出遞迴
        if (head == null || head.next == null) {
            return head;
        }

        ListNode mid = getMidNode(head);
        ListNode rightHead = mid.next;
        //切斷
        mid.next = null;

        //分別對左右兩個連結串列進行歸併排序
        ListNode left = sortList(head);
        ListNode right = sortList(rightHead);

        //合併後返回
        return mergeList(left, right);
    }

    //使用快慢指標尋找連結串列中點
    private ListNode getMidNode(ListNode head) {
        //該初始化方式得到的結果中:
        //當結點有偶數個時,slow指向兩個中間結點的前一個結點;
        //當結點有奇數個時,slow指向中間結點。
        ListNode slow = head;
        ListNode fast = head.next;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        return slow;
    }

    //合併兩個有序連結串列
    private ListNode mergeList(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;

    	if (l1.val < l2.val) {
            l1.next = mergeList(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeList(l1, l2.next);
            return l2;
        }
    }
}

快速排序:

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null) {
            return head;
        }
        return quickSort(head, null);
    }

    //對head開頭,end結尾的連結串列進行快排(左閉右開),返回排序後的結點
    private ListNode quickSort(ListNode head, ListNode end) {
        //base case, 沒有結點或只有一個結點預設有序,退出遞迴
        if (head == end || head.next == end) {
            return head;
        }

        ListNode left = head, right = head;     //pivot為head
        ListNode cur = head.next;               //用來遍歷剩餘結點

        while (cur != end) {
            if (cur.val <= head.val) {          //頭插法將小於等於pivot的結點放置在pivot之前
                //cur.next 被改變,先儲存下來
                ListNode next = cur.next;
                
                cur.next = left;
                left = cur;
                
                cur = next;
            } else {                            //尾插法將大於pivot的結點放置在pivot之後
                right.next = cur;
                right = cur;
                cur = cur.next;
            }
        }

        right.next = end; //此步很重要
        ListNode vhead = quickSort(left, head);
        head.next = quickSort(head.next, end);

        return vhead;
    }

}

143. 重排連結串列

給定一個單連結串列 LL0L1 →…→Ln-1Ln
將其重新排列後變為: L0LnL1Ln-1L2Ln-2 →…

你不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。

class Solution {
    /**
     *  思路:
     *      找到中點,反轉後半部分,然後交叉合併為一個連結串列
     */
    public void reorderList(ListNode head) {
        //沒有或只有1、2個結點無需操作
        if (head == null || head.next == null || head.next.next == null) {
            return;
        }

        ListNode midNode = getMidNode(head);
        ListNode nextHalf = midNode.next;
        midNode.next = null;    //切斷前後兩部分連線

        //反轉後半部分連結串列
        ListNode nHead = reverse(nextHalf);

        //合併連結串列
        boolean flag = true;
        ListNode l1 = head, l2 = nHead;

        while (l2 != null) {
            if (flag) {
                ListNode next = l1.next;
                l1.next = l2;
                l1 = next;
            } else {
                ListNode next = l2.next;
                l2.next = l1;
                l2 = next;
            }

            flag = !flag;
        }
    }

    private ListNode getMidNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head.next;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        return slow;
    }

    private ListNode reverse(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;

        while (cur != null) {
            ListNode next = cur.next;

            cur.next = pre;
            pre = cur;

            cur = next;
        }

        return pre;
    }
}

141. 環形連結串列

給定一個連結串列,判斷連結串列中是否有環。

如果連結串列中有某個節點,可以通過連續跟蹤 next 指標再次到達,則連結串列中存在環。

如果連結串列中存在環,則返回 true 。 否則,返回 false

public class Solution {
    /**
     *  思路: 
     *      快慢指標,若慢指標最終追上快指標說明有環
     */
    public boolean hasCycle(ListNode head) {
        //沒有或只有一個結點說明沒有環
        if (head == null || head.next == null) {
            return false;
        }

        ListNode slow = head;
        ListNode fast = head.next;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast) {
                return true;
            }
        }

        return false;
    }
}

142. 環形連結串列 II

給定一個連結串列,返回連結串列開始入環的第一個節點。 如果連結串列無環,則返回 null

思路:快慢指標,快慢指標相遇後,慢指標回到頭,快慢指標步伐一致一起移動,相遇點即為入環點。有興趣的可以自己看下官方題解中的推導。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 沒有結點或只有一個非自環結點
        if (head == null || head.next == null) {
            return null;
        }

        ListNode slow = head;
        ListNode fast = head.next;

        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;

            if (slow == fast) {
                //快指標從第一次相交點下一個節點開始移動, 慢指標重新從頭開始移動
                fast = fast.next;  //注意
                slow = head;

                while (fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }

                // 相遇點即為入環點
                return fast;
            }
        }

        return null;
    }
}

234. 迴文連結串列

請判斷一個連結串列是否為迴文連結串列。

思路:快慢指標找到中點,反轉後半部分,然後依次比較,最後要記得還原連結串列

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        //沒有或只有一個結點
        if (head == null || head.next == null) {
            return true;
        }

        ListNode mid = getMidNode(head);
        ListNode rHead = reverse(mid.next);
        //切斷
        mid.next = null;

        ListNode lTemp = head;
        ListNode rTemp = rHead;

        //右半部分連結串列長度小於等於左半部分連結串列長度
        while (rTemp != null) {
            if (lTemp.val != rTemp.val) {
                return false;
            }
            
            lTemp = lTemp.next;
            rTemp = rTemp.next;
        }

        //還原
        mid.next = reverse(rHead);
        
        return true;
    }

    //對於偶數個結點和奇數個結點,slow都位於右部分的前一個結點
    private ListNode getMidNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head.next;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        return slow;
    }

    //反轉連結串列
    private ListNode reverse(ListNode head) {
        ListNode pre = null, cur = head, next = head;

        while (cur != null) {
            next = cur.next;
            cur.next = pre;

            pre = cur;
            cur = next;
        }

        return pre;
    }
}
  • 推薦使用slow = head;fast = head.next;的方式使用快慢指標
  • 該方式下,結點個數為奇數,slow指向中點;結點個數為偶數,slow指向左邊中點

138. 複製帶隨機指標的連結串列

給你一個長度為 n 的連結串列,每個節點包含一個額外增加的隨機指標 random ,該指標可以指向連結串列中的任何節點或空節點。

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return head;
        }

        //第一次遍歷:先複製所有結點,佔時不管指標域,並將新舊結點通過HashMap關聯起來
        Node cur = head;
        Map<Node, Node> map = new HashMap<>();
        while (cur != null) {
            Node newNode = new Node(cur.val);
            map.put(cur, newNode);

            cur = cur.next;
        }

        //第二次遍歷:通過HashMap關聯各個新結點
        cur = head;
        while (cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);

            cur = cur.next;
        }

        return map.get(head);
    }
}

以上題目整理來自:演算法入門(go語言實現),這裡給出java實現,實現思想有所不同。