Skip to content
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

Add 82. Remove Duplicates from Sorted List II.md #4

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
241 changes: 241 additions & 0 deletions 82. Remove Duplicates from Sorted List II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# step 1
[83. Remove Duplicates from Sorted List](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/)と
やっていることはほぼ同じ。
今注目しているノードを元に、次に進むべきノードを適切に取ってくる。
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
def hasSameValueAsNextNode(node):
Copy link

Choose a reason for hiding this comment

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

LeetCode で指定されている関数名は lowerCamel なのですが、 Python では一般的には関数名は lower_snake で書くことが多いと思います。lower_camel で書くことをお勧めいたします。

https://peps.python.org/pep-0008/#function-and-variable-names

Function names should be lowercase, with words separated by underscores as necessary to improve readability.

https://google.github.io/styleguide/pyguide.html#316-naming

function_name

return node is not None and node.next is not None \
and node.val == node.next.val
def skipDuplicates(node):
if node is None:
return None

while hasSameValueAsNextNode(node):
node = node.next
if hasSameValueAsNextNode(node.next):
return skipDuplicates(node.next)
return node.next

dummy_head = ListNode(-1, head)
node = dummy_head
while node is not None:
next_node = node.next
if hasSameValueAsNextNode(next_node):
next_node = skipDuplicates(next_node)
node.next = next_node
node = node.next
return dummy_head.next
```

初めは以下のような、dummy_headのところを`previous`とし、
次に繋げるべきノードを`node`とするような解き方をしていた。
が、これでは末尾にduplicatesがあると`previous.next`がNoneとなり、エラーで動かない。
この解法でも、適切な位置でNoneチェックを入れれば動くが、そもそも
Singly-linked listであれば、previousの値を気にするような解き方よりは、
今注目しているノードとその先のノードの関係を調べるような解き方の方が素直だと感じて修正した。
入力が特定のデータ構造を持っているのなら、解法もそのデータ構造に合わせておきたい。
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
def isSameAsNext(node):
return node is not None and node.next is not None \
and node.val == node.next.val
def skipDuplicates(node):
if node is None:
return None
while node.next is not None and node.val == node.next.val:
node = node.next
if isSameAsNext(node.next):
return skipDuplicates(node.next)
Copy link

Choose a reason for hiding this comment

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

同じ値を持つ一連のノードを取り除いたあと、その直後の同じ値を持つ一連のノードを取り除くために再起関数を使うという点が、不必要に複雑に感じました。

return node.next

dummy_head = ListNode(-1, head)
previous = dummy_head
node = head
while node is not None:
if isSameAsNext(node):
previous.next = skipDuplicates(node)
previous = previous.next
node = previous.next
return dummy_head.next
```

修正可能箇所
- `skipDuplicates()`が再帰関数となっている。
今回は、入力ノード数がデフォルトのrecursion limit(1000)以下なので問題ないが、
pythonのpythonの末尾再帰最適化がどうなっているのかよくわかっていないので、
この書き方でノード数が増えたらどうなるのか聞かれたら答えられない。
- `hasSameValueAsNextNode(node)`という関数名。
やりたいこと自体は伝わると思うが、inner functionにしては少し長い気もする。
- magic number:`dummy_head = ListNode(-1, head)`としているが、どうせsentinelとして用いるだけなので、値はいらない。
- `dummy_head = ListNode(-1, head)`:解法が切断しておいて、duplicateでない値であったらくっつけるやり方をしているのに、dummy_head.nextがheadになっているのが気持ち悪い。

nをノード数として
- time complexity: O(n)
- space complexity: O(n) (Auxiliary space: O(1))

# step 2
参考
- https://github.com/konnysh/arai60/pull/4
切断しておいて確定したら繋ぐタイプの解法
- https://github.com/ichika0615/arai60/pull/5/files
切断しておいて確定したら繋ぐタイプの解法
- https://github.com/tarinaihitori/leetcode/pull/4
繋いでおいて確定したら切るタイプの解法、その他set()を使ったものなど
- https://discord.com/channels/1084280443945353267/1227073733844406343/1228673329284513843
切断しておいて確定したら繋ぐ方法と、繋いでおいて確定したら切る方法があるという話。切断しておいて繋ぐ方が自然に感じた。
- https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html
pythonはtail recursive eliminationをサポートしていないみたい。
実際、手元でtail recursiveなfactorial関数を作り`sys.getrecursionlimit()`(1000)を超える値で動かしてみたがRecursionErrorとなった。
今回の実装で再帰を使うメリットは、入力されたリストにcycleがあった場合、Recursion Errorとなる。
デメリットとしては、入力サイズが大きい場合、cycleのない入力であってもRecursion Errorとなる。

(切断しておいて繋ぐ)二重ループでの解法
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode()
tail = dummy_head
node = head
while node is not None:
if node.next is None or node.val != node.next.val:
tail.next = node
tail = tail.next
node = node.next
Copy link

Choose a reason for hiding this comment

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

切断しておいて繋ぐ、が私の意図とは異なるもので伝わっている気がします。
ここの部分は、node が取り除かれないものであると確定して、node が node.next に移った瞬間だと思いますが、この時点で tail.next が node を指しているはずです。
つまり、「dummy_head からたどれるものは、すべて最終的な完成品に含まれると確定したものだけである」とはなっていませんね。
「切断しておいて繋ぐ」の私の意図は、ここで tail.next = None することによって、「dummy_head からたどれるものは、すべて最終的な完成品に含まれると確定したものだけである」ようにしようということです。そうすると return の前の tail.next = None が不要になります。

Copy link
Owner Author

Choose a reason for hiding this comment

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

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

  • 切断しておいて繋ぐ:nodeが取り除かれないと確定したタイミングでtailを更新(tail.nextはそのままnodeの先に繋がる)
  • 繋いでおいて切断:今注目しているnode(これ自体は、取り除かれないと確定)について、node.nextを更新

みたいな認識でした。
たしかに、このコードだと切断のニュアンスはありませんね。

continue
duplicated_value = node.val
while node.next is not None and node.next.val == duplicated_value:
node = node.next
node = node.next
tail.next = None
return dummy_head.next
```
(切断しておいて繋ぐ)一重ループでの解法
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode()
tail = dummy_head
node = head
has_duplicate = False
while node is not None:
if node.next is not None and node.val == node.next.val:
has_duplicate = True
node = node.next
continue

if has_duplicate:
has_duplicate = False
else:
tail.next = node
tail = tail.next
node = node.next
tail.next = None
return dummy_head.next
```

(繋いでおいて切断する)二重ループ解法
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(next=head)
node = dummy_head
while node is not None:
Comment on lines +144 to +145
Copy link

Choose a reason for hiding this comment

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

nodeをNoneチェックする必要があるか疑問に感じました。
というのも、nodeの初期値がNoneの可能性があるheadなら、nodeにはNoneチェックが必要なものが入っていって、nodeがNoneの可能性がない実体で始まるなら、それと同じようにnodeにはNoneの可能性がない実体があるものが入っていくのかなと個人的には考えます。

繋いでから切断、切断してから繋ぐみたいなのも、自分自身ちゃんとは分かっていないので、もしかしたら変な指摘かもしれません🙇‍♂️

Copy link
Owner Author

Choose a reason for hiding this comment

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

繋いでから切断、切断してから繋ぐみたいなのも、自分自身ちゃんとは分かっていないので、もしかしたら変な指摘かもしれません🙇‍♂️

こちらですが、言い換えただけにはなりますが、自分にとって腑に落ちたイメージを話します。
まず、繋いでから切断する、切断しておいて繋ぐという解法のどちらも以下の共通点があります。

  • 与えられたsingly-linked listをheadから順に見ていく
  • 値に重複があれば、適宜ノードの繋ぐ先を繋ぎかえる
  • 返り値として、「重複のない、昇順に並んだsingly-linked listのhead」を返す

次に相違点ですが、「与えられたsingly-linked listをheadから順に見ていく」ループの中で、保持している値に違いがあります。

  • 切断しておいて繋ぐ:常に、「回答となるものの一部もしくは全部」、つまり、「重複のない、昇順に並んだsingly-linked list」を保持します。
  • 繋いでから切断:「回答となるものの一部もしくは全部 + まだチェックしていないlinked-list」を保持します。

ですので、例えば1 -> 1 -> 2 -> 3 -> 4 -> 4 -> Noneなるリストが入力として与えられた場合、各数値ごとにそれぞれ次のような遷移をします。

  • 切断しておいて繋ぐ
    (何もチェックしていない)
    dummy_head -> None
    1 -> 1 -> 2 -> 3 -> 4 -> 4 -> None
    
    (1までチェックした)
    dummy_head -> None
    2 -> 3 -> 4 -> 4 -> None
    
    (2までチェックした)
    dummy_head -> 2 -> None
    3 -> 4 -> 4 -> None
    
    (3までチェックした)
    dummy_head-> 2 -> 3 -> None
    4 -> 4 -> None
    
    (4までチェックした)
    dummy_head-> 2 -> 3 -> None
    None
    
    (末尾まで到達)
    dummy_head -> 2 -> 3 -> None
    None
    
  • 繋いでおいて切断する
    (何もチェックしていない)
    dummy_head -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> None
    
    (1までチェックした)
    dummy_head -> 2 -> 3 -> 4 -> 4 -> None
    
    (2までチェックした)
    dummy_head -> 2 -> 3 -> 4 -> 4 -> None
    
    (3までチェックした)
    dummy_head -> 2 -> 3 -> 4 -> 4 -> None
    
    (4までチェックした)
    dummy_head -> 2 -> 3 -> None
    
    (末尾まで到達)
    dummy_head -> 2 -> 3 -> None
    

どちらの解法も考えやすい部分と、考えにくい部分があると思います。
切断しておいて繋ぐ場合、ループ内で意味的に変わらないものが返り値の条件を満たしている点では考えやすいですが、一つのリストの繋ぎ換えをしているのに、元のリストと返り値となるリストを別で扱うようなイメージになる点が少し考えにくいです(これは、ノードの繋ぎ変えの順序を間違えるとうまく動作しない面倒さに現れていると思います)。
一方で、繋いでおいて切断するものは、一つのリストを繋ぎかえているイメージはつきやすい一方、返り値の条件を満たすものが最後の最後まで出てこない面倒さがあると思います。また、単にチェック済みのノードの末尾をtailと置くと、tail.next以降にもリストがつながっていることがあり、下手な命名をすると単語の意味が実際と結びつきません。
どちらの解法もstep 4で追加したので、よければ見てみてください(step 3までの解法は、すべて繋いでおいて切断するになっています。認識違いでした)。

nodeをNoneチェックする必要があるか疑問に感じました。
というのも、nodeの初期値がNoneの可能性があるheadなら、nodeにはNoneチェックが必要なものが入っていって、nodeがNoneの可能性がない実体で始まるなら、それと同じようにnodeにはNoneの可能性がない実体があるものが入っていくのかなと個人的には考えます。

こちらはおそらく型に誤解があるのではないかと感じました。nodeの初期値がdummy_head(=ListNode(next=head))となっているため、nodeの型をListNodeと捉えているのではないでしょうか?
僕の想定では、この型はOptional[ListNode]( = ListNode | None)となっています。というのも、nodeを末尾まで探索していくと、サイクルがなければいずれはnode = Noneとなるからです。こちらについては、typeアノテーションしておくべきだったかもしれません。

こちらが参考になれば幸いです。コメントありがとうございます。

Copy link

Choose a reason for hiding this comment

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

繋いでから切断、切断してから繋ぐ

これ、私が適当に様子を表現しただけで、特に専門用語ではありませんので、それほどこれ自体を真面目に考える必要はないです。

ただ、これ、ループごとに仕事の引き継ぎをしていると思う(物理的にこれを繋ぎ変える仕事をしていると考える、下のリンク参照)と、「こういう状態にして引き継ぐからね」という約束をどうするかはとても大切なはずです。

ichika0615/arai60#3 (comment)
列車
https://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966

https://discord.com/channels/1084280443945353267/1231966485610758196/1239417493211320382

Copy link

Choose a reason for hiding this comment

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

丁寧な説明ありがとうございます!繋いでから切断、切断してから繋ぐのイメージがわかりました

こちらはおそらく型に誤解があるのではないかと感じました。nodeの初期値がdummy_head(=ListNode(next=head))となっているため、nodeの型をListNodeと捉えているのではないでしょうか?

自分の書き方が悪くて意図を伝えられてませんでした。意図としてはnodeの初期値がListNodeなら、ループが進んでも同様に、nodeをOptionalの付かないListNodeの型にしておける書き方がある、ということを伝えたかったです。自分が書いた1重whileのコードと動作が同じなのに、こちらだと2重whileのコードになっていたので、nodeのNoneチェックするwhileを減らせると思いコメントしました。

has_duplicate = False
Copy link

Choose a reason for hiding this comment

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

has_duplicate フラグで処理を切り替える部分が不必要に複雑に感じました。 step3 の最初のソースコードのほうがシンプルだと感じました。

while node.next is not None and node.next.next is not None \
and node.next.val == node.next.next.val:
has_duplicate = True
node.next = node.next.next
if has_duplicate:
node.next = node.next.next
else:
node = node.next
return dummy_head.next
```

(繋いでおいて切断する)一重ループ解法
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(next=head)
node = dummy_head
has_duplicate = False
while node is not None:
if node.next is not None and node.next.next is not None \
and node.next.val == node.next.next.val:
has_duplicate = True
node.next = node.next.next
continue

if has_duplicate:
has_duplicate = False
node.next = node.next.next
else:
node = node.next
return dummy_head.next
```

# step 3
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode()
tail = dummy_head
node = head
while node is not None:
if node.next is None or node.val != node.next.val:
tail.next = node
tail = tail.next
node = node.next
continue

duplicated_value = node.val
while node.next is not None and node.next.val == duplicated_value:
node = node.next
node = node.next
tail.next = None
return dummy_head.next
```

```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode()
tail = dummy_head
node = head
has_duplicate = False
while node is not None:
if node.next is not None and node.val == node.next.val:
has_duplicate = True
node = node.next
continue

if has_duplicate:
has_duplicate = False
else:
tail.next = node
tail = tail.next
node = node.next
tail.next = None
return dummy_head.next
```
```python3
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy_head = ListNode(next=head)
node = dummy_head
while node is not None:
has_duplicate = False
while node.next is not None and node.next.next is not None \
and node.next.val == node.next.next.val:
node.next = node.next.next
has_duplicate = True

if has_duplicate:
node.next = node.next.next
else:
node = node.next
return dummy_head.next
```