[LeetCode][C++] 用乾淨的方式寫 #19 Remove Nth Node From End of List

又是一個資料結構練習? 但仔細一看他希望我們做 one pass 就可以輸出答案。

我 OK,你先走 n 步

如果我們用左右兩個指標,讓右邊的指標先走 n 步,會發生什麼事?


我們就先模擬看看,當右邊的人一踩到 nullptr,左邊的指標在哪?

假設 n=2 我們看到左邊的指標正好指著要刪除的 node,好像就做完了。

問題是,現在要怎麼刪除左邊的人? 單向的 linked list 一旦往後走就不能回頭了,我們想把 2 接到 4 現在就卡死動彈不得了。

我 OK,你先走 n + 1 步

既然我們想要把左右兩邊的指標再距離寬一點,那我們就會很直覺的想把 n + 1 再模擬看看:


但如果 n 剛好等於 linked list 長度,右邊先跑 n + 1 步的話左邊的人不就會超出範圍?

所以我們就用一個常見的技巧,創一個虛擬的 node,LeetCode 上叫做 dummy,不過我覺得叫 root 比較好,比較有起始點的感覺。

當右邊指標撞到 nullptr 左邊指標的下一個人剛好就是要刪除的對象,這時候就不會有問題。

Boundary Cases

養成好習慣我們還是要稍微想一下比較容易出問題的邊界條件:

  • 如果 n=1 會不會有問題? 不會
  • 如果 n=size 會不會有問題? left 會在 root 的位置,刪除下一個人也不會有問題

好讀的 C++ 程式碼

我們想要讓維護的人一看就知道程式碼在做什麼,所以我的習慣就是用小小的 function 加上很好懂得名稱告訴別人我 function 在做什麼,而且一次只做一件事。

這樣 removeNthFromEnd 就會看起來很清爽:


class Solution {
 public:
  ListNode *removeNthFromEnd(ListNode *head, int n) {
    ListNode root(0, head);
    ListNode *left = &root;
    ListNode *right = &root;
    right = moveNSteps(right, n + 1);
    while (right != nullptr) {
      left = moveNSteps(left, 1);
      right = moveNSteps(right, 1);
    }
    deleteNextNode(left);
    head = root.next;
    return head;
  }

  ListNode *moveNSteps(ListNode *node, int numSteps) {
    for (int i = 0; i < numSteps; i++) {
      assert(node != nullptr);
      node = node->next;
    }
    return node;
  }

  void deleteNextNode(ListNode *node) {
    ListNode *nextNode = node->next;
    assert(nextNode != nullptr);
    node->next = nextNode->next;
    delete nextNode;
  }
};

還有一些小細節是防止錯誤用的:

  • 第 4 行為什麼不用 new ListNode 的方式?
    • 不然就會像 LeetCode 上的解答最後忘了把 dummy 刪除,造成 memory leak
    • 這邊這樣寫一離開 function,root 自然就會被刪除
  • 第 13 行為什麼要先讓 head = root.next?
    • 因為我們刪除 left 有可能會刪到 head,怕以後修改的人會不小心用到已經被刪除的 head,所以我們趕緊先把他修正
    • 如果沒修正我們會把 13 行之後的 head 叫做 dangling pointer
  • 第 19 和 27 行為什麼要用 assert?
    • 有時候加一些 assert 可以幫助 debug,也避免一些 nullptr 存取造成奇怪的問題
    • 兩處都是為了要避免應該不可能發生的條件,不然 nullptr->next 不知道會發生什麼事 (undefined behavior),常常令人頭痛

Submission

這題主要是資料結構與演算法的設計,和速度/記憶體應該就沒什麼關係了:

結語

我其實一開始是用 nullptr 來當作開頭的,不過程式碼不僅會比較長,一個致命缺陷就是會分不清楚到底 nullptr 是代表開頭還是結尾,所以後來還是改成用一個 root 的方式。

不過說實在的,LeetCode 說 linked list 最長也才 30,恐怕 one pass 和 two pass 真的看不出什麼差別。討論區有人在爭論說 one pass 和 two pass 時間根本一樣,不過實際上跑起來 one pass 可能還是會快一點,詳細的說明請看 x86hacker 在討論區的回答

留言

此網誌的熱門文章

[試算表] 追蹤台股 Google Spreadsheet (未實現損益/已實現損益)

[Side Project] 互動式教學神經網路反向傳播 Interactive Computational Graph

[插件] 在 Chrome 網頁做區分大小寫的搜尋