82. Remove Duplicates from Sorted List II - #4
Conversation
|
|
||
| ### Step 2 | ||
|
|
||
| まずは「重複したら引き継ぐ」パターンで解いてみた。dummy.next を return する前に previous.next = None と書くのが疑問だったが、最後が重複で終わった時 ([1,2,3,3]の場合など)でも previous.next = current としているため、previous の先には重複がまだ残っていることになるため、必要だと理解した。 |
There was a problem hiding this comment.
dummy.next を return する前に previous.next = None と書くのが疑問
これもまあ、約束次第で「次に処理する列車」を後ろにつけておくという約束( previous.next = current )ならば外さないといけません。
「切り離して引き継ぐ」という約束にしておけば外す必要はないです。
There was a problem hiding this comment.
おっしゃる通りですね。
確かに以下のようにすれば「切り離して引き継ぐ」になり、previous.next = Noneを最後につける必要は無くなりますね!
class SolutionWithSeparationAndHandOver:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(next=head)
previous = dummy
current = head
val_to_remove = None
while current is not None:
if current.val == val_to_remove:
current = current.next
continue
if current.next is not None and current.val == current.next.val:
val_to_remove = current.val
previous.next = None // 切り離してから引き継ぐ
current = current.next
continue
previous.next = current
previous = previous.next
current = current.next
return dummy.next
nanae772
left a comment
There was a problem hiding this comment.
お疲れ様です。全体的にロジックが整理されていて非常に読みやすかったです。私はこの問題のロジックが上手く整理できず苦戦したので、いろいろと勉強になりました。
|
|
||
| ### Step 1 | ||
|
|
||
| 最初は既存の List を変更する形で行なっていたが、重複している node を完全に排除するには新しい List を作る方がすぐできるのではと思い、実装してみたが、時間が足らず 5 分経過。 |
There was a problem hiding this comment.
新しい List を作る方がすぐできるのでは
この「すぐに」はプログラムの実行速度的な意味でしょうか?
そうであれば新しいListを作るには各ListNodeのオブジェクト生成がかかるのであまり速くはならないのかなと思いました。
There was a problem hiding this comment.
分かりづらくてすみません、これは、より実装時間をかけないで完成させられるのではないかという意味合いで書いた次第です。
そうであれば新しいListを作るには各ListNodeのオブジェクト生成がかかるのであまり速くはならないのかなと思いました。
実行速度という観点では確かにそうですね!
| if current.next is not None and current.val == current.next.val: | ||
| value_to_skip = current.val | ||
| continue | ||
| previous.next = current |
There was a problem hiding this comment.
「上記2つのifのどちらでもない場合ということはここの条件は…」と少し理解に時間がかかったので、コメントやassertなどでここに来る条件が一目で分かるようになっているとより読みやすいかなと個人的には思いました
There was a problem hiding this comment.
ありがとうございます。
個人的には、value_to_skipと同じ値ではなくて (value_to_skip != current.val)、かつ 現在の値とその次の値が異なる(current.val != current.next.val) ということは、値がダブっていないということだ、というのは自明に思えたので、あえてコメントとしては残しませんでした。
参考までに、他のパターンと比べてこの step 2の SolutionWithHandOver が分かりづらかった理由を聞いてもよろしいでしょうか?あと、これは「重複を見つけたら引き継ぐ」パターンで書いたのですが、「重複を見つけたら帰るな」だとコメント不要だと思った感じですか?
There was a problem hiding this comment.
ありがとうございます。
まず結論を先に述べると「コメントやassertはやはり無くてもよいかも」になりました(意見変わってすみません 🙇♂️ )
以下、当時の私が上のコメントをした理由になります。
16行目に入る条件は何だろうと考えるときに
- 10行目の条件の否定を考えて覚えておく
- 13行目の条件の否定を考えて覚えておく
- 1,2のandを取る
と思考に数ステップ必要なので、以下のようにコメントかassertがあったらそれを読むだけでよいのでその思考を省略できて分かりやすいかなと考えました。
# value_to_skip != current.val and (current.next is None and current.val != current.next.val)
assert value_to_skip != current.val and (current.next is None and current.val != current.next.val)またもう1つの解法は前のifが1つだけなので前にあるif条件の否定を考えるだけでよく、そちらは不要だなと思っていました。
…とこのコメントをした当時は考えていたのですが、今になりちょっとそれも微妙かなと思い直しました。
- コメントにしろassertにしろ、余分な行を付け加えることはそれを読んでなぜそれが書かれているかを解釈するというコストがかかってトータルで読み手の負担の軽減にはならなさそうなこと。
- 将来ロジックに変更があった場合、コメントやassertの整合性を取るコストも増え、それを忘れてしまうとより分かりにくい・ミスリードなコードになってしまうこと。
なのでif2個くらいなら書かないほうがマシかなと今は思っています。
There was a problem hiding this comment.
ご丁寧に返答いただき、ありがとうございます! 🙏
コメントにしろassertにしろ、余分な行を付け加えることはそれを読んでなぜそれが書かれているかを解釈するというコストがかかってトータルで読み手の負担の軽減にはならなさそうなこと。
将来ロジックに変更があった場合、コメントやassertの整合性を取るコストも増え、それを忘れてしまうとより分かりにくい・ミスリードなコードになってしまうこと。
全体的に同意です。コメントがあることもコストになるので、あくまでもコードから読み取れないことかつ知ることで読みやすくなること (例えば、Linked List Cycle でいうなら、Hare and Tortoise のリンクなど) をコメントすべきということですよね 👍
| previous.next = current | ||
| previous = previous.next | ||
| current = current.next | ||
| previous.next = None |
There was a problem hiding this comment.
ここはwhileの中でやるのもいいのかなと思いました。コードは大きく変わらないですが
- whileの中でpreviousの連結リストが常に一つしかないものだけを指しているようにできる
- whileの外でやるとやや例外的な処理に見える
ということでwhileの中でやるのが個人的な好みです。
There was a problem hiding this comment.
oda さんとのコメントでも出てきた、「切り離してから引き継ぐ」ですね。
whileの中でpreviousの連結リストが常に一つしかないものだけを指しているようにできる
whileの外でやるとやや例外的な処理に見える
確かに while の中で処理をする方が previous の動きをよりわかりやすい形で提示できそうですね 👍
| @@ -0,0 +1,64 @@ | |||
| class Solution1: | |||
There was a problem hiding this comment.
私はこのやり方が思いつきませんでした、読んでみてコードもシンプルで分かりやすくてよいなと思いました!
| previous = dummy | ||
| current = head |
There was a problem hiding this comment.
(全体的にですが)previous, currentは「前のもの」「今見てるもの」くらいのと書き手の視点からの名前になっているので、もう少し伝わりやすい名前をつけてあげるとより分かりやすくなるかなと思いました。
特にcurrentについては一般的には違和感があるようです(私はあまりまだその感覚が身についてないですが)。
Kota-Isayama/leetcode#1 (comment)
There was a problem hiding this comment.
current の文脈としては、 previous との対比であえて使ってみていました。
が、変数名に情報がほとんどないというのは同意です。なので一旦 node と tail を使う形で書いてみました!
class SolutionWithClearVariableNames:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
def moveToDistinctNode(node: Optional[ListNode], val_to_skip: int) -> ListNode:
while node is not None and node.val == val_to_skip:
node = node.next
return node
dummy = ListNode()
dummy.next = head
tail = dummy
node = head
while node is not None:
if node.next is not None and node.val == node.next.val:
node = moveToDistinctNode(node.next, node.val)
tail.next = None
continue
tail.next = node
tail = tail.next
node = node.next
return dummy.next|
|
||
| class Solution3: | ||
| def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]: | ||
| def skipDuplicates(node, value_to_skip): |
There was a problem hiding this comment.
1まとまりの処理を関数に切り出していただいてるのは非常に読みやすくてよいなと思いました!
名前がskipDuplicatesだけだと何が返ってくるかやや不明瞭な点があるため、自分ならmoveToNextDistinctNodeとかにするかもと思いました。
あと引数や返り値の型アノテーションがあるとより親切かなと思いました。
There was a problem hiding this comment.
名前がskipDuplicatesだけだと何が返ってくるかやや不明瞭な点があるため、自分ならmoveToNextDistinctNodeとかにするかもと思いました。
確かにListNode[]を返すのかListNodeを返すのかあるいは何も返さないのかが不明瞭ですね。moveToDistinctNode に変えました!
あと引数や返り値の型アノテーションがあるとより親切かなと思いました。
確かに自分で関数を定義するときは特に型アノテーションがあった方が可読性が上がりますね!
class SolutionWithMoveToDistinctNode:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
def moveToNextDistinctNode(node: Optional[ListNode], val_to_skip: int) -> ListNode:
while node is not None and node.val == val_to_skip:
node = node.next
return node
dummy = ListNode()
dummy.next = head
previous = dummy
current = head
while current is not None:
if current.next is not None and current.val == current.next.val:
current = moveToNextDistinctNode(current.next, current.val)
previous.next = None
continue
previous.next = current
previous = previous.next
current = current.next
return dummy.next
t-ooka
left a comment
There was a problem hiding this comment.
思考過程がしっかりと言語化されていて素晴らしいなと思いました。一点だけコメントをしています!
| dummy.next = head | ||
| previous = dummy | ||
| current = head | ||
| value_to_skip = None |
There was a problem hiding this comment.
value_to_skip を変数として持たないでも前後のListNodeでvalが同じかどうかを見て、どこまで飛ばすか判断できるかなと思いました。
There was a problem hiding this comment.
なるほど、その発想はなかったです!
class SolutionWithoutValueToSkip:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(next=head)
previous = dummy
current = head
while current is not None:
if current.next is not None and current.val == current.next.val:
while current.next is not None and current.val == current.next.val:
current = current.next
current = current.next
continue
previous.next = current
previous = previous.next
current = current.next
previous.next = None
return dummy.next
こうみると、説明変数を定義しなくていい代わりに、while が2重になってしまうので、ケースバイケースで使い分けるのが良さそうですね。
| return node | ||
|
|
||
| dummy = ListNode() | ||
| dummy.next = head |
There was a problem hiding this comment.
dummy = ListNode(0) とした方が自分はダミーとしての意図が伝わりやすくていいかなと思いました。
あと、ListNode(0, head)にした方が、少し簡潔になると思います。
There was a problem hiding this comment.
dummy = ListNode(0) とした方が自分はダミーとしての意図が伝わりやすくていいかなと思いました。
dummy と命名しているので、ダミーという意図は十分伝わるかなと思ったのと、0よりも undefined にしておく方が初期値に意味がないことを明示できていいかなと思った次第です。
初期値0 は dummy とするみたいな慣例がある感じなのでしょうか?
あと、ListNode(0, head)にした方が、少し簡潔になると思います。
確かに行数は減りますね!
個人的には previous = previous.next と current = current.next に書き方を合わせたかったのであえてそう書きました。ポインターを次にずらす、という同じことはなるべく同じ書き方で表現した方が一貫していてわかりやすいかなと思ったためです。
https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/
Next: https://leetcode.com/problems/add-two-numbers/