LeetCode 總結 - 搞定 Linked List 面試題

  • 鏈表刪除
    • [203] Remove Linked List Elements
    • [19] Remove Nth Node From End of List
    • [83] Remove Duplicates from Sorted List
    • [82] Remove Duplicates from Sorted List II
  • 鏈表反轉與旋轉
    • [206] Reverse Linked List
    • [92] Reverse Linked List II
    • [24] Swap Nodes in Pairs
    • [25] Reverse Nodes in k-Group
    • [141] Linked List Cycle
    • [142] Linked List Cycle II
  • 鏈表排序
    • [148] Sort List
    • [147] Insertion Sort List
  • 鏈表操作
    • [143] Reorder List
    • [61] Rotate List
    • [86] Partition List
    • [328] Odd Even Linked List
    • [725] Split Linked List in Parts
    • [234] Palindrome Linked List
  • 進位加法
    • [2] Add Two Numbers
    • [445] Add Two Numbers II
  • 鏈表合并
    • [21] Merge Two Sorted Lists
    • [23] Merge k Sorted Lists
    • [160] Intersection of Two Linked Lists
  • 其他
    • [138] Copy List with Random Pointer
    • [109] Convert Sorted List to Binary Search Tree

鏈表刪除

[203] Remove Linked List Elements 移除鏈表元素

https://leetcode.com/problems/remove-linked-list-elements

問題:刪除鏈表中等于給定值val的所有節點。
Example:
Given: 1 –> 2 –> 6 –> 3 –> 4 –> 5 –> 6, val = 6
Return: 1 –> 2 –> 3 –> 4 –> 5

思路:非常簡單,刪除就是把要刪除的節點前一個的 next 指向別的,把它自己的 next 連上去。注意刪除掉next結點的話就不用prev = prev.next了,刪除結點時prev.next發生變化所以不用做什么,只有不刪除時才向后移動,否則有可能空指針!

public ListNode removeElements(ListNode head, int val) {
    if (head == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    head = dummy;
    while (head.next != null) {
        if (head.next.val == val) {
            head.next = head.next.next;
        } else {
            head = head.next;
        }
    }
    return dummy.next;
}

參考講解:

[19] Remove Nth Node From End of List 刪除鏈表中倒數第n個節點

https://leetcode.com/problems/remove-nth-node-from-end-of-list

問題:刪除鏈表中倒數第n個節點。
Example:
Given: 1->2->3->4->5, and n = 2
Return: 1->2->3->5

思路:利用雙指針快速定位到要刪除節點的位置。先用一個fast指針走n步,然后再來一個slow指針從頭開始和fast同時向后走,當fast走到鏈表末尾的時候,slow指針即為倒數第n個結點。

public ListNode removeNthFromEnd(ListNode head, int n) {
    if (head == null || head.next == null || n <= 0) return head;
    // 設立頭結點
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    // 初始化slow,fast
    ListNode slow = dummy, fast = dummy;
    // fast指針先走n步
    for (int i = 0; i < n; i++) {
        fast = fast.next;
    }
    // 兩個指針同時移動直到p2到達最后
    while (fast.next != null) {
        slow = slow.next;
        fast = fast.next;
    }
    // 刪除并返回
    slow.next = slow.next.next;
    return dummy.next;
}

參考講解:

[83] Remove Duplicates from Sorted List 刪除重復元素

https://leetcode.com/problems/remove-duplicates-from-sorted-list

問題:Given a sorted linked list, delete all duplicates such that each element appear only once. For example,
Given 1->1->2, return 1->2.
Given 1->1->2->3->3, return 1->2->3.

思路:從這道題我們可以學習一下刪除結點的兩種方法,可以說各種所長。

  • 第一種是向后比較結點,發現重復就刪掉。因為可能會刪掉后面的結點,所以一定要注意cur的判空條件。當正常遍歷時,cur可能為空,當刪掉了后面結點時cur.next可能為空,都要判斷。
  • 第二種是向前比較結點,即用prev記錄前一個結點的值,發現相同就刪掉當前結點,判空條件簡單一些,但一定注意prev和cur的更新。因為這道題肯定不會刪除head,所以也就沒用到dummy頭結點。
// Version-1: Compare cur and cur.next without prev
// Note both cur and cur.next could reach null
public ListNode deleteDuplicates(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode cur = head;
    while (cur != null && cur.next != null) {
        if (cur.val == cur.next.val) {
            // 直接刪除掉后面那個重復的節點,并且cur不變
            cur.next = cur.next.next;
        } else {
            // 只有當比較的兩個元素不同,cur才移動到下一個節點
            cur = cur.next;
        }
    }
    return head;
}

// Version-2: compare cur and prev
// Note update of prev and cur
public ListNode deleteDuplicates2(ListNode head) {
    if (head == null || head.next == null) return head;    
    // Invariant: node prior to prev (inclusive) has no duplicates
    ListNode prev = head;    
    ListNode cur = head.next;
    while (cur != null) {
        if (cur.val == prev.val) {
            prev.next = cur.next;
            cur = prev.next;
        } else {
            prev = cur;
            cur = cur.next;
        }
    }
    return head;
}

參考講解:

[82] Remove Duplicates from Sorted List II 刪除所有重復元素

https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii
https://www.nowcoder.com/questionTerminal/fc533c45b73a41b0b44ccba763f866ef

問題:給定一個有序的鏈表,刪除該鏈表中所有有重復的節點,重復的結點不保留,返回鏈表頭指針。例如:
給定 1->2->3->3->4->4->5,則返回 1->2->5
給定 1->1->1->2->3,則返回 2->3

思路:因為要刪除所有duplicate,而不是只保留一份,所以while循環里必須嵌套循環,發現duplicate就一刪到底。如果只用一層循環,那么效果就跟第83題一樣了,必須保留一份duplicate,否則下一輪循環時不知道之前重復的是哪個結點。

public ListNode deleteDuplicates(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    head = dummy;
    while (head.next != null && head.next.next != null) {
        if (head.next.val == head.next.next.val) {
            int duplicate = head.next.val;
            while (head.next != null && head.next.val == duplicate) {
                head.next = head.next.next;
            }
        } else {
            head = head.next;
        }
    }
    return dummy.next;
}

參考講解:

鏈表反轉與旋轉

[206] Reverse Linked List 反轉鏈表

https://leetcode.com/problems/reverse-linked-list
https://www.nowcoder.com/questionTerminal/75e878df47f24fdc9dc3e400ec6058ca

問題:輸入一個鏈表,反轉鏈表后,輸出鏈表的所有元素。

思路:最簡單的就是建一個Dummy node,然后不斷地將原來List的Node插入到dummy node的后面, 但是這樣需要了額外的空間。
更好的方法是用一個variable pre保存前一個node,一個cur保存現在的Node,不斷地改變這兩個node 的指針關系,并且將pre和cur更新向下兩個點。

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    // 前驅指針prev初始化為空
    ListNode prev = null;
    ListNode cur = head;
    // 訪問某個節點cur.next時,要檢驗cur是否為null
    while (cur != null) {
        ListNode next = cur.next;   // next保存著原來cur.next的地址
        cur.next = prev;            // 使cur指向prev,這樣就與后面的鏈表斷開了
        prev = cur;                 // prev指針往后移
        cur = next;                 // cur指針往后移
    }
    // 當cur為null時,prev即為鏈表尾節點,直接返回作為新反轉鏈表的頭
    return prev;
}

// 劍指Offer版本
public ListNode reverseList2(ListNode head) {
    if (head == null || head.next == null) return head;
    // 逆置后的頭結點
    ListNode reversedHead = null;
    ListNode prev = null;
    // 當前頭結點
    ListNode cur = head;
    while (cur != null) {
        // 保存后繼
        ListNode next = cur.next;

        // next為null的節點為尾節點(翻轉后的頭結點一定是原始鏈表的尾結點)
        if (next == null) reversedHead = cur;

        // 逆轉的過程,并且能將頭結點的 prev 置為 NULL
        cur.next = prev;

        // 指針往后移
        prev = cur; // 前繼結點到現任節點,勿忘斷鏈的情形,需要使用 pre 指針保存狀態,pre 等價于是后移一個結點
        cur = next; // 現任節點到下一結點,cur 后移一個結點
    }
    return reversedHead;
}

// dummy node
public ListNode reverseList3(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    ListNode cur = head;
    while (cur != null) {
        ListNode next = cur.next;
        cur.next = dummy.next;
        dummy.next = cur;
        cur = next;
    }
    return dummy.next;
}

參考講解:

[92] Reverse Linked List II 反轉部分鏈表

https://leetcode.com/problems/reverse-linked-list-ii

問題:給定了起始位置m和結束位置n,反轉這個區間內的鏈表。

思路:采用頭插法,不斷地把要反轉的節點插到反轉區間頭節點的前面。重點就是記錄第m個結點的前驅結點和第n個結點的后續結點。

public ListNode reverseListBetween(ListNode head, int m, int n) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode prev = dummy;
    // 找反轉區間頭節點的前驅,即1->2->3->4->5->NULL中的1,循環過后prev指向1、cur指向2
    for (int i = 0; i < m - 1; i++) {
        prev = prev.next;
    }
    // cur此時是反轉區間的頭節點
    ListNode cur = prev.next;
    // 在指定區間內不斷進行反轉
    for (int i = 0; i < n - m; i++) {
        ListNode temp = cur.next;
        cur.next = temp.next;       // 越過cur.next,指向cur.next.next,即本來2->3,現在變成了2->4
        temp.next = prev.next;      // temp指向prev.next,即本來3->4,現在變成了3->2
        prev.next = temp;           // 即本來1->2,現在變成了1->3
        // 經過以上步驟變成了1->3->2->4->5
        // 再經過一次最后變成了1->4->3->2->5
    }
    return dummy.next;
}

參考講解:

[24] Swap Nodes in Pairs 成對交換鏈表節點

https://leetcode.com/problems/swap-nodes-in-pairs

問題:成對交換鏈表節點。也就是說,節點1、2交換,節點3、4交換,節點5、6交換...
For example,
Given 1->2->3->4, you should return the list as 2->1->4->3.

思路:每次跳兩個節點,后一個接到前面,前一個接到后一個的后面,最后現在的后一個(也就是原來的前一個)接到下下個結點(如果沒有則接到下一個)。

image
public ListNode swapPairs(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode l1 = dummy;
    ListNode l2 = head;
    while (l2 != null && l2.next != null) {
        // 保存下一對的首節點
        ListNode temp = l2.next.next;
        l1.next = l2.next;
        l2.next.next = l2;
        l2.next = temp;
        l1 = l2;
        l2 = l2.next;
    }
    return dummy.next;
}

參考講解:

[25] Reverse Nodes in k-Group 每k個節點翻轉

https://leetcode.com/problems/reverse-nodes-in-k-group

問題:將鏈表按每k個一組進行區間內翻轉,Swap Nodes in Pairs其實是這道題k=2的特殊情況。

思路:


image
public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode prev = dummy;
    while (prev != null) {
        // 當return null,說明反轉操作已經完成了(不用reverse)
        prev = reverse(prev, k);
    }
    return dummy.next;
}

public ListNode reverse(ListNode prev, int k) {
    ListNode last = prev;
    // last指針往后移動k+1步,也就是位于反轉區間后一個位置
    for (int i = 0; i < k + 1; i++) {
        last = last.next;
        // last為null了,反轉區間還如果不夠k個元素,就返回null
        if (i != k && last == null) return null;
    }
    // 指向反轉區間首元素,也就是逆轉后的尾元素
    ListNode tail = prev.next;
    // 跨過首元素,從第二個開始進行鏈表頭插,也就是把cur提到tail的前面
    ListNode cur = prev.next.next;
    // 當cur移到last,說明要反轉的區間已操作完畢
    while (cur != last) {
        // 暫存next指針
        ListNode next = cur.next;
        // 2->3 變成 2->1
        cur.next = prev.next;
        // dummy->1 變成 dummy->2
        prev.next = cur;
        // 1->2 變成 1->3
        tail.next = next;
        // 接著cur后移,以處理下一個節點
        cur = next;
    }
    // tail將會是下一個子序列的prev
    return tail;
}

參考講解:

[141] Linked List Cycle 判斷鏈表是否有環

https://leetcode.com/problems/linked-list-cycle

問題:給定一個鏈表,判斷該鏈表是否有環。

思路:快慢指針同時從鏈表的頭結點出發,快指針fast步長為2,慢指針slow步長為1,如不存在環,快指針到達鏈表尾部遇到null退出;如果存在環,則某個時刻兩者一定在環里相遇(fast==slow)。從而檢測到鏈表中有環。

image
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        // fast如果和slow相遇了,證明有環
        if (slow == fast) return true;
    }
    // fast如果走到了null,證明沒有環
    return false;
}

參考講解:

[142] Linked List Cycle II 找到環中的第一個節點

https://leetcode.com/problems/linked-list-cycle-ii
https://www.nowcoder.com/questionTerminal/253d2c59ec3e4bc68da16833f79a38e4

問題:如果一個鏈表中包含環,請找出該鏈表的環的入口結點。

思路:前面還是一樣,讓快慢指針同時走直到在環內相遇。接下來,讓快指針回到鏈表的頭部重新走,步長變成了1,那么當兩者再次相遇的時候,就是環路的入口了。

public ListNode detectCycle(ListNode head) {
    if (head == null || head.next == null) return null;
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        // 如果相遇了
        if (fast == slow) {
            // 從頭有個指針開始走了
            ListNode slow2 = head;
            // 兩個指針相遇時也就剛好到達了環的第一個節點,參照博客分析
            while (slow != slow2) {
                slow = slow.next;
                slow2 = slow2.next;
            }
            return slow;
        }
    }
    return null;
}

參考講解:

鏈表排序

[148] Sort List 鏈表排序

https://leetcode.com/problems/sort-list

問題:排序一個鏈表,時間復雜度為O(nlogn)。

思路:題目限定了時間必須為O(nlogn),符合要求只有快速排序,歸并排序,堆排序,而根據單鏈表的特點,最適于用歸并排序。

public ListNode sortList(ListNode head) {
    if (head == null || head.next == null) return head;    // 如果為空或只有一個點,直接return
    ListNode mid = findMiddle(head);        // 找中點
    ListNode right = sortList(mid.next);    // 對mid的右邊鏈表先排序
    mid.next = null;                        // 這時候才把它斷開
    ListNode left = sortList(head);         // 再對mid的左邊鏈表排序
    return merge(left, right);
}

private ListNode findMiddle(ListNode head) {   // 快慢指針
    ListNode slow = head, fast = head.next;    // 因為不知道新的頭是誰,所以要使用dummy node
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    return slow;
}

private ListNode merge(ListNode head1, ListNode head2) {
    ListNode dummy = new ListNode(0);
    ListNode tail = dummy;     // 尾指針指向dummy node
    while (head1 != null && head2 != null) {    // 兩個頭是否為空
        if (head1.val < head2.val) {  // 如果左邊的頭小
            tail.next = head1;     // 把左邊頭放到tail里
            head1 = head1.next;
        } else {                        // 如果右邊的頭小
            tail.next = head2;     // 把右邊頭放到tail里
            head2 = head2.next;
        }
        tail = tail.next;  //tail往后移
    }
    if (head1 != null) {        // 看左邊頭還是右邊頭沒分完就分過去
        tail.next = head1;
    } else {
        tail.next = head2;
    }
    return dummy.next;
}

參考講解:

[147] Insertion Sort List 鏈表插入排序

https://leetcode.com/problems/insertion-sort-list

問題:使用插入排序來排序一個鏈表。

思路:數組版本的插入排序會不斷向前比較,找到合適位置后右移其他大于當前的數據,然后再處理下一個。而鏈表版本則有兩點不同:一是單向鏈表沒法向前比較,所以變通一下,我們每次都從頭往后比較,找到大于當前結點值的位置;其二是把當前結點插入到那個位置的操作可以說是普通插入的復雜版本,因為插入的元素本身也有前后結點連接著。所以,遍歷當前結點的指針和前面找位置的指針都必須是前一個結點(prev),否則會產生Cycle導致死循環。還有個有趣的現象是,當把插入完成后,當前結點無需移動,因為后面的元素在一點點減少,循環會自己中止的。但注意如果當前結點在前半部分已是最大,無需移動時,這時需要手動移動當前指針以防死循環。

public ListNode insertionSortList(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode cur = head;
    ListNode temp = null, prev = null;
    while (cur != null && cur.next != null) {
        if (cur.val <= cur.next.val) {
            cur = cur.next;
        } else {
            temp = cur.next;
            cur.next = temp.next;
            prev = dummy;
            while (prev.next.val <= temp.val) {
                prev = prev.next;
            }
            temp.next = prev.next;
            prev.next = temp;
        }
    }
    return dummy.next;
}

public ListNode insertionSortList2(ListNode head) {
    if (head == null) return null;
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode cur = head;
    while (cur.next != null) {
        // Find where to insert cur.next, or stop at cur
        ListNode pos = dummy;
        while (pos.next.val < cur.next.val) {
            pos = pos.next;
        }
        //  pos(a),pos.next(b),...cur(c),cur.next(d),cur.next.next(e)
        //  => a,d,b,...,c,e
        if (pos != cur) {
            ListNode tmp = pos.next;
            pos.next = cur.next;
            cur.next = cur.next.next;
            pos.next.next = tmp;
        } else {
            cur = cur.next;
            // error1: cur.next is updated already above, but it must update here!
        }
    }
    return dummy.next;
}

參考講解:

鏈表操作

[143] Reorder List 重排鏈表

https://leetcode.com/problems/reorder-list

問題:Given a singly linked list L: L0→L1→…→L(n-1)→Ln, reorder it to: L0→Ln→L1→L(n-1)→L2→L(n-2)→…
For example, Given {1,2,3,4}, reorder it to {1,4,2,3}.

思路:這是一道比較綜合的鏈表操作的題目,要按照題目要求給鏈表重新連接成要求的結果。其實理清思路也比較簡單,分三步完成:(1)將鏈表切成兩半,也就是找到中點,然后截成兩條鏈表;(2)將后面一條鏈表進行reverse操作,就是反轉過來;(3)將兩條鏈表按順序依次merge起來。

public void reorderList(ListNode head) {
    if (head == null || head.next == null) return;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode temp = null;
    ListNode slow = head, fast = head;
    ListNode l1 = head;
    while (fast != null && fast.next != null) {
        temp = slow;
        slow = slow.next;
        fast = fast.next.next;
    }
    // 截斷后半部分
    temp.next = null;
    ListNode l2 = reverse(slow);
    merge(l1, l2);
}

public ListNode reverse(ListNode head) {
    ListNode prev = null;
    while (head != null) {
        ListNode next = head.next;
        head.next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

public void merge(ListNode l1, ListNode l2) {
    while (l1 != l2) {
        ListNode n1 = l1.next;
        ListNode n2 = l2.next;
        l1.next = l2;
        if (n1 == null) break;
        l2.next = n1;
        l1 = n1;
        l2 = n2;
    }
}

參考講解:

[61] Rotate List 旋轉鏈表

https://leetcode.com/problems/rotate-list

問題:把后k個rotate到list前面去,k可以超過list本身長度。
For example:
Given 1->2->3->4->5->NULL and k = 2,
return 4->5->1->2->3->NULL.

思路:用walker-runner定位到要旋轉的那個結點,然后將下一個結點設為新表頭,并且把當前結點設為表尾。

image
public ListNode rotateRight(ListNode head, int k) {
    if (head == null || head.next == null) return head;

    ListNode index = head;
    int len = 1;
    // 得到鏈表長度
    while (index.next != null) {
        index = index.next;
        len++;
    }
    // 因為k可能大于鏈表長度len,所以需要取余處理
    k %= len;

    // 鏈表首尾連成一個環
    index.next = head;
    // 得到新的鏈表頭
    for (int i = 1; i < len - k; i++) {
        head = head.next;
    }
    // res是新的head
    ListNode res = head.next;
    // 斷開環
    head.next = null;
    return res;
}

參考講解:

[86] Partition List 劃分鏈表

https://leetcode.com/problems/partition-list

問題:劃分一個鏈表,把所有小于給定值的節點都移到前面,大于該值的節點順序不變。

思路:使用兩個鏈表,p1和p2,以此遍歷原鏈表,如果節點的值小于x,就掛載到p1下面,反之則放到p2下面,最后將p2掛載到p1下面就成了。

public ListNode partition(ListNode head, int x) {
    if (head == null || head.next == null) return head;
    ListNode smallerHead = new ListNode(0);
    ListNode greaterHead = new ListNode(0);
    ListNode smaller = smallerHead;
    ListNode greater = greaterHead;
    while (head != null) {
        ListNode temp = new ListNode(head.val);
        if (head.val < x) {
            smaller.next = temp;
            smaller = smaller.next;
        } else {
            greater.next = temp;
            greater = greater.next;
        }
        head = head.next;
    }
    smaller.next = greaterHead.next;
    return smallerHead.next;
}

參考講解:

[328] Odd Even Linked List 奇偶鏈表

https://leetcode.com/problems/odd-even-linked-list

問題:給我們一個鏈表,分開奇偶節點(奇偶指的是節點位置而不是節點上的值),所有奇節點在前,偶節點在后。例如,給出鏈表1->2->3->4->5->NULL,應返回鏈表1->3->5->2->4->NULL。

思路:新建兩個鏈表,分別存儲奇數位置和偶數位置的節點,最后將兩個鏈表接上,得到的結果即為所求。
這種不是刪除結點而是移動重新插入結點的問題要注意:prev/cur等游標指針的移動,是不是后面結點被移走了就不用移動了,是不是后面結點移走了就null了等等。

image
public ListNode oddEvenList(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode odd = head;
    ListNode even = head.next;
    ListNode evenHead = even;
    // 因為odd肯定在even之前,所以只需要判斷even和even.next不為空就可以
    while (even != null && even.next != null) {
        odd.next = even.next;
        odd = odd.next;
        even.next = odd.next;
        even = even.next;
    }
    // 偶鏈表連在奇鏈表后面
    odd.next = evenHead;
    return head;
}

參考講解:

[725] Split Linked List in Parts 拆分鏈表成k部分

https://leetcode.com/problems/split-linked-list-in-parts

問題:給我們一個鏈表和一個正數k,讓我們分割鏈表成k部分,盡可能的平均分割,如果結點不夠了,就用空結點。平均分后多出來的節點優先分配給前面的部分。

思路:常規的鏈表操作題。

public ListNode[] splitListToParts(ListNode root, int k) {
    ListNode[] res = new ListNode[k];

    int len = 0;
    for (ListNode cur = root; cur != null; cur = cur.next) len++;

    int part = len / k;
    int surplus = len % k;
    ListNode head = root;
    ListNode prev = null;
    for (int i = 0; i < k; i++, surplus--) {
        res[i] = head;
        // 多的部分有k+1個,少的部分有k個
        for (int j = 0; j < part + (surplus > 0 ? 1 : 0); j++) {
            prev = head;
            head = head.next;
        }
        // 與后面部分斷開
        if (prev != null) prev.next = null;
    }
    return res;
}

參考講解:

[234] Palindrome Linked List 判斷鏈表是否是回文串

https://leetcode.com/problems/palindrome-linked-list

問題:判斷一個鏈表是否為回文鏈表。

思路:簡單解法很容易,遍歷鏈表時用一個ArrayList存所有值,然后檢查ArrayList就行了。注意:ArrayList里的是Integer,判斷相等要用equals而不能用==!O(1)空間做法與第143題非常像!就是用快慢指針找到鏈表中點后,反轉后半鏈表,然后從前后兩個方向模仿數組那樣檢查Palindrome。注意:終止條件是right!=mid或left、right都不為空,如果只檢查left!=right的話會空指針或死循環!同時關于reverse,不能用“頭插法”,因為我們要在reverse完成后從后往前做比較,“頭插法”是不合適的!
鏈表比字符串難的地方就在于不能通過坐標來直接訪問,而只能從頭開始遍歷到某個位置。那么根據回文串的特點,我們需要比較對應位置的值是否相等,那么我們首先需要找到鏈表的中點,這個可以用快慢指針來實現。我們使用快慢指針找中點的原理是fast和slow兩個指針,每次快指針走兩步,慢指針走一步,等快指針走完時,慢指針的位置就是中點。
我們還需要用棧,每次慢指針走一步,都把值存入棧中,等到達中點時,鏈表的前半段都存入棧中了,由于棧的后進先出的性質,就可以和后半段鏈表按照回文對應的順序比較了。
這道題的Follow Up讓我們用O(1)的空間,那就是說我們不能使用Stack了,那么我們可以在找到中點后,將后半段的鏈表原地反轉一下,這樣我們就可以按照回文的順序比較了。

public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) return true;
    // 找中點
    ListNode mid = findMiddle(head);
    // 對中點后的節點進行反轉
    mid.next = reverse(mid.next);
    // 分別對兩部分[head,mid)和[mid.next,null)的值一一進行比較
    ListNode p = head;
    ListNode q = mid.next;
    while (p != null && q != null) {
        if (p.val != q.val) return false;
        p = p.next;
        q = q.next;
    }
    return true;
}

private ListNode findMiddle(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 prev = null;
    while (head != null) {
        ListNode next = head.next;
        head.next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

參考講解:

進位加法

[2] Add Two Numbers 兩個鏈表相加

https://leetcode.com/problems/add-two-numbers

問題:給定兩個鏈表分別代表兩個非負整數。數位以倒序存儲,并且每一個節點包含一位數字。將兩個數字相加并以鏈表形式返回。

思路:

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode cur = dummy;
    int sum = 0;
    while (l1 != null || l2 != null) {
        if (l1 != null) {
            sum += l1.val;
            l1 = l1.next;
        }
        if (l2 != null) {
            sum += l2.val;
            l2 = l2.next;
        }
        cur.next = new ListNode(sum % 10);
        sum /= 10;
        cur = cur.next;
    }
    if (sum == 1) cur.next = new ListNode(1);
    return dummy.next;
}

參考講解:

[445] Add Two Numbers II 兩個鏈表倒序相加

https://leetcode.com/problems/add-two-numbers-ii

問題:

思路:

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode cur = dummy;

    Stack<Integer> stack = new Stack<>();
    Stack<Integer> s1 = new Stack<>();
    Stack<Integer> s2 = new Stack<>();
    while (l1 != null) {
        s1.push(l1.val);
        l1 = l1.next;
    }
    while (l2 != null) {
        s2.push(l2.val);
        l2 = l2.next;
    }

    int cn = 0;
    while (!s1.isEmpty() || !s2.isEmpty()) {
        int val = cn;
        if (!s1.isEmpty()) {
            val += s1.pop();
        }
        if (!s2.isEmpty()) {
            val += s2.pop();
        }
        // 產生進位cn
        cn = val / 10;
        val = val % 10;
        stack.push(val);
    }

    // 當l1、l2都到達鏈表尾且有進位時
    if (cn != 0) stack.push(cn);

    while (!stack.isEmpty()) {
        cur.next = new ListNode(stack.pop());
        cur = cur.next;
    }

    return dummy.next;
}

參考講解:

鏈表合并

[21] Merge Two Sorted Lists 合并兩個有序鏈表

https://leetcode.com/problems/merge-two-sorted-lists
https://www.nowcoder.com/questionTerminal/d8b6b4358f774294a89de2a6ac4d9337

問題:輸入兩個單調遞增的鏈表,輸出兩個鏈表合成后的鏈表,當然我們需要合成后的鏈表滿足單調不減規則。

思路:

public ListNode Merge(ListNode list1, ListNode list2) {
    if (list1 == null) return list2;
    else if (list2 == null) return list1;

    // 新建一個頭節點,用來存合并的鏈表
    ListNode dummy = new ListNode(0);
    dummy.next = null;
    ListNode head = dummy;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            head.next = list1;
            head = list1;
            list1 = list1.next;
        } else {
            head.next = list2;
            head = list2;
            list2 = list2.next;
        }
    }

    // 把未結束的鏈表連接到合并后的鏈表尾部
    if (list1 != null) head.next = list1;
    if (list2 != null) head.next = list2;
    return dummy.next;
}

參考講解:

[23] Merge k Sorted Lists 合并k個有序鏈表

https://leetcode.com/problems/merge-k-sorted-lists

問題:

思路:

public ListNode mergeKLists(ListNode[] lists) {
    ListNode dummy = new ListNode(0);
    if (lists == null || lists.length == 0) return dummy.next;

    int len = lists.length;
    ListNode cur = dummy;
    PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
        @Override
        public int compare(ListNode o1, ListNode o2) {
            return o1.val - o2.val;
        }
    });
    // 把所有List的首節點(如果不為空)都放入優先隊列中
    for (int i = 0; i < len; i++) {
        if (lists[i] != null) {
            queue.add(lists[i]);
        }
    }
    while (queue.size() != 0) {
        // 從優先隊列中取出一個最小的
        ListNode node = queue.poll();
        cur.next = node;
        cur = cur.next;
        if (node.next != null) queue.add(node.next);
    }
    return dummy.next;
}

參考講解:

[160] Intersection of Two Linked Lists 兩個相交鏈表的交點

https://leetcode.com/problems/intersection-of-two-linked-lists

問題:找出兩個單鏈表相交的開始處。

思路:兩個單鏈表相交只可能為“>-”型,因為單鏈表只有一個next指針。如果兩個單鏈表有共同的節點,那么從第一個共同節點開始,后面的節點都會重疊,直到鏈表結束。
因此我們分別從head1,head2開始遍歷兩個鏈表獲得其長度len1與len2。假設len1>=len2,那么指針p1由head1開始向后移動len1-len2步。指針p2=head2,下面p1、p2每次向后前進一步并比較p1和p2是否相等,如果相等即返回該結點,否則說明兩個鏈表沒有交點。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode nodeA = headA, nodeB = headB;
    int lenA = 0, lenB = 0;
    // 計算鏈表A的長度
    while (nodeA != null) {
        nodeA = nodeA.next;
        lenA++;
    }
    // 計算鏈表B的長度
    while (nodeB != null) {
        nodeB = nodeB.next;
        lenB++;
    }
    // 讓較長的鏈表先飛一會
    for (int i = 0; i < Math.abs(lenA - lenB); i++) {
        if (lenA < lenB) headB = headB.next;
        else if (lenA > lenB) headA = headA.next;
    }
    while (headA != null && headB != null) {
        if (headA == headB) return headA;
        headA = headA.next;
        headB = headB.next;
    }
    return null;
}

參考講解:

其他

[138] Copy List with Random Pointer 復制復雜鏈表(包含一個隨機指針)

https://leetcode.com/problems/copy-list-with-random-pointer

問題:

思路:復雜鏈表,其結點除了有一個m_pNext指針指向下一個結點外,還有一個m_pSibling指向鏈表中的任一結點或者NULL。第一步根據原始鏈表的每個結點N,創建對應的N’,把N’鏈接在N的后面;第二步是設置復制出來的鏈表上的結點的m_pSibling;第三步是把這個長鏈表拆分成兩個:把奇數位置的結點鏈接起來就是原始鏈表,把偶數位置的結點鏈接出來就是復制出來的鏈表。

public RandomListNode copyRandomList(RandomListNode head) {
    RandomListNode iter = head, next;

    // First round: make copy of each node,
    // and link them together side-by-side in a single list.
    while (iter != null) {
        next = iter.next;

        RandomListNode copy = new RandomListNode(iter.label);
        iter.next = copy;
        copy.next = next;

        iter = next;
    }

    // Second round: assign random pointers for the copy nodes.
    iter = head;
    while (iter != null) {
        if (iter.random != null) {
            iter.next.random = iter.random.next;
        }
        iter = iter.next.next;
    }

    // Third round: restore the original list, and extract the copy list.
    iter = head;
    RandomListNode pseudoHead = new RandomListNode(0);
    RandomListNode copy, copyIter = pseudoHead;

    while (iter != null) {
        next = iter.next.next;

        // extract the copy
        copy = iter.next;
        copyIter.next = copy;
        copyIter = copy;

        // restore the original list
        iter.next = next;

        iter = next;
    }

    return pseudoHead.next;
}

參考講解:

[109] Convert Sorted List to Binary Search Tree 有序鏈表轉BST

https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree
https://www.nowcoder.com/questionTerminal/947f6eb80d944a84850b0538bf0ec3a5

問題:將一個排好序的鏈表轉成一個平衡二叉樹。

思路:對于一個二叉樹來說,左子樹一定小于根節點,而右子樹大于根節點。所以我們需要找到鏈表的中間節點,這個就是根節點,鏈表的左半部分就是左子樹,而右半部分則是右子樹,我們繼續遞歸處理相應的左右部分,就能夠構造出對應的二叉樹了。
這題的難點在于如何找到鏈表的中間節點,我們可以通過fast,slow指針來解決,fast每次走兩步,slow每次走一步,fast走到結尾,那么slow就是中間節點了。
取中點作為當前函數的根。這里的問題是對于一個鏈表我們是不能常量時間訪問它的中間元素的,這時候就要利用到樹的中序遍歷了,按照遞歸中序遍歷的順序對鏈表結點一個個進行訪問,而我們要構造的二分查找樹正是按照鏈表的順序來的。思路就是先對左子樹進行遞歸,然后將當前結點作為根,迭代到下一個鏈表結點,最后再遞歸求出右子樹即可。整體過程就是一次中序遍歷,時間復雜度是O(n),空間復雜度是棧空間O(logn)。

public TreeNode sortedListToBST(ListNode head) {
    if (head == null) return null;
    if (head.next == null) return new TreeNode(head.val);

    return build(head, null);
}

private TreeNode build(ListNode start, ListNode end) {
    if (start == end) return null;

    ListNode fast = start;
    ListNode slow = start;
    // fast走到結尾,那么slow就是中間節點了,即BST的根節點
    while (fast != end && fast.next != end) {
        slow = slow.next;
        fast = fast.next.next;
    }

    // 遞歸處理相應的左右部分,鏈表的左半部分就是左子樹,而右半部分則是右子樹
    TreeNode node = new TreeNode(slow.val);
    node.left = build(start, slow);
    node.right = build(slow.next, end);

    return node;
}

參考講解:

劍指Offer

[面試題13] 在O(1)時間內刪除鏈表節點

問題:只給定單鏈表中某個結點p(并非最后一個結點),刪除該結點。

思路:首先是放p中數據,然后將p.next的數據復制到p中,接下來刪除p.next即可。

public void deleteNode(ListNode head, ListNode toBeDeleted) {
    if (head == null || toBeDeleted == null) return;

    // 鏈表有多個節點,要刪除的結點不是尾結點: O(1) 時間
    if (toBeDeleted.next != null) {
        ListNode next = toBeDeleted.next;
        toBeDeleted.val = next.val;
        toBeDeleted.next = next.next;
        next = null;
    } else if (head == toBeDeleted) {
        // 鏈表只有一個結點,刪除頭結點(也是尾結點):O(1) 時間
        toBeDeleted = null;
        head = null;
    } else {
        // 鏈表有多個節點,要刪除的是尾節點: O(n) 時間
        ListNode temp = head;
        while (temp.next != toBeDeleted) {
            temp = temp.next;
        }
        temp.next = null;
    }
}

[面試題15] 鏈表中倒數第k個結點

https://www.nowcoder.com/questionTerminal/529d3ae5a407492994ad2a246518148a

問題:輸入一個鏈表,輸出該鏈表中倒數第k個結點。

思路:使用兩個節點p1和p2,快指針先從表頭走k-1步,從第k步開始兩個快慢指針一起走,直到快指針走到尾節點退出while循環。由于兩個指針的距離保持在k-1,當快指針到達鏈表的尾結點時,慢指針正好是倒數第k個結點。注意:k大于鏈表長度的情況要返回null。

public ListNode findKthToTail(ListNode head, int k) {
    if (head == null || k < 1) return null;
    ListNode fast = head;
    ListNode slow = head;
    for (int i = 0; i < k - 1; i++) {
        // 鏈表節點數可能小于k
        if (fast.next != null)
            // 快指針先走k-1步
            fast = fast.next;
        else
            return null;
    }
    // 快指針走到尾節點就退出循環
    while (fast.next != null) {
        // 從第k步開始,兩個指針一起走
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

[面試題5] 從尾到頭打印鏈表

https://www.nowcoder.com/questionTerminal/d0267f7f55b3412ba93bd35cfa8e8035

問題:輸入一個鏈表,從尾到頭打印鏈表每個節點的值。

思路:

/**
 * 用棧保存遍歷結果
 */
public List<Integer> printListFromTailToHead(ListNode head) {
    List<Integer> results = new ArrayList<>();
    if (head == null) return results;
    Stack<Integer> stack = new Stack<>();
    ListNode node = head;
    // 只要鏈表未到達表尾
    while (node != null) {
        // 就依次遍歷鏈表并添加到Stack中
        stack.push(node.val);
        node = node.next;
    }
    // 只要棧不空
    while (!stack.isEmpty()) {
        // 就不斷地將元素添加到List中,并出棧
        results.add(stack.pop());
    }
    return results;
}

/**
 * 頭插法
 */
public List<Integer> printListFromTailToHeadToucha(ListNode head) {
    ArrayList<Integer> results = new ArrayList<>();
    if (head == null) return results;
    ListNode node = head;
    while (node != null) {
        // 頭插法
        results.add(0, node.val);
        node = node.next;
    }
    return results;
}

/**
 * 遞歸
 */
public List<Integer> printListFromTailToHeadRec(ListNode head) {
    List<Integer> results = new ArrayList<>();
    if (head == null) return results;
    dfs(head, results);
    return results;
}

private void dfs(ListNode head, List<Integer> results) {
    if (head != null) {
        if (head.next != null) {
            // 因為要反過來輸出鏈表,所以先遞歸輸出后面的節點
            dfs(head.next, results);
        }
        // 再輸出自身
        results.add(head.val);
    }
}

解題技巧

  • slow,fst雙指針,因為鏈表無法得知長度,所以嘗試用這種方法來達到某種效果(長度、檢測環等)
  • 對于涉及鏈表長度的問題,往往會通過兩個指針進行幾何變換來得到想要的差額==要好好畫圖理解思考
  • 使用一些臨時變量來存儲next指針,以完成插入刪除等操作
  • 對于插入和刪除等操作,往往需要一個額外的指針來記錄其前面的節點,再編程之前好好思考其間關系效果會比較好
  • 對一些依賴于后面節點才可以完成的操作,使用遞歸的方式來解決
  • 對于有些題目提前使用循環獲得其鏈表的長度也是一種有效的方法
  • 對于要考慮最后幾個節點的操作,有事可以再遍歷之前先將頭指針向后移動k個節點
  • 插入、刪除操作往往需要使用目標節點前面的節點,所以往往會定義一個新的鏈表節點其next指針指向head節點

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容