-
Notifications
You must be signed in to change notification settings - Fork 0
82. Remove Duplicates from Sorted List II #38
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| node = head | ||
|
|
||
| while node: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こちらのコメントをご参照ください。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. コメントの参照ありがとうございます。今回の場合では |
||
| 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] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こちらのコードは、入力を書き換えていることは認識しておくとよいです。 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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さえあればいいので)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. コード例の提示ありがとうございます。 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)$ で済む | ||
| - だいぶ前に(別の問題だが)似た解法を使ったのを思い出した | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この方針ではソートされていることが前提ですが、元の方針ならソートされていなくてもできる、ということはおさえておきたいですね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 とかもいいかもしれません。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if A:
長い処理
continue
短い処理の形ですが、これだと「長い処理」を読んだあと「短い処理」の部分の条件を把握(思い出す)のに「長い処理」の前の A に遡って条件を確認しないといけなくて(あるいは、「not A の場合どうなるんだろう」、というのを頭に置いたまま「長い処理」を読むので長時間ワーキングメモリを取られる感じがして)個人的にはやや負荷を感じました。 if not A:
短い処理
continue
長い処理のように入れ替えるとより読みやすいかもしれません。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 確かに初見だと、現在の分岐の書き方だとワーキングメモリを喰われますね。ご指摘ありがとうございます。 |
||
|
|
||
| return sentinel.next | ||
| ``` | ||
| - 所要時間: | ||
| - 1回目: 2:31 | ||
| - 2回目: 2:16 | ||
| - 3回目: 3:41 | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
さらに付け加えると、3.7 以降は通常の dict でもデフォルトで挿入順を保持するのが仕様として定められているので基本的には通常の dict を使うのが良いとされているようで、バージョンを意識しながら書けると良さそうです。
https://docs.python.org/ja/3/c-api/dict.html#ordered-dictionaries
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3.7以降であれば基本的に通常のdictで良さそうですね