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

Create remove_duplicate_from_sorted_list_II #5

Open
wants to merge 5 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
172 changes: 172 additions & 0 deletions remove_duplicate_from_sorted_list_II.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 82. Remove duplicate from sorted list II

## すでに解いた方々(in:レビュー依頼 titleで検索)
- https://github.com/atomina1/Arai60_review/pull/3/files
- https://github.com/t0hsumi/leetcode/pull/4/files
- https://github.com/katataku/leetcode/pull/3/files
- https://github.com/ichika0615/arai60/pull/5/files
- https://github.com/tarinaihitori/leetcode/pull/4/files

***********************************************************************************************************************************************************************************
以前「やってることは間違っていない」とコメントをいただいているので、その点は安心しているのですが、step1~3を終えるのに6時間くらいかかってしまいました。
度々こういうことがありまして、コードの読み書きに不慣れとはいえ時間がかかりすぎで、何か根本的な問題があるような気がしています。
内訳は
- Step1(2時間くらい):「どの解法がいいかな」とPRを漁るのに時間がかかる。自分の発想に近い解き方をしている人を探していて、工程がStep2と混ざっているかも。
- Step2 (4時間くらい):レビューをお願いする5人分を確認し、PRを一通り読んで問題に対してどんな解法があるのか一通り理解する。どんな解法があるのか抑える過程にすごく時間がかかるが、これが理解できないとコードがちゃんと読めていない感じがする。
- Step3(20分くらい):Step2で見た解法のうち、しっくりくるものを思い出しながら再現する。打ってるうちに覚えてくるので、ここはあまり時間はかからない。
という感じなのですが、改善すべき意識や工程などありますか…?それとも初心者はこんなもので、やってるうちに早くなるものなんでしょうか?
***********************************************************************************************************************************************************************************
Copy link

Choose a reason for hiding this comment

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

6時間は長めな気はしますが、はじめ3時間位だったのが30問くらいで1時間を切るようになった人もいると聞いています。


## Step 1
### 考えたこと
- 1問前の.nextを一つ先まで見ればいけそうだなと思い、node_next = node.nextという変数を導入してトライしてみるが、初手で1, 1と続いたケースが通らない。headを動かしたり重複がみられた変数をset()に放り込むなどやってみたが、こんがらがってきたところでギブアップ。
- 皆さんのコードを眺めてみると、ListNode(0)の次にheadを置いている。言われてみれば当たり前だが、これはサッパリ思いつかなかった。
- Optional[int]を使うとダミー用の番号は不要でNoneで動くらしい:https://discord.com/channels/1084280443945353267/1226508154833993788/1246022270984392724
- 答えを見たが一度読んだだけではしっくりこず、元の発想通り重複がみられた変数をset()に放り込む方法で書いてみた。納得はできたが、「なぜこれだと動かないのか」「なぜここを変えると動くようになるのか」を納得するのに時間がかかりすぎている感がある(今回ならstep1に2時間とかかかってしまった…)。
Copy link

Choose a reason for hiding this comment

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

他人のコードが読めないときには print 文を大量に挟んで実行してみるのがお勧めです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

恥ずかしながらこのご指摘をいただいてはじめて、LeetCode上で標準出力が確認できることに気づきました。。。
おかげ様で全てのステップが格段に楽になると思われます。ご丁寧に指摘いただきありがとうございました。


 -> 一度自分で解いた解法じゃないと他の人のコードを全然読めていなくて、そもそもコードを読んだり頭の中で挙動を再現する力が弱そう。現状Step1の時点では「誰かの回答を写経して、気になるところを変えながら挙動を理解していく」とかなら時間がかからなそうなので、次は試してみる。
Copy link

Choose a reason for hiding this comment

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

個人的には1問1時間位で考えています。あまり長いと退屈に感じるでしょう。この練習方法は学習曲線の傾きを急にする方法で、一番の敵は、難しくて辛いというよりは退屈なことです。つまり、報酬を感じないことです。
step 3 で「はじめはかかっていたものが、さくっと書けるようになること」自体を報酬に感じるのが理想なのですが。

- Inplaceとnot-inplaceというらしい: https://github.com/cheeseNA/leetcode/pull/9/files

### 当初の発想で書いたコード
```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(None, head)
node = dummy

value_duplicated = set()

while node.next:
if node.val == node.next.val:
value_duplicated.add(node.val)
node = node.next

node = dummy

while node:
while node.next and node.next.val in value_duplicated:
node.next = node.next.next
node = node.next

return dummy.next
```

## Step 2
### 学んだこと
- 例によってsortされてるのでsetとして重複変数を保存する必要はなかった。while文1つにまとめれそう https://discord.com/channels/1084280443945353267/1195700948786491403/1196701558382018590 。
- 長すぎる条件が気になっていたが、これをまとめるアイデアがあった: https://github.com/rinost081/LeetCode/pull/6#discussion_r1744911468
- 大きく分けて解き方は3つっぽい。再帰で解く、繋いでから切る、2つListNodeを用意して重複のないものだけを繋いでいく。
- https://github.com/tarinaihitori/leetcode/pull/4/files#r1807830600 を見ると、.next.nextを用いたやり方が自分の最初の発想と連続していて、素直で理解しやすいと感じる。重複の判定を別の関数でやるのもスッキリしているのでこれを目指す。
- whileとif breakはwhile notに書き換えれるらしい: https://discord.com/channels/1084280443945353267/1227073733844406343/1228598526712483902
- If elseを見ると読み慣れている人はこんなふうに考えるのか: https://github.com/atomina1/Arai60_review/pull/3/files#r1893541458
- 命名としてdummyではなくsentinel, valじゃなくてvalueの方が良さそう:https://github.com/katataku/leetcode/pull/3/files

### 繋いでから切るコード
```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(None, head)
node = sentinel

while node:
is_duplicated = False
Copy link

Choose a reason for hiding this comment

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

フラグが使われたコードは、読むにあたりやや認知負荷が高くなるように思います。無理なく避けられるのであれば、避けたほうが良いと思います。以下のようなコードはいかがでしょうか?

if node.next and node.next.next and node.next.val == node.next.next.val:
    while node.next.next and node.next.val == node.next.next.val:
        node.next = node.next.next
    node.next = node.next.next
else:
    node = node.next

一部コードが重複してしまっていますが、許容範囲だと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

当初はご提示いただいたものと近い方向で考えていたのですが、条件分が長くなったり重複するのが嫌で他の方のコードを真似して書いてみました。フラグを使うことの認知負荷自体考えたこともなかったので、今後他の方のコードを読む際に意識してみます。

while node.next and node.next.next and node.next.val == node.next.next.val:
is_duplicated = True
node.next = node.next.next
if is_duplicated:
node.next = node.next.next
else:
node = node.next

return sentinel.next
```
### 重複のないものだけを繋いでいくコード
Copy link

Choose a reason for hiding this comment

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

質問なんですが、この解法って「切断しておいて使うものだけ繋ぐ」解法とは違うものと考えていいんでしょうか?

書かれている解法ではdummy = ListNode(None, head)でdummy.nextがheadを指す状態でスタートして、whileループ内で、ノードの繋ぎ変えをしているので、「繋いでから切るコード」と何が違うのかよくわからなかったです。

Copy link

Choose a reason for hiding this comment

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

繋ぐ、繋がないみたいなのの自分のイメージは以下の通りとなります

t0hsumi/leetcode#4 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

遅くなってすいません。実はこの辺何度読んでも理解できていないのですが、もう少し考えてみます…。
自分は
切断しておいて繋ぐ:2つポインタを用意しておいて、片方は重複の有無を走査し、もう片方のポインタでは条件を満たすものだけを繋いでいく
繋いでから切断する:単一のポインタを走らせて、条件を満たさないものはスキップする
というイメージで書いておりましたが、日を置いてご指摘をいただくと確かにやってること同じですね…。

Copy link

Choose a reason for hiding this comment

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

あー、ループを仕事の引き継ぎだと思いましょう。
「直径 1 m 金属の輪っか」に 2 m の鎖が生えていてその先に「南京錠」がついているとしましょう。LinkedList とは、これを一直線に並べて、南京錠を隣の輪っかにつなげていったものです。最後の南京錠は何にもつながっていません。
という状況です。

ループを引き継ぐ瞬間に、前日のシフトが今日のシフトに申し送りをします。

  • 「dummy からはじまって unique で終わる」まで繋がっている一連のやつは、重複が除かれた鎖。
  • scan から始まる部分は、まだ処理されてない鎖。

ところで、ここでもう一つ、引き継ぎの瞬間に「unique.next == scan」を約束しているか、という問題があります。
どちらでもできるが決めておかないと後の人がどうするか分からなくなります。

Copy link
Owner Author

Choose a reason for hiding this comment

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

お2人のコメントを交互に読んで、やっと理解できたと思います。

引き継ぎの瞬間に「unique.next == scan」を約束しているか、という問題があります。
どちらでもできるが決めておかないと後の人がどうするか分からなくなります。

1つ目のifのところだけunique.next = scanが担保されてないところですかね...?日をおいてもう一度考えてみます。

Copy link

Choose a reason for hiding this comment

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

理解した気がします。僕が各変数に割り当てられている意味を十分に意識していなかったですね。

  • 繋いでから切るコード
    • 「sentinelからnodeまで」が処理済み
    • node.next以降が未処理
  • 重複のないものだけを繋いでいくコード
    • 「dummy からはじまって unique で終わる」まで繋がっている一連のやつは、重複が除かれた鎖。

    • scan から始まる部分は、まだ処理されてない鎖。

    • 引き継ぎの瞬間に「unique.next == scan」を約束

で、明示的に重複のないノードまでの変数(unique)を置いているかが違いますね。

一方で、

  • 切断しておいて繋ぐ場合、
    • 「dummy からはじまって unique で終わる」まで繋がっている一連のやつは、重複が除かれた鎖。
    • scan から始まる部分は、まだ処理されてない鎖。
    • 引き継ぎの瞬間に「unique.next is None」を約束

とすれば書けますね。

Copy link

Choose a reason for hiding this comment

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

そうですね。

一応、もう一つの選択肢として、引き継ぎの瞬間に「unique.next is None」か「unique.next == scan」は不明。次の日の人が片付けろ。というのもあります。

理解して意識的にどちらでも対応するとしていればいいのですが、分かっていない場合は、初日とそれ以外の日でやっていることが違うのでこの現象が起きて、そして、ループが終わった後に後始末をしなくてはいけなくなったりします。

二分探索で混乱している人も、これと基本的に同じところで混乱しているように見えるので、二分探索の理解にもこれは役に立つかもしれませんね。

```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(None, head)
unique = dummy
scan = head

while scan:
# 値が重複するときはscanを走査してスキップ
if scan.next and scan.val == scan.next.val:
while scan.next and scan.val == scan.next.val:
scan.next = scan.next.next
unique.next = scan.next
# 値が重複しないときはuniqueに追加
elif scan.next and scan.val != scan.next.val:
unique.next = scan
unique = scan
Comment on lines +98 to +99
Copy link

Choose a reason for hiding this comment

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

以下の方がuniqueに追加して進めている感じが強いと思いましたが、好みだと思います。

unique.next = scan
unique = unique.next

# scan.nextがNoneの時は終了
else:
Copy link

Choose a reason for hiding this comment

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

この3つの条件、

if not scan.next:
    unique.next = scan
    break

を一番上に持ってくると、次の条件は、scan.next を前提にできるので scan.val == scan.next.val で if-else にできますね。

Copy link

Choose a reason for hiding this comment

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

あるいは、そのまま break せずに scan = scan.next とすれば、while の条件をそのまま抜けてくれます。

個人的には「重複のないものだけを繋いでいくコード」が一番人が手でやるんだったら考えそうな方法に思えるのでこれが理解できたのはよいと思います。(こういうところを報酬にできるといいです。ゲームの隠し要素を発見みたいな感覚で自分で自分を褒められるかどうかです。)

Copy link
Owner Author

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.

せっかく教えていただいたので後で見やすい様に追記

if not scan.next:を一番上に持ってくる

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(None, head)
        unique = dummy
        scan = head

        while scan:
            # 値が重複するときはscanを走査してスキップ
            if not scan.next:
                unique.next = scan
                break

            if scan.val == scan.next.val:
                while scan.next and scan.val == scan.next.val:
                    scan.next = scan.next.next
                unique.next = scan.next
            # 値が重複しないときはuniqueに追加
            else:
                unique.next = scan
                unique = scan

            scan = scan.next

        return dummy.next

break せずに scan = scan.next とすれば、while の条件をそのまま抜けてくれます
ここまでのif elseですでにscan.nextが存在するケースは抑えてるのでbreakしなくても良い。

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(None, head)
        unique = dummy
        scan = head

        while scan:
            # 値が重複するときはscanを走査してスキップ
            if scan.next and scan.val == scan.next.val:
                while scan.next and scan.val == scan.next.val:
                    scan.next = scan.next.next
                unique.next = scan.next
            # 値が重複しないときはuniqueに追加
            elif scan.next and scan.val != scan.next.val:
                unique.next = scan
                unique = scan
            else:
                unique.next = scan

            scan = scan.next

        return dummy.next

print(scan.next)
unique.next = scan
break

scan = scan.next

return dummy.next
```
### 再帰を使ったコード
回答を見ていると再帰を使っている人も多いので、練習のため1度再帰で通してみる。

```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None or head.next is None:
return head

if head.val != head.next.val:
head.next = self.deleteDuplicates(head.next)
return head

while head.next is not None and head.val == head.next.val:
head = head.next
return self.deleteDuplicates(head.next)
```



## Step 3
- ListNodeの問題、Nodeを1個ずつ処理していくイメージがあったので、while node:~ node = node.nextでかけて欲しい気持ちがあるが、良い書き方が思い浮かばなかった。
- 3:54, 3:21, 3:08
```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(None, head)
node = sentinel

while node:
is_duplicate = False
while node.next and node.next.next and node.next.val == node.next.next.val:
node.next = node.next.next
is_duplicate = True
if is_duplicate:
node.next = node.next.next
else:
node = node.next

return sentinel.next
```
### Step4?
しっくり来てなかったので日をおいて解き直し
```Python
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(0, head)
ptr = sentinel

while ptr:
scan = ptr
if scan.next and scan.next.next and scan.next.val == scan.next.next.val:
while (
scan.next and scan.next.next and scan.next.val == scan.next.next.val
):
scan.next = scan.next.next
scan.next = scan.next.next
else:
ptr.next = scan.next
ptr = ptr.next

return sentinel.next
```