2095. Delete the Middle Node of a Linked List
拜讀了你所不知道的 C 語言: linked list 和非連續記憶體一文中所提到的 “good taste” 段落 , 覺得非常有趣而躍躍欲試,所以找這題來練練手。(後來發現底下的例子也有寫到這題😅,而且效率又比我想到的解更好,甚至還有一些記憶體管理的細節,學到了!)
原始題目
這題是給定一個不固定長度 的鏈結串列,然後要求刪除最中間的那個節點( node ) (精確來說是第 個節點)
值得紀錄之處
這裡所謂的 good taste 是指利用一些技巧,去減少「特例」發生的情形,進而使程式碼更加乾脆俐落。舉例來說,原本在這題常見的解法有兩種:
- 先用迴圈把整個鏈結串列遍歷,計算有幾個節點。算完再跑一次
for迴圈把最中間的節點刪除。 - 利用
fast和slow兩指標,丟進迴圈裡面,fast每回合前進兩格,slow每回合前進一格,這樣只要一次迴圈就可以抓到要刪除的節點並且刪除。
非 indirect pointer 的解
- 此程式碼取自 leetcode 論壇的某位大大寫的詳解文章微調而得
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
if (head -> next == nullptr)
return nullptr;
ListNode *slow = head, *fast = head -> next -> next;
while (fast != nullptr && fast -> next != nullptr) {
slow = slow -> next;
fast = fast -> next -> next;
}
slow -> next = slow -> next -> next;
return head;
}
};class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
if (head -> next == nullptr)
return nullptr;
ListNode *slow = head, *fast = head -> next -> next;
while (fast != nullptr && fast -> next != nullptr) {
slow = slow -> next;
fast = fast -> next -> next;
}
slow -> next = slow -> next -> next;
return head;
}
};可以注意到,上面的解法 (line 4) 會有一個特例 (edge case), 如果只有一個 node, 那就如何如何……。有時候特例一多,可能會寫了整面的 if-else, 看了就痛苦😵💫。如果有個魔法可以使這個特例更減少,那程式碼就可以更精簡,這也是我這次的練習目標。
因為題意規定 1 <= node.size() <= 100000, 因此前者的原本的做法:
- 不用判斷是否為空 list
- 要判斷是否只有一個 node, 若是只有一個 node, 則把該 node 刪除
- 若非只有一個 node, 則按照
fast,slow做法從頭開始經過此 list 每個節點。
承上所述,我會需要某個「東西」,這個東西有辦法視情況具有 head 或 slow 的功能,而那個東西就是間接指標 (indirect pointer) ,一種指向指標的記憶體位址的指標(有點繞舌😆)。
原本
slow無法對head造成影響使用
indirect_del可以修改head刪除前
// 因為沒有 edge case, 所以跟後面刪除中間節點的程式碼可以一樣 *indirect_del = (*indirect_del)->next;// 因為沒有 edge case, 所以跟後面刪除中間節點的程式碼可以一樣 *indirect_del = (*indirect_del)->next;刪除後
使用
indirect_del也可以刪除中間的任意節點刪除前
*indirect_del = (*indirect_del)->next;*indirect_del = (*indirect_del)->next;刪除後
indirect pointer 的解
(這個魔法我在原文花了好幾個小時反覆研究了很多遍,一開始覺得超級不好懂XD)
// good taste 的解法
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
ListNode **indirect_del = &head, *fast = head;
// 利用 go_next 去判斷 indirect_del 需不需要前進一格
for(bool go_next = false; fast; fast = fast->next, go_next = !go_next)
indirect_del = go_next ? &(*indirect_del)->next : indirect_del;
*indirect_del = (*indirect_del)->next;
return head;
}
};
// 比較:多了 edge case 的解法
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
if (head -> next == nullptr)
return nullptr;
ListNode *slow = head, *fast = head -> next -> next;
while (fast != nullptr && fast -> next != nullptr) {
slow = slow -> next;
fast = fast -> next -> next;
}
slow -> next = slow -> next -> next;
return head;
}
};// good taste 的解法
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
ListNode **indirect_del = &head, *fast = head;
// 利用 go_next 去判斷 indirect_del 需不需要前進一格
for(bool go_next = false; fast; fast = fast->next, go_next = !go_next)
indirect_del = go_next ? &(*indirect_del)->next : indirect_del;
*indirect_del = (*indirect_del)->next;
return head;
}
};
// 比較:多了 edge case 的解法
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
if (head -> next == nullptr)
return nullptr;
ListNode *slow = head, *fast = head -> next -> next;
while (fast != nullptr && fast -> next != nullptr) {
slow = slow -> next;
fast = fast -> next -> next;
}
slow -> next = slow -> next -> next;
return head;
}
};因為多了這層「間接的關聯」, indirect_del 一開始是指向 head 的地址,因此可以快樂地除去前述惱人的 edge case. 之後 indirect_del 又可以指向任何一個 node 的 node->next 指標,以獲得所有指標的功能。到這邊可能要多對照幾次兩者之間有無 “indirect pointer” 的差別會比較容易理解,理解之後不禁感嘆程式這門藝術真是博大精深!