【LeetCode題解】鏈表Linked List


1. 鏈表

數組是一種順序表,index與value之間是一種順序映射,以\(O(1)\)的復雜度訪問數據元素。但是,若要在表的中間部分插入(或刪除)某一個元素時,需要將后續的數據元素進行移動,復雜度大概為\(O(n)\)。鏈表(Linked List)是一種鏈式表,克服了上述的缺點,插入和刪除操作均不會引起元素的移動;數據結構定義如下:

public class ListNode {
  String val;
  ListNode next;
  // ...
}

常見的鏈表有單向鏈表(也稱之為chain),只有next指針指向后繼結點,而沒有previous指針指向前驅結點。

鏈表的插入與刪除操作只涉及到next指針的更新,而不會移動數據元素。比如,要在FAT與HAT插入結點GAT,如圖所示:

Java實現:

ListNode fat, gat;
gat.next = fat.next;
fat.next = gat;

又比如,要刪除結點GAT,如圖所示:

Java實現:

fat.next = fat.next.next;

從上述代碼中,可以看出:因為沒有前驅指針,一般在做插入和刪除操作時,我們需要通過操作前驅結點的next指針開始。

2. 題解

LeetCode題目 歸類
237. Delete Node in a 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
24. Swap Nodes in Pairs 移動
206. Reverse Linked List
92. Reverse Linked List II
61. Rotate List
86. Partition List
328. Odd Even Linked List
21. Merge Two Sorted Lists 合並
23. Merge k Sorted Lists
141. Linked List Cycle 有環
142. Linked List Cycle II 有環
234. Palindrome Linked List
143. Reorder List
160. Intersection of Two Linked Lists
2. Add Two Numbers
445. Add Two Numbers II

237. Delete Node in a Linked List
刪除指定結點。由於是單向鏈表,因此只需更新待刪除節點即可。

public void deleteNode(ListNode node) {
  node.val = node.next.val;
  node.next = node.next.next;
}

203. Remove Linked List Elements
刪除指定值的結點。用兩個指針實現,curr用於遍歷,prev用於暫存前驅結點。

public ListNode removeElements(ListNode head, int val) {
  ListNode fakeHead = new ListNode(Integer.MIN_VALUE);
  fakeHead.next = head;
  for (ListNode curr = head, prev = fakeHead; curr != null; curr = curr.next) {
    if (curr.val == val) { // remove
      prev.next = curr.next;
    } else { // traverse
      prev = prev.next;
    }
  }
  return fakeHead.next;
}

19. Remove Nth Node From End of List
刪除鏈表的倒數第n個結點。思路:因為單向鏈表是沒有前驅指針的,所以應找到倒數第n+1個結點;n有可能等於鏈表的長度,故先new一個head的前驅結點fakeHead。用兩個指針slow、fast從fakeHead開始,先移動fast n+1步,使得其距離slow為n+1;然后,兩個指針同步移動,當fast走到null時,slow即處於倒數第n+1個結點,刪除slow的next結點即可。

public ListNode removeNthFromEnd(ListNode head, int n) {
  ListNode fakeHead = new ListNode(Integer.MIN_VALUE);
  fakeHead.next = head;
  ListNode slow = fakeHead, fast = fakeHead;
  for (int i = 1; i <= n + 1; i++) {
    fast = fast.next;
  }
  while(fast != null) {
    fast = fast.next;
    slow = slow.next;
  }
  slow.next = slow.next.next; // the n-th node from end is `slow.next`
  return fakeHead.next;
}

83. Remove Duplicates from Sorted List
刪除有序鏈表中的重復元素。處理思路有上一問題類似,不同的是判斷刪除的條件。

public ListNode deleteDuplicates(ListNode head) {
  ListNode fakeHead = new ListNode(Integer.MIN_VALUE);
  fakeHead.next = head;
  for (ListNode curr = head, prev = fakeHead; curr != null && curr.next != null; curr = curr.next) {
    if (curr.val == curr.next.val) { // remove
      prev.next = curr.next;
    } else {
      prev = prev.next;
    }
  }
  return fakeHead.next;
}

82. Remove Duplicates from Sorted List II
上一問題的升級版,刪除所有重復元素結點。在里層增加一個while循環,跳過重復元素結點。

public ListNode deleteDuplicates(ListNode head) {
  ListNode fakeHead = new ListNode(Integer.MIN_VALUE);
  fakeHead.next = head;
  for (ListNode curr = head, prev = fakeHead; curr != null; curr = curr.next) {
    while (curr.next != null && curr.val == curr.next.val) { // find the last duplicate
      curr = curr.next;
    }
    if (prev.next == curr) prev = prev.next;
    else prev.next = curr.next;
  }
  return fakeHead.next;
}

24. Swap Nodes in Pairs
鏈表中兩兩交換。按step = 2 遍歷鏈表並交換;值得注意的是在更新next指針是有次序的。

public ListNode swapPairs(ListNode head) {
  ListNode fakeHead = new ListNode(Integer.MIN_VALUE);
  fakeHead.next = head;
  for (ListNode prev = fakeHead, p = head; p != null && p.next != null; ) {
    ListNode temp = p.next.next;
    prev.next = p.next; // update next pointer
    p.next.next = p;
    p.next = temp;
    prev = p;
    p = temp;
  }
  return fakeHead.next;
}

206. Reverse Linked List
逆序整個鏈表。逆序操作可以看作:依次遍歷鏈表,將當前結點插入到鏈表頭。

public ListNode reverseList(ListNode head) {
  ListNode newHead = null;
  for (ListNode curr = head; curr != null; ) {
    ListNode temp = curr.next;
    curr.next = newHead; // insert to the head of list
    newHead = curr;
    curr = temp;
  }
  return newHead;
}

92. Reverse Linked List II
上一問題的升級,指定區間[m, n]內做逆序;相當於把該區間的鏈表逆序后,再拼接到原鏈表中。

public ListNode reverseBetween(ListNode head, int m, int n) {
  ListNode newHead = null, curr = head, firstHead = null, firstHeadPrev = null;
  for (int i = 1; curr != null && i <= n; i++) {
    if (i < m - 1) {
      curr = curr.next;
      continue;
    }
    if (i == m - 1) {
      firstHeadPrev = curr; // mark first head previous node
      curr = curr.next;
    } else {
      if (i == m) firstHead = curr; // mark first head node
      ListNode temp = curr.next;
      curr.next = newHead;
      newHead = curr;
      curr = temp;
    }
  }
  firstHead.next = curr;
  if (firstHeadPrev != null) firstHeadPrev.next = newHead;
  if (m == 1) return newHead;
  return head;
}

61. Rotate List
指定分隔位置,將鏈表的左右部分互換。只需修改左右部分的最后節點的next指針即可,有一些special case需要注意,諸如:鏈表為空,k為鏈表長度的倍數等。為了得到鏈表的長度,需要做一次pass。故總共需要遍歷鏈表兩次。

public ListNode rotateRight(ListNode head, int k) {
  if (head == null || k == 0) return head;
  int n = 0, i;
  ListNode curr, leftLast = head, rightFist, rightLast = head;
  for (curr = head; curr != null; curr = curr.next) { // get the length of list
    n++;
  }
  k %= n; // k maybe larger than n
  if (k == 0) return head;
  for (i = 1, curr = head; i <= n; i++, curr = curr.next) { // mark the split node
    if (i == n - k) leftLast = curr;
    if (i == n) rightLast = curr;
  }
  rightFist = leftLast.next;
  leftLast.next = null;
  rightLast.next = head;
  return rightFist;
}

86. Partition List
類似於quick sort的partition,不同的是要保持鏈表的原順序。思路:用兩個鏈表,一個保留小於指定數x,一個保留不大於指定數x;最后拼接到一起即可。

public ListNode partition(ListNode head, int x) {
  if (head == null) return null;
  ListNode lt = new ListNode(-1), gte = new ListNode(-2); // less than, greater than and equal
  ListNode p, p1, p2;
  for (p = head, p1 = lt, p2 = gte; p != null; p = p.next) {
    if (p.val < x) {
      p1.next = p;
      p1 = p1.next;
    } else {
      p2.next = p;
      p2 = p2.next;
    }
  }
  p2.next = null;
  p1.next = gte.next;
  return lt.next;
}

328. Odd Even Linked List
鏈表分成兩部分:偶數編號與奇數編號,將偶數鏈表拼接到奇數鏈表的后面。

public ListNode oddEvenList(ListNode head) {
  if (head == null || head.next == null) return head;
  ListNode odd = head, even = head.next, evenHead = head.next;
  while (odd.next != null && odd.next.next != null) {
    odd.next = odd.next.next;
    odd = odd.next;
    if (even != null && even.next != null) {
      even.next = even.next.next;
      even = even.next;
    }
  }
  odd.next = evenHead; // splice even next to odd
  return head;
}

21. Merge Two Sorted Lists
合並兩個有序鏈表。比較簡單,分情況比較。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
  ListNode head = new ListNode(-1), p, p1, p2;
  for (p = head, p1 = l1, p2 = l2; p1 != null || p2 != null; p = p.next) {
    if (p1 != null) {
      if (p2 != null && p1.val > p2.val) {
        p.next = p2;
        p2 = p2.next;
      } else {
        p.next = p1;
        p1 = p1.next;
      }
    } else {
      p.next = p2;
      p2 = p2.next;
    }
  }
  return head.next;
}

23. Merge k Sorted Lists
合並k個有序鏈表。思路:借助於堆,堆的大小為k,先將每個鏈表的首結點入堆,堆頂元素即為最小值,堆頂出堆后將next入堆;依此往復,即可得到整個有序鏈表。

public ListNode mergeKLists(ListNode[] lists) {
  if (lists.length == 0) return null;
  PriorityQueue<ListNode> minHeap = new PriorityQueue<>(lists.length, new Comparator<ListNode>() {
    @Override
    public int compare(ListNode o1, ListNode o2) {
      return o1.val - o2.val;
    }
  });
  // initialization
  for (ListNode node : lists) {
    if (node != null)
      minHeap.offer(node);
  }
  ListNode head = new ListNode(-1);
  for (ListNode p = head; !minHeap.isEmpty(); ) {
    ListNode top = minHeap.poll();
    p.next = top;
    p = p.next;
    if (top.next != null)
      minHeap.offer(top.next);
  }
  return head.next;
}

141. Linked List Cycle
判斷鏈表是否有環。用兩個指針,一個快指針一個慢指針,一個每次移動兩步,一個每次移動一步;最后兩者相遇,即說明有環。

public boolean hasCycle(ListNode head) {
  if (head == null) return false;
  for (ListNode slow = head, fast = head; fast.next != null && fast.next.next != null; ) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) return true;
  }
  return false;
}

142. Linked List Cycle II
找出鏈表中環的起始節點\(s\)。解決思路:用兩個指針——fast、slow,先判斷是否環;兩者第一次相遇的節點與\(s\)的距離 == 鏈表起始節點與\(s\)的距離(有興趣可以證明一下)。

public ListNode detectCycle(ListNode head) {
  if (head == null || head.next == null) return null;
  ListNode slow = head, fast = head;
  boolean isCycled = false;
  while (slow != null && fast != null && fast.next != null) { // first meeting
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) {
      isCycled = true;
      break;
    }
  }
  if (!isCycled) return null;
  for (fast = head; slow != fast; ) { // find the cycle start node
    slow = slow.next;
    fast = fast.next;
  }
  return slow;
}

234. Palindrome Linked List
判斷鏈表\(L\)是否中心對稱。中心對稱的充分必要條件:對於任意的 i <= n/2 其中n為鏈表長度,有L[i] = L[n+1-i]成立。因此先找出middle結點(在距離首結點n/2處),然后逆序右半部分鏈表,與左半部分鏈表的結點一一比較,即可得到結果。在找出middle結點時也用到了小技巧——快慢兩個指針遍歷鏈表,當fast遍歷完成時,slow即為middle結點(證明分n為奇偶情況);當n為偶數時,middle結點有兩個,此時slow為左middle結點。換句話說,無論n為奇數或偶數,此時的slow為右半部分子鏈表的第一個結點的前驅結點。

public boolean isPalindrome(ListNode head) {
  if (head == null) return true;
  ListNode slow = head, fast = head, p;
  while (fast.next != null && fast.next.next != null) {
    slow = slow.next;
    fast = fast.next.next;
  }
  ListNode q = reverseList(slow.next); // the first node of the right half is `slow.next`
  for (p = head; q != null; p = p.next, q = q.next) {
    if (p.val != q.val) return false;
  }
  return true;
}

143. Reorder List
對於除去首結點外的鏈表,將右半部分子鏈表從后往前依次插入進左半部分鏈表。解決思路與上類似,找出middle結點,然后依次插入。值得注意:Java的對象傳參是引用類型,需要更新左半部份子鏈表的最后一個結點的next指針,不然則鏈表的結點的無限循環導致OOM。

public void reorderList(ListNode head) {
  if (head == null || head.next == null) return;
  ListNode slow = head.next, fast = head.next;
  while (fast.next != null && fast.next.next != null) {
    slow = slow.next;
    fast = fast.next.next;
  }
  ListNode p, q = reverseList(slow.next);
  slow.next = null; // update the next pointer of the left half's last node
  for (p = head; q != null; ) {
    ListNode pNext = p.next, qNext = q.next;
    p.next = q; // insert qNode into the next of p node
    q.next = pNext;
    p = pNext;
    q = qNext;
  }
}

160. Intersection of Two Linked Lists
求兩個鏈表相交的第一個結點\(P\)。假定兩個鏈表的長度分別為m、n,相交的第一個結點\(P\)分別距離兩個鏈表的首結點為a、b,則根據鏈表相交的特性:兩個鏈表的尾節點都是同一個,即m-a = n-b;移項后有m+b = n+a。根據上述性質,在遍歷完第一個鏈表后,再往右b個結點,即到達了結點\(P\)

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  if (headA == null || headB == null) return null;
  ListNode ptrA = headA, ptrB = headB;
  while (ptrA != ptrB) { // in case ptrA == ptrB == null
    ptrA = (ptrA != null) ? ptrA.next : headB;
    ptrB = (ptrB != null) ? ptrB.next : headA;
  }
  return ptrA;
}

2. Add Two Numbers
模擬兩個鏈表的加法。開始的時候沒理解清楚題意,被坑了多次WA。鏈表的head表示整數的個位,則應從首端對齊開始做加法。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
  ListNode head = new ListNode(-1);
  boolean carry = false; // mark whether has carry
  for (ListNode p = l1, q = l2, r = head; p != null || q != null || carry; r = r.next) {
    int pVal = (p == null) ? 0 : p.val;
    int qVal = (q == null) ? 0 : q.val;
    int sum = carry ? pVal + qVal + 1 : pVal + qVal;
    carry = sum >= 10;
    r.next = new ListNode(sum % 10);
    if (p != null) p = p.next;
    if (q != null) q = q.next;
  }
  return head.next;
}

445. Add Two Numbers II
與上一題不同的是,鏈表的head表示整數的最高位,則應是尾端對齊相加。為了尾端對齊,將采用stack來逆序鏈表,之后相加步驟與上類似;但創建新鏈表應使用頭插法。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
  ListNode p;
  Stack<Integer> s1 = new Stack<>();
  Stack<Integer> s2 = new Stack<>();
  for (p = l1; p != null; p = p.next) {
    s1.push(p.val);
  }
  for (p = l2; p != null; p = p.next) {
    s2.push(p.val);
  }
  ListNode head = new ListNode(-1);
  boolean carry = false; // mark whether has carry
  for (ListNode r = null; !s1.isEmpty() || !s2.isEmpty() || carry; ) {
    int pVal = (s1.isEmpty()) ? 0 : s1.pop();
    int qVal = (s2.isEmpty()) ? 0 : s2.pop();
    int sum = carry ? pVal + qVal + 1 : pVal + qVal;
    carry = sum >= 10;
    ListNode node = new ListNode(sum % 10);
    node.next = r;
    head.next = node;
    r = node;
  }
  return head.next;
}


免責聲明!

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



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