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
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とか)を使うことありきの実装になっていて、不健全
Comment on lines +11 to +13

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.

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

- 計算量
- 時間計算量: 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)でそちらが律速になりそうです。。。

- 空間計算量: 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<>();
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を優先的に使用するよう記載されてますね。ドキュメントの確認が足りていませんでした。
リンクまで添えていただきありがとうございます。

ListNode node = head;

while (node != null) {
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していきます。

int val = stack.pop();
while (node != null && val == node.val) {
node = node.next;
}
}
}

// 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 (!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) {

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

// ダミー、セットされている値に意味は無い
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;

ListNode tail = dummy;
Comment on lines +127 to +128
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としていました。
確かに入力の末尾と混同するかもしれませんね。

ListNode node = head;

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なのかを変数名に含めるべきでした。
ありがとうございます!

// 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);

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.

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

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
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.

確かに提案していただいたコードの方が、誤解や事故が起こりづらそうです。
ありがとうございます!

}
return dummy.next;
}
}
```