-
Notifications
You must be signed in to change notification settings - Fork 0
Create RemoveDuplicatesfromSortedListII.md #4
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,224 @@ | ||||||
| # step1 何も見ずに解く | ||||||
| - Javaのint、Integerの比較についておさらい | ||||||
| - int同士の比較 → 常に値で比較 | ||||||
| - `==`で比較すればよい | ||||||
| - Integer同士の比較 → 常にオブジェクトで比較 | ||||||
| - 値を比較したいなら`Objects.equals()`などで比較 | ||||||
| - ただし -128 ~ 127 の範囲の整数はキャッシュされてるため、同じオブジェクトを参照しており `==` が true になる。 | ||||||
| - intとIntegerの比較 → 常にintで比較 | ||||||
| - Integerがunboxingされて、int同士の比較として実行される | ||||||
| ## 解答 | ||||||
| - もっとシンプルな解法があるはずと思い、挑戦するも挫折 | ||||||
| - とりあえず動くものを一旦実装 | ||||||
| - でもこれって自分の手慣れた道具(MapとかSetとか)を使うことありきの実装になっていて、不健全 | ||||||
| - 計算量 | ||||||
| - 時間計算量: O(n) | ||||||
|
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. Leetcodeの問題って案外実行時間の制限が記載されていないので、そこまで深く考えられていませんでした。 あとこの問題において改めて実行時間を考えてみると、実は |
||||||
| - 空間計算量: O(n) | ||||||
| ```java | ||||||
| /** | ||||||
| * Definition for singly-linked list. | ||||||
| * public class ListNode { | ||||||
| * int val; | ||||||
| * ListNode next; | ||||||
| * ListNode() {} | ||||||
| * ListNode(int val) { this.val = val; } | ||||||
| * ListNode(int val, ListNode next) { this.val = val; this.next = next; } | ||||||
| * } | ||||||
| */ | ||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
| if (head == null) { | ||||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| // 各ノードの数を数える | ||||||
| Map<Integer, Integer> uniqueValCounter = new HashMap<>(); | ||||||
| ListNode node = head; | ||||||
| while (node != null) { | ||||||
| // Mapに値がセットされていなければ1をセット | ||||||
| // Mapに値がセットされていればインクリメントしてセット | ||||||
| // 以下のコードと等価 | ||||||
| // uniqueValCounter.compute(node.val, (k, v) -> v == null ? 1 : v + 1); | ||||||
| uniqueValCounter.merge(node.val, 1, Integer::sum); | ||||||
| node = node.next; | ||||||
| } | ||||||
|
|
||||||
| // 重複していない値をListに詰める | ||||||
| List<Integer> uniqueVals = new ArrayList<>(); | ||||||
| uniqueValCounter.forEach((k, v) -> { | ||||||
| // 重複していない値だけ、Listに詰める | ||||||
| if (v == 1) { | ||||||
| uniqueVals.add(k); | ||||||
| } | ||||||
| }); | ||||||
| Collections.sort(uniqueVals); | ||||||
|
|
||||||
| // 新しいLinkedListを生成 | ||||||
| ListNode newHead = null; | ||||||
| ListNode newTail = null; | ||||||
| for (int val : uniqueVals) { | ||||||
| if (newHead == null) { | ||||||
| newHead = new ListNode(val); | ||||||
| newTail = newHead; | ||||||
| } else { | ||||||
| newTail.next = new ListNode(val); | ||||||
| newTail = newTail.next; | ||||||
| } | ||||||
| } | ||||||
| return newHead; | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
| - stackを使った実装 | ||||||
| - これまでStackってほとんど使ったことがなかったが、書いてみるとすんなり書けた | ||||||
| - Mapを使った実装よりも随分と自然な実装になった | ||||||
| - 計算量 | ||||||
| - 時間計算量: O(n) | ||||||
| - 空間計算量: O(n) | ||||||
| ```java | ||||||
| import java.util.Stack; | ||||||
|
|
||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
| Stack<Integer> stack = new Stack<>(); | ||||||
|
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. スタックデータ構造であれば、 Stack より ArrayDeque を優先的に使うことをおすすめします。 https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayDeque.html
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. StackのリファレンスにもDequeを優先的に使用するよう記載されてますね。ドキュメントの確認が足りていませんでした。 |
||||||
| ListNode node = head; | ||||||
|
|
||||||
| while (node != null) { | ||||||
| if (stack.empty() || stack.peek() != node.val) { | ||||||
| stack.push(node.val); | ||||||
| node = node.next; | ||||||
| } else { | ||||||
|
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. continueを使うとelse以降のネストを減らせるかなと思いました
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. 確かに。ネスト減らしてあげた方が親切ですね。積極的にcontinueしていきます。 |
||||||
| int val = stack.pop(); | ||||||
| while (node != null && val == node.val) { | ||||||
| node = node.next; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ListNode head = null; | ||||||
| ListNode tail = null; | ||||||
|
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より「重複を除去した新しい連結リストの先頭」であることが分かる名前だと良いかなと思いました
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. そうですね。最終的に とでもしておくべきでした。 |
||||||
| while (!stack.empty()) { | ||||||
| tail = new ListNode(stack.pop(), tail); | ||||||
| } | ||||||
| return tail; | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| # step2 他の方の解答を見る | ||||||
| - 参考文献 | ||||||
| - https://github.com/SanakoMeine/leetcode/pull/5/files | ||||||
| - https://github.com/Kyosuke-Asaki/leetcode/pull/5/files | ||||||
| - https://github.com/fuga-98/arai60/pull/5/files | ||||||
| - 番兵: `sentinel`というタームをはじめて知る | ||||||
| - 最初混乱してしまったのは、ループの中で全て処理しようとし過ぎていたことも一因に思う | ||||||
| - 番兵を用いることで条件分岐を減らすことでシンプルに実装できる | ||||||
| - 計算量 | ||||||
| - 時間計算量: O(n) | ||||||
| - 空間計算量: O(1) | ||||||
| ## 解答 | ||||||
| - 最初に見たこの解法、理解するまでにとても時間がかかってしまった | ||||||
| - この実装だと`tail.next`に重複ノードがセットされるのではと懸念していたが次のループで`tail`ごと更新することで問題が起きない | ||||||
| - そこを理解するまでに時間がかかった | ||||||
| ```java | ||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
|
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. val をセットされているので、処理の流れとしては近しいですが、「実装を見つけたら引き継ぐ」で書いてみるとどうなりますか?具体的には、 while を1重で書くとしたらどうなりますか?
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. この方針の実装はあまり考えていなかったです。リンク付きで紹介いただきありがとうございます! class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode();
dummy.next = head;
ListNode tail = dummy;
ListNode node = head;
Integer skippingVal = null;
while (node != null) {
if (node.val == skippingVal) {
node = node.next;
continue;
}
if (node.next != null && node.val == node.next.val) {
skippingVal = node.val;
tail.next = null;
node = node.next;
continue;
}
tail.next = node;
tail = tail.next;
node = node.next;
}
return dummy.next;
}
} |
||||||
| // ダミー、セットされている値に意味は無い | ||||||
| ListNode dummy = new ListNode(Integer.MIN_VALUE, head); | ||||||
|
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. 一行でnextに値をセットできるかつ、valがnullの場合の例外処理など考える必要が無くなって良いかなと思ったのですが、確かに以下のように2行に分けて書いてしまった方が誤解が少ないかもしれないですね。 ListNode dummy = new ListNode();
dummy.next = head; |
||||||
| ListNode tail = dummy; | ||||||
|
Comment on lines
+127
to
+128
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.
ListNode dummy_head = new ListNode(Integer.MIN_VALUE, head);
ListNode dummy_node = dummy_head;として
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. 実行中は常に応答するListNodeの末尾のノードが代入されているので |
||||||
| ListNode node = head; | ||||||
|
|
||||||
| while (node != null && node.next != null) { | ||||||
| int val = node.val; | ||||||
| // 次のノードと異なる場合 | ||||||
| if (val != node.next.val) { | ||||||
|
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. ここでは
Suggested change
とし、このifを抜けた後に本当に必要になったタイミング(nodeが変わっていくので今のnode.valを変数に固定していないと困る)でvalを定義するのも良いかなと思いました。 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. val だとなんの value なのかの情報がないので、val_to_remove などの名前に変えて情報量を増やすと見通しがより良くなるかなと思いました
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. 仰る通りどのnodeのvalなのかコードを読まないと判断できないですね。 |
||||||
| // nextに誤ったノードがセットされていても、ここでtail丸ごと上書き | ||||||
| tail = node; | ||||||
| node = node.next; | ||||||
| continue; | ||||||
| } | ||||||
| // 同じ値のノードが連続する場合、スキップしていく | ||||||
| while (node != null && val == node.val) { | ||||||
| node = node.next; | ||||||
| } | ||||||
| // 重複があるノードの可能性があるが一旦セット | ||||||
| // 重複していた場合、次のループでtailごと上書きされる | ||||||
| tail.next = node; | ||||||
| } | ||||||
| return dummy.next; | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
| - 外側のループ開始時、nodeは連続する同じvalを持つnodeの先頭であることが常に保たれる | ||||||
| - これが一番分かりやすく感じた | ||||||
| ```java | ||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
| ListNode dummy = new ListNode(Integer.MIN_VALUE, head); | ||||||
| ListNode tail = dummy; | ||||||
| ListNode node = head; | ||||||
|
|
||||||
| // 外側のループ開始時、nodeは連続する同じvalを持つnodeの先頭であることが常に保たれる | ||||||
| while (node != null) { | ||||||
| if (node.next == null || node.val != node.next.val) { | ||||||
| tail.next = new ListNode(node.val); | ||||||
|
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. ここでnodeではなく新しいListNodeのインスタンスを作られているのはなぜでしょうか?
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. nodeをセットしてしまうとnextにゴミ(省かれるべき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. すみません、単にnodeを繋げてしまうと上手くいかないのでそこは調整する必要がありましたね。失礼しました。 質問の意図としてはこのメソッドを非破壊的操作(元のリストに変更を加えずに新しいリストを作成する)にしたかったのか、破壊的操作(元のリストをin-placeで変更して重複を消去する)にしたかったか、どちらだったでしょうか? ということをお聞きしたかったです 🙇♂️
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. 正直なお答えをいただきありがとうございます 🙇♂️
となると思うので、どちらかに統一されているとより良いのかなと思った次第でした!
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 = tail.next; | ||||||
| } else { | ||||||
| // 重複したノードを省き、次のvalを持つノードが現れるまでスキップ | ||||||
| while (node.next != null && node.val == node.next.val) { | ||||||
| node.next = node.next.next; | ||||||
| } | ||||||
| } | ||||||
| node = node.next; | ||||||
| } | ||||||
| return dummy.next; | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
| - 再起 | ||||||
| - シンプルだけど、パッと見では挙動をイメージできなかった | ||||||
| - 末尾の要素から繋げていくということか | ||||||
| ```java | ||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
| if (head == null || head.next == null) { | ||||||
| return head; | ||||||
| } | ||||||
| if (head.val != head.next.val) { | ||||||
| head.next = deleteDuplicates(head.next); | ||||||
| return head; | ||||||
| } else { | ||||||
| while (head.next != null && head.val == head.next.val) { | ||||||
| head.next = head.next.next; | ||||||
| } | ||||||
| return deleteDuplicates(head.next); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
| # step3 3回ミスなく書く | ||||||
| - 一番シンプルで分かりやすいと感じた実装を採用 | ||||||
| - 所要時間 平均3分程度 | ||||||
| ## 解答 | ||||||
| ```java | ||||||
| class Solution { | ||||||
| public ListNode deleteDuplicates(ListNode head) { | ||||||
| ListNode dummy = new ListNode(Integer.MIN_VALUE, null); | ||||||
| ListNode tail = dummy; | ||||||
| ListNode node = head; | ||||||
|
|
||||||
| while(node != null) { | ||||||
| if (node.next == null || node.val != node.next.val) { | ||||||
| tail.next = new ListNode(node.val); | ||||||
| tail = tail.next; | ||||||
| } else { | ||||||
| while(node.next != null && node.val == node.next.val) { | ||||||
| node.next = node.next.next; | ||||||
| } | ||||||
| } | ||||||
| node = node.next; | ||||||
|
Comment on lines
+211
to
+219
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 (node.next == null || node.val != node.next.val) {
tail.next = new ListNode(node.val);
tail = tail.next;
node = node.next;
continue;
}
while(node.next != null && node.val == node.next.val) {
node.next = node.next.next;
}
node = node.next;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. 確かに提案していただいたコードの方が、誤解や事故が起こりづらそうです。 |
||||||
| } | ||||||
| return dummy.next; | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
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.
個人的には使い慣れた道具で自然に目的を達成できるコードが書けるのであれば不健全という印象は無いかなと思いましたが、どのあたりが不健全だと感じられましたでしょうか?
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.
HashMapを使った実装だと途中でソートを行う必要があります。
折角入力の順序が保証されているのだから、それを活用しソート不要な実装をするのが自然かつ目指すべきところかなと思いまして。
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.
なるほど、理解できました。ご回答ありがとうございます!