Skip to content

Create RemoveDuplicatesfromSortedListII.md#4

Open
kt-from-j wants to merge 1 commit intomainfrom
RemoveDuplicatesfromSortedListII
Open

Create RemoveDuplicatesfromSortedListII.md#4
kt-from-j wants to merge 1 commit intomainfrom
RemoveDuplicatesfromSortedListII

Conversation

@kt-from-j
Copy link
Owner

今回解いた問題:
82. Remove Duplicates from Sorted List II
次に解く問題:
2. Add Two Numbers

- とりあえず動くものを一旦実装
- でもこれって自分の手慣れた道具(MapとかSetとか)を使うことありきの実装になっていて、不健全
- 計算量
- 時間計算量: O(n)
Copy link

Choose a reason for hiding this comment

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

どれだけの時間以内で計算が終わることを期待していて、それを見積もる手段として時間計算量があるのでそれらの関係について言及していると良いと思いました。

ref: akmhmgc/arai60#7 (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.

Leetcodeの問題って案外実行時間の制限が記載されていないので、そこまで深く考えられていませんでした。
この実装はいくつかのパート(走査、ソートなど)があるのですが、入力が300件でも1パート辺り50msはかからないだろうし、合計で20msもかからないだろう。LeetCodeが期待する最低限の実行時間にも引っかからないだろうという気持ちでした。確かに時間計算量が分かっても実行時間を概算できなければ意味がないので、今後の問題についてはもう少しこの辺を考えてみます!

あとこの問題において改めて実行時間を考えてみると、実はCollections.sort使っているのでO(n log n)でそちらが律速になりそうです。。。

Comment on lines +127 to +128
ListNode dummy = new ListNode(Integer.MIN_VALUE, head);
ListNode tail = dummy;
Copy link

Choose a reason for hiding this comment

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

tailという名前が入力の末尾を意味しているように感じてしまうので

        ListNode dummy_head = new ListNode(Integer.MIN_VALUE, head);
        ListNode dummy_node = dummy_head;

としてdummy_head.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.

実行中は常に応答するListNodeの末尾のノードが代入されているのでtailとしていました。
確かに入力の末尾と混同するかもしれませんね。

Comment on lines +211 to +219
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;
Copy link

Choose a reason for hiding this comment

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

今のノードと次のノードの値が同じかどうか関わらず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;

Choose a reason for hiding this comment

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

確かにnode = node.next;がifでもelseでも実行されることを見落としやすい構造になっているので、2か所に書かれていると間違いが起きにくそうだなと思いました

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

@nanae772 nanae772 left a comment

Choose a reason for hiding this comment

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

お疲れ様です。HashMapからスタックや再帰、ループなどいろいろ解き方で解かれていて勉強になりました。コードも読みやすく分かりやすかったです!

Comment on lines +11 to +13
- もっとシンプルな解法があるはずと思い、挑戦するも挫折
- とりあえず動くものを一旦実装
- でもこれって自分の手慣れた道具(MapとかSetとか)を使うことありきの実装になっていて、不健全

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.

HashMapを使った実装だと途中でソートを行う必要があります。
折角入力の順序が保証されているのだから、それを活用しソート不要な実装をするのが自然かつ目指すべきところかなと思いまして。

Choose a reason for hiding this comment

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

なるほど、理解できました。ご回答ありがとうございます!

if (stack.empty() || stack.peek() != node.val) {
stack.push(node.val);
node = node.next;
} else {

Choose a reason for hiding this comment

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

continueを使うとelse以降のネストを減らせるかなと思いました

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かに。ネスト減らしてあげた方が親切ですね。積極的にcontinueしていきます。

}

// ListNode head = null;
ListNode tail = null;

Choose a reason for hiding this comment

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

これはtailより「重複を除去した新しい連結リストの先頭」であることが分かる名前だと良いかなと思いました

Copy link
Owner Author

Choose a reason for hiding this comment

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

そうですね。最終的にreturn tail;しているのは明らかに不自然ですね。

ListNode newHead = null;

とでもしておくべきでした。

while (node != null && node.next != null) {
int val = node.val;
// 次のノードと異なる場合
if (val != node.next.val) {
Copy link

@nanae772 nanae772 Sep 14, 2025

Choose a reason for hiding this comment

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

ここでは

Suggested change
if (val != node.next.val) {
if (node.val != node.next.val) {

とし、このifを抜けた後に本当に必要になったタイミング(nodeが変わっていくので今のnode.valを変数に固定していないと困る)でvalを定義するのも良いかなと思いました。
好みかもしれません。

Choose a reason for hiding this comment

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

val だとなんの value なのかの情報がないので、val_to_remove などの名前に変えて情報量を増やすと見通しがより良くなるかなと思いました

Copy link
Owner Author

Choose a reason for hiding this comment

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

仰る通りどの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);

Choose a reason for hiding this comment

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

ここでnodeではなく新しいListNodeのインスタンスを作られているのはなぜでしょうか?
新しいリストを構築する非破壊的操作にしたかったのかなと思いましたが、次のelseでは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.

nodeをセットしてしまうとnextにゴミ(省かれるべきnode)が入るためです。

tail.next = node;

にすると
[1,2,2]
の場合に
[1,2]
となってしまいます。

Choose a reason for hiding this comment

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

すみません、単にnodeを繋げてしまうと上手くいかないのでそこは調整する必要がありましたね。失礼しました。

質問の意図としてはこのメソッドを非破壊的操作(元のリストに変更を加えずに新しいリストを作成する)にしたかったのか、破壊的操作(元のリストをin-placeで変更して重複を消去する)にしたかったか、どちらだったでしょうか? ということをお聞きしたかったです 🙇‍♂️

Copy link
Owner Author

Choose a reason for hiding this comment

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

正直に言うとあまり意識していませんでした。
次のelse節でnode.nextを書き換えている通り、元のLinked listを破壊してもいいとは考えていました。
その一方で前述の問題を避けるために新しいインスタンスを作成しております。
振り返ってみると場当たり的ですね。。。

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.

現状の実装はその点の一貫性がなく、場当たり的になってしまっていますね。
この問題上は破壊でも非破壊でもどちらでもいいのですが、なんらかの意図を持って一貫させるべきでした。
通常プログラムを書く上で破壊/非破壊を意識しないことはありえないと思うので、その点意識してみます。
ありがとうございます!

Comment on lines +211 to +219
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;

Choose a reason for hiding this comment

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

確かにnode = node.next;がifでもelseでも実行されることを見落としやすい構造になっているので、2か所に書かれていると間違いが起きにくそうだなと思いました


class Solution {
public ListNode deleteDuplicates(ListNode head) {
Stack<Integer> stack = new Stack<>();
Copy link

Choose a reason for hiding this comment

The 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

このクラスは通常、スタックとして使われるときはStackよりも高速で、キューとして使われるときはLinkedListよりも高速です。

Copy link
Owner Author

Choose a reason for hiding this comment

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

StackのリファレンスにもDequeを優先的に使用するよう記載されてますね。ドキュメントの確認が足りていませんでした。
リンクまで添えていただきありがとうございます。

class Solution {
public ListNode deleteDuplicates(ListNode head) {
// ダミー、セットされている値に意味は無い
ListNode dummy = new ListNode(Integer.MIN_VALUE, head);

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.

一行で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) {

Choose a reason for hiding this comment

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

val だとなんの value なのかの情報がないので、val_to_remove などの名前に変えて情報量を増やすと見通しがより良くなるかなと思いました

- そこを理解するまでに時間がかかった
```java
class Solution {
public ListNode deleteDuplicates(ListNode head) {

Choose a reason for hiding this comment

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

val をセットされているので、処理の流れとしては近しいですが、「実装を見つけたら引き継ぐ」で書いてみるとどうなりますか?具体的には、 while を1重で書くとしたらどうなりますか?
https://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966

Copy link
Owner Author

Choose a reason for hiding this comment

The 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;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants