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;
}
