Skip to content
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
142 changes: 142 additions & 0 deletions memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# 82. Remove Duplicates from Sorted List II
- 問題: https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
- 言語: Python

## Step1
- 一瞬Setを作成せよということかと思ったが重複する要素(節)を全て削除する
- 入力の最大長が300であるため、線形探索で十分と判断
- 節の値をキー、節オブジェクトに値(配列)としてハッシュマップ(辞書)で持つ
- ハッシュマップからリストを再構成し、ハッシュマップの値の配列の長さが2以上である場合を節の重複とみなし、読み飛ばすようにする
- 重複検出はできたが、削除されずSetを作成している状態で20分経過したので正答を見る

### WAとなるコード
```py
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
value_to_node = defaultdict(list)
node = head

while node:
value_to_node[node.val].append(node)
node = node.next

v_to_n_lst = list(value_to_node.values())
i = 0
while i < len(v_to_n_lst) - 1:
if len(v_to_n_lst[i]) > 1:
i += len(v_to_n_lst[i])
continue
v_to_n_lst[i][0].next = v_to_n_lst[i+1][0]
i += 1

return v_to_n_lst[0][0]
```

### 正答
#### 元の方針を修正した解法
```py
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
value_to_node = defaultdict(list)

@h-masder h-masder Jun 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

一応ですが、Python2.7以降Pyhon3.7以前ならOrdereddictを使うと入れた順番を保持します。
https://pypi.org/project/ordereddict/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

さらに付け加えると、3.7 以降は通常の dict でもデフォルトで挿入順を保持するのが仕様として定められているので基本的には通常の dict を使うのが良いとされているようで、バージョンを意識しながら書けると良さそうです。

Since Python 3.7, dictionaries are ordered by default, so there is usually little need for these functions; prefer PyDict* where possible.

https://docs.python.org/ja/3/c-api/dict.html#ordered-dictionaries

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

3.7以降であれば基本的に通常のdictで良さそうですね

node = head

while node:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちらのコメントをご参照ください。
ksaito0629/leetcode_arai60#1 (comment)

@skypenguins skypenguins Jun 28, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コメントの参照ありがとうございます。今回の場合では node is not None の方がより安全だと理解しました。

value_to_node[node.val].append(node)
node = node.next

# 1個だけのグループ(重複なし)だけ残す
unique_groups = [nodes for nodes in value_to_node.values() if len(nodes) == 1]

# 全て重複していた場合
if not unique_groups:
return None

# ユニークな節を順番に繋ぎ直す
for i in range(len(unique_groups) - 1):
unique_groups[i][0].next = unique_groups[i + 1][0]

# 末尾節の古いnextを切る
unique_groups[-1][0].next = None

return unique_groups[0][0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちらのコードは、入力を書き換えていることは認識しておくとよいです。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.gys2docabo9y

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

入力を書き換えないのであれば、例えば、以下のように書けます。

from collections import OrderedDict
class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        node = head
        value_to_count = OrderedDict()
        while node is not None:
            if node.val not in value_to_count:
                value_to_count[node.val] = 1
            else:
                value_to_count[node.val] += 1
            node = node.next

        dummy = ListNode()
        tail = dummy
        for value, count in value_to_count.items():
            if count == 1:
                tail.next = ListNode(value)
                tail = tail.next

        return dummy.next

(nodeを保持する代わりに何個あったかカウントするようにしました。新しいnodeをつくるときは、valueさえあればいいので)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コード例の提示ありがとうございます。
この関数が使われる状況によりますが、入力を書き換えているかどうかは意識すべきですね

@h-masder h-masder Jun 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

元のコードについてですが、dummyを使うと、もう少しシンプルに書けそうです

        dummy = ListNode()
        tail = dummy

        for nodes in value_to_node.values():
            if len(nodes) == 1:
                tail.next = nodes[0]
                tail = tail.next

        tail.next = None

        return dummy.next

```
- 空間計算量: $O(n)$
- `if len(...) > 1` → `if len(...) == 1` でフィルタ
- `i += len(v_to_n_lst[i])` → そもそもフィルタ済みハッシュマップのグループ単位でアクセスすれば良い
- 古い `next` を切る

#### 2つのポインタを使う方法
```py
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head)
prev = dummy
curr = head

while curr:
# curr が重複しているか確認
if curr.next and curr.val == curr.next.val:
val = curr.val

# 同じ値の節を全部スキップ
while curr and curr.val == val:
curr = curr.next

prev.next = curr
else:
prev = curr
curr = curr.next

return dummy.next
```
- こちらは空間計算量: $O(1)$ で済む
- だいぶ前に(別の問題だが)似た解法を使ったのを思い出した

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この方針ではソートされていることが前提ですが、元の方針ならソートされていなくてもできる、ということはおさえておきたいですね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

おっしゃる通り、ソート前提でなく(重複文字列が連続していない)とも使えるような解法を考えていました。


## Step2
- 典型コメント集: https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.xzxd7jwvkwc5

- https://github.com/goto-untrapped/Arai60/pull/43
- C++
- 確かに、命名として `node` 、 `nextNode` だとちょっと分かりづらいと思った

- https://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966
- Python
- 列車の例え

- https://github.com/nittoco/leetcode/pull/9/changes
- Python
- 二重whileループでない場合は、状態管理の変数が必要になる

## Step3
### 読みやすく書き直したコード
```py
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
sentinel = ListNode(0, head)
previous = sentinel
current = head
Comment on lines +121 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

previous, current という名前ですが、処理の途中で「previous は置き去りにしたまま current をどんどん進める」ところがあって個人的には少しギャップを感じました。

previous の代わりに unique_tail, saved_tail, tail_so_far などの説明的な名前でもいいのかなと思います。

current は current のままでもいいかもしれませんし、 checking とか単に node とかもいいかもしれません。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

自分だと tail_so_far がより適切だと感じました。ご指摘ありがとうございます。


while current:
if current.next and current.val == current.next.val:
visited_value = current.val

while current and current.val == visited_value:
current = current.next

previous.next = current
continue

previous = current
current = current.next
Comment on lines +125 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if A:
    長い処理
    continue
短い処理

の形ですが、これだと「長い処理」を読んだあと「短い処理」の部分の条件を把握(思い出す)のに「長い処理」の前の A に遡って条件を確認しないといけなくて(あるいは、「not A の場合どうなるんだろう」、というのを頭に置いたまま「長い処理」を読むので長時間ワーキングメモリを取られる感じがして)個人的にはやや負荷を感じました。

if not A:
    短い処理
    continue
長い処理

のように入れ替えるとより読みやすいかもしれません。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

確かに初見だと、現在の分岐の書き方だとワーキングメモリを喰われますね。ご指摘ありがとうございます。


return sentinel.next
```
- 所要時間:
- 1回目: 2:31
- 2回目: 2:16
- 3回目: 3:41