Conversation
| - とりあえず動くものを一旦実装 | ||
| - でもこれって自分の手慣れた道具(MapとかSetとか)を使うことありきの実装になっていて、不健全 | ||
| - 計算量 | ||
| - 時間計算量: O(n) |
There was a problem hiding this comment.
どれだけの時間以内で計算が終わることを期待していて、それを見積もる手段として時間計算量があるのでそれらの関係について言及していると良いと思いました。
There was a problem hiding this comment.
Leetcodeの問題って案外実行時間の制限が記載されていないので、そこまで深く考えられていませんでした。
この実装はいくつかのパート(走査、ソートなど)があるのですが、入力が300件でも1パート辺り50msはかからないだろうし、合計で20msもかからないだろう。LeetCodeが期待する最低限の実行時間にも引っかからないだろうという気持ちでした。確かに時間計算量が分かっても実行時間を概算できなければ意味がないので、今後の問題についてはもう少しこの辺を考えてみます!
あとこの問題において改めて実行時間を考えてみると、実はCollections.sort使っているのでO(n log n)でそちらが律速になりそうです。。。
| ListNode dummy = new ListNode(Integer.MIN_VALUE, head); | ||
| ListNode tail = dummy; |
There was a problem hiding this comment.
tailという名前が入力の末尾を意味しているように感じてしまうので
ListNode dummy_head = new ListNode(Integer.MIN_VALUE, head);
ListNode dummy_node = dummy_head;としてdummy_head.nextを返すとかが良いと思いました。
There was a problem hiding this comment.
実行中は常に応答するListNodeの末尾のノードが代入されているのでtailとしていました。
確かに入力の末尾と混同するかもしれませんね。
| 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; |
There was a problem hiding this comment.
今のノードと次のノードの値が同じかどうか関わらずnode = node.next;を呼ぶのでこのように書いているだと思いますが、個人的にはnode = node.next;が二箇所に書かれていても早期にcontinue;でループから抜ける方が好みです。
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.
確かにnode = node.next;がifでもelseでも実行されることを見落としやすい構造になっているので、2か所に書かれていると間違いが起きにくそうだなと思いました
There was a problem hiding this comment.
確かに提案していただいたコードの方が、誤解や事故が起こりづらそうです。
ありがとうございます!
nanae772
left a comment
There was a problem hiding this comment.
お疲れ様です。HashMapからスタックや再帰、ループなどいろいろ解き方で解かれていて勉強になりました。コードも読みやすく分かりやすかったです!
| - もっとシンプルな解法があるはずと思い、挑戦するも挫折 | ||
| - とりあえず動くものを一旦実装 | ||
| - でもこれって自分の手慣れた道具(MapとかSetとか)を使うことありきの実装になっていて、不健全 |
There was a problem hiding this comment.
個人的には使い慣れた道具で自然に目的を達成できるコードが書けるのであれば不健全という印象は無いかなと思いましたが、どのあたりが不健全だと感じられましたでしょうか?
There was a problem hiding this comment.
HashMapを使った実装だと途中でソートを行う必要があります。
折角入力の順序が保証されているのだから、それを活用しソート不要な実装をするのが自然かつ目指すべきところかなと思いまして。
| if (stack.empty() || stack.peek() != node.val) { | ||
| stack.push(node.val); | ||
| node = node.next; | ||
| } else { |
There was a problem hiding this comment.
確かに。ネスト減らしてあげた方が親切ですね。積極的にcontinueしていきます。
| } | ||
|
|
||
| // ListNode head = null; | ||
| ListNode tail = null; |
There was a problem hiding this comment.
これはtailより「重複を除去した新しい連結リストの先頭」であることが分かる名前だと良いかなと思いました
There was a problem hiding this comment.
そうですね。最終的にreturn tail;しているのは明らかに不自然ですね。
ListNode newHead = null;
とでもしておくべきでした。
| while (node != null && node.next != null) { | ||
| int val = node.val; | ||
| // 次のノードと異なる場合 | ||
| if (val != node.next.val) { |
There was a problem hiding this comment.
ここでは
| if (val != node.next.val) { | |
| if (node.val != node.next.val) { |
とし、このifを抜けた後に本当に必要になったタイミング(nodeが変わっていくので今のnode.valを変数に固定していないと困る)でvalを定義するのも良いかなと思いました。
好みかもしれません。
There was a problem hiding this comment.
val だとなんの value なのかの情報がないので、val_to_remove などの名前に変えて情報量を増やすと見通しがより良くなるかなと思いました
There was a problem hiding this comment.
仰る通りどのnodeのvalなのかコードを読まないと判断できないですね。
どのような役割のnodeのvalなのかを変数名に含めるべきでした。
ありがとうございます!
| // 外側のループ開始時、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.
ここでnodeではなく新しいListNodeのインスタンスを作られているのはなぜでしょうか?
新しいリストを構築する非破壊的操作にしたかったのかなと思いましたが、次のelseではnode.nextを書き換えられているので破壊的になっていたため少し疑問に思いました
There was a problem hiding this comment.
nodeをセットしてしまうとnextにゴミ(省かれるべきnode)が入るためです。
tail.next = node;
にすると
[1,2,2]
の場合に
[1,2]
となってしまいます。
There was a problem hiding this comment.
すみません、単にnodeを繋げてしまうと上手くいかないのでそこは調整する必要がありましたね。失礼しました。
質問の意図としてはこのメソッドを非破壊的操作(元のリストに変更を加えずに新しいリストを作成する)にしたかったのか、破壊的操作(元のリストをin-placeで変更して重複を消去する)にしたかったか、どちらだったでしょうか? ということをお聞きしたかったです 🙇♂️
There was a problem hiding this comment.
正直に言うとあまり意識していませんでした。
次のelse節でnode.nextを書き換えている通り、元のLinked listを破壊してもいいとは考えていました。
その一方で前述の問題を避けるために新しいインスタンスを作成しております。
振り返ってみると場当たり的ですね。。。
There was a problem hiding this comment.
正直なお答えをいただきありがとうございます 🙇♂️
このライブラリを使用するユーザーの目線で見ると
- 破壊的操作であれば元のインスタンスが使われていることを期待する
- 非破壊的操作であれば元のリストは変更されていないことを期待する
となると思うので、どちらかに統一されているとより良いのかなと思った次第でした!
There was a problem hiding this comment.
現状の実装はその点の一貫性がなく、場当たり的になってしまっていますね。
この問題上は破壊でも非破壊でもどちらでもいいのですが、なんらかの意図を持って一貫させるべきでした。
通常プログラムを書く上で破壊/非破壊を意識しないことはありえないと思うので、その点意識してみます。
ありがとうございます!
| 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; |
There was a problem hiding this comment.
確かにnode = node.next;がifでもelseでも実行されることを見落としやすい構造になっているので、2か所に書かれていると間違いが起きにくそうだなと思いました
|
|
||
| class Solution { | ||
| public ListNode deleteDuplicates(ListNode head) { | ||
| Stack<Integer> stack = new Stack<>(); |
There was a problem hiding this comment.
スタックデータ構造であれば、 Stack より ArrayDeque を優先的に使うことをおすすめします。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayDeque.html
このクラスは通常、スタックとして使われるときはStackよりも高速で、キューとして使われるときはLinkedListよりも高速です。
There was a problem hiding this comment.
StackのリファレンスにもDequeを優先的に使用するよう記載されてますね。ドキュメントの確認が足りていませんでした。
リンクまで添えていただきありがとうございます。
| class Solution { | ||
| public ListNode deleteDuplicates(ListNode head) { | ||
| // ダミー、セットされている値に意味は無い | ||
| ListNode dummy = new ListNode(Integer.MIN_VALUE, head); |
There was a problem hiding this comment.
セットされている値に意味はない、と書くよりは、値自体何も設定しないようにするとより明示的になってコメントも消せるのではと思います。
There was a problem hiding this comment.
一行でnextに値をセットできるかつ、valがnullの場合の例外処理など考える必要が無くなって良いかなと思ったのですが、確かに以下のように2行に分けて書いてしまった方が誤解が少ないかもしれないですね。
ListNode dummy = new ListNode();
dummy.next = head;| while (node != null && node.next != null) { | ||
| int val = node.val; | ||
| // 次のノードと異なる場合 | ||
| if (val != node.next.val) { |
There was a problem hiding this comment.
val だとなんの value なのかの情報がないので、val_to_remove などの名前に変えて情報量を増やすと見通しがより良くなるかなと思いました
| - そこを理解するまでに時間がかかった | ||
| ```java | ||
| class Solution { | ||
| public ListNode deleteDuplicates(ListNode head) { |
There was a problem hiding this comment.
val をセットされているので、処理の流れとしては近しいですが、「実装を見つけたら引き継ぐ」で書いてみるとどうなりますか?具体的には、 while を1重で書くとしたらどうなりますか?
https://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966
There was a problem hiding this comment.
この方針の実装はあまり考えていなかったです。リンク付きで紹介いただきありがとうございます!
個人的には「重複を発見したら帰るな」の方が理解しやすくて好きです。
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;
}
}
今回解いた問題:
82. Remove Duplicates from Sorted List II
次に解く問題:
2. Add Two Numbers