Skip to content

82. Remove Duplicates from Sorted List II #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions 82. Remove Duplicates from Sorted List II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@

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

# Step 1

- 実装時間: 15+3分
- 時間計算量: O(n)
- 空間計算量: O(1)
- 問題を見て、「ループで実装する方法」と「再帰で実装する方法」の二種類を思いついた。ループで解いてみたが15分で解けなかったので、再帰で実装。

```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None
if head.next is None:
return head
if head.val != head.next.val:
head.next = self.deleteDuplicates(head.next)
return head
target_val = head.val
node = head
while node is not None and node.val == target_val:
node = node.next
return self.deleteDuplicates(node)
```

↓が時間切れで解き切れなかったループの残骸。入力`[1,1]`でエラーになる。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

エラーになった原因は理解していますか?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントありがとうございます。

エラーの原因がわかったかどうかですが、「わかった気がしていたが、odaさんのコメントを拝見して自信がなくなった」という感じです。。。

レビュー依頼時点と、odaさんのコメントを見た後での思考を言語化してみます。

  • レビュー依頼時点では、
    • 「あぁ漏れてるケースがあったね。よくよく考えて実装すれば、処理を追加してOKになった。次からは、Step3みたいな書き方をすればわかりやすく・間違いなくかけるね」ぐらいに捉えてました。
  • odaさんのコメントを見た時点では、
    • おっしゃる通り、preの意味を「注目してるものの前」ぐらいの意味で使っていおり、pre.nextの二つを完全に混同してました。
      • どちらかと言うと後者の意味で使ってましたが、頭の中では二つの違いを意識できてなかったです。
    • 今振り返ると、「Linked Listの要素ひとつづつ処理をしていく意識」みたいなものが薄かった気がしてます。
      • 今回の例で言うと、「preまでは確定しているんだ」とか「今からこれを処理するんだ」みたいな意識の話です。
      • だからこそ、僕はStep3のような「ひとつづつ処理してることがわかりやすい書き方」に寄っていったのかもと思いました。
        • 言い換えると、自分が「曖昧に扱っていた変数」があるコードを(無意識で)避けたのかなと思います。

改めて、現状の理解(エラーの原因がわかったかどうか)を言葉にすると、「この問題については、言語化して指摘をいただいたのでわかった。が、自分で言語化できなかった以上、本当の意味でわかってないのかもしれない」という状況です。。


```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None
node = head
dummy = ListNode(0, head)
pre = dummy
while node is not None:
if node.next is not None and node.val == node.next.val:
target_val = node.val
while node is not None and node.val == target_val:
node = node.next
continue
pre.next = node
pre = node
node = node.next
return dummy.next
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre.next について、ループを回している間、どのようなものであると思っているのかが、一定していないように見受けられます。

  • duplicate でないと確定したものだけ、pre.next につなぐのか
  • ひとまず、次に処理をするつもりであるものが pre.next にいるのか

dummy = ListNode(0, head) 後者
continue (の前の行にpre.next = nodeがない)前者
pre.next = node 後者
return dummy.next (の前の行にpre.next = nodeがない)前者

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。まさに、その二つは一定にできておらず、意識もできてませんでした。

nodchipさんのコメントの方に、内省など書きました。
https://github.com/katataku/leetcode/pull/3/files#r1836676340

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これはどちら側に合わせても書けるはずなので、書いてみて比較して比べてみるといいと思います。

よく、私は、ループを仕事の引き継ぎで例えているんです。
前の人は次の人に対して、何を約束して、仕事を引き継いでいるのかが、そのときの気分で変わっていると、引き継ぎを受けた人は困るということですね。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discord 内で「引き継ぎ」の話を結構しているので、よければ調べておいてください。

```

# Step 2

- 再帰はオーバーヘッドがあるので避けるべきだと思っていたが、別の観点もある。
- 再帰でない記述だと入力にループがある場合無限ループになるのに対して、再帰で書くと`RecursionError`を投げてくれるのでこちらのほうがよいという考えもありますね。
https://docs.python.org/3/library/exceptions.html#RecursionError

- 命名について
- `sentinel`という命名があることを知った。
- `target_val`も意味のあまりない命名だと気づいた。`value_to_remove`を使う。
- `val`もvalueの略語なので、避ける。

- ループでの実装について。
- 「重複していて取り除く」のか「重複していないから取り除かない」のかの二種類の状態がある。
- 「重複していないから取り除かない」でearly returnするとみやすい。
- `pre`と`node`の2変数は不要。1つの変数で管理できる。

```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None:
return None
dummy = ListNode(0, head)
node = dummy
while node.next is not None:
if node.next.next is None or node.next.val != node.next.next.val:
node = node.next
continue
value_to_remove = node.next.val
while node.next is not None and node.next.val == value_to_remove:
node.next = node.next.next
return dummy.next
```

- `is_duplicated`などのフラグを使うと、whileループのネストを避けることができ、O(n)ということが伝わりやすい。
- `Optional[int]`という方法も。
- https://discord.com/channels/1084280443945353267/1226508154833993788/1246022270984392724

```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head)
node = dummy
deleting_number = None
while node.next is not None:
if deleting_number is not None and node.next.val == deleting_number:
node.next = node.next.next
continue
if node.next.next is not None and node.next.val == node.next.next.val:
deleting_number = node.next.val
continue
deleting_number = None
node = node.next

return dummy.next
```

# Step 3

- 時間計算量: O(n)
- 空間計算量: O(1)

```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(0, head)
node = sentinel
deleting_number = None
while node.next is not None:
if deleting_number is not None and node.next.val == deleting_number:
node.next = node.next.next
continue
if node.next.next is not None and node.next.val == node.next.next.val:
deleting_number = node.next.val
continue
deleting_number = None
node = node.next
return sentinel.next
```

# Step 4

preを使った書き方についてコメントをいただいたので、一晩おいた後に書いてみた。

```python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(0, head)
pre = sentinel
node = head
while node is not None:
if node.next is None or node.val != node.next.val:
pre = pre.next
node = node.next
continue
value_to_remove = node.val
while node is not None and node.val == value_to_remove:
node = node.next
Comment on lines +144 to +146

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この部分はヘルパー関数に切り出しても良いかもですね。

pre.next = node
return sentinel.next
```