Skip to content
Open
Show file tree
Hide file tree
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,142 @@


## 使用言語
- Go

## STEP1
実装後の後付けになりますが、コンピューターにお願いしたいことを整理しました。
- 空の箱を作る
- 紐でつながっている品物をばらして、箱の中に入れる
- 箱の中にある品物で、重複している品物は一個にする
- 紐でつなぎなおす

「紐でつなぎなおす」部分の実装がパッと思いつきませんでしたが、
過去に二分木の実装で再帰関数を使用しているケースをみかけたことがあったのを思い出したため、
過去の例を見ながら取り入れました。

```Go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
import "slices"

func deleteDuplicates(head *ListNode) *ListNode {
var newList *ListNode
node := head
sortedList := make([]int, 0)

for node != nil {
sortedList = append(sortedList, node.Val)
node = node.Next
}

noDuplicates := slices.Compact(sortedList)

for _, v := range noDuplicates {
newList = insert(newList, v)
}

return newList
}

func insert(l *ListNode, v int) *ListNode {
if l == nil {
return &ListNode{Val: v}
}

l.Next = insert(l.Next, v)

return l
}
```

個人的には、「リストの順序は保証されている」という制約が、この関数と強い依存関係あることに引っかかりを覚えまして、この依存関係を分かるようにしないとバグの温床になりそうだなと思いました。
公式パッケージ「slices」の関数「Compact」も、順番が保証されないと重複を削除しないので、`head`の順番が保証されないのなら動かないコードになってしまうと思いました。
順序が保証されていないのであれば、「slices.Compact」の前に「slices.Sort」で箱の中身をソートかけると思います。

また、再帰関数は読み解くのに認知負荷が高いと思いました。(自分が例を見て理解するのに時間がかかった為)


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

入力を破壊する(in-place)か否か、という観点もあります。

Step 2のコードはin-placeになっています。入力が返ってくる関数で引数にも変更が起きると使用者はびっくりするかもしれません。(必ず悪、という意味ではないです)

## STEP2
他解答を見て、下記がシンプルな方法だと感じました。
自分なりの改良として、下記を行いました。
- `head`はソート済みであることを指す「sorted」に変更
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この関数の正しさは「同じ値が連続して現れる」前提に依存しています(問題で保証される昇順ソートはそのために十分ですが、必要ではないです)。

ただし変数名でその前提を強調する必要はないと思うので、sortedよりnodeなどの方が読み手に余計な期待を持たせません。sorted := headとあると、以降の実装が.Valの昇順な順序特有のロジックを含むように見える気がします。

つまり問題側でソート済みなのは、同値を連続区間として扱いやすくするためで、ここではその連続区間を1回の走査で圧縮しているだけなので、変数名はnodeなどが無難と考えました。

Copy link
Copy Markdown
Owner Author

@Zun-U Zun-U Nov 5, 2025

Choose a reason for hiding this comment

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

レビューいただき、ありがとうございます。
一つの関数に余計な関心事が紛れ込んでる、ということですよね。

関数自体を変えられるのであれば、メソッドにする、あるいは引数の型にソート済みであることの情報を含ませる、などでなるべく関数の外で制限をかける(型で制約をかける)方法を考えていました。

type Sorted *ListNode

// Sorted型に付随する関数
func (s Sorted) DeleteDuplicates(head *ListNode) *ListNode {
...
}

// あるいは、引数の型で制約する
func deleteDuplicates(head Sorted) *ListNode {
...
}

この関数はソート済みのリストが与えられないと上手く機能しませんよ、ということを伝えたかった(制約を課したかった)のですが、
ちょっと考えすぎたのかもしれません。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ありがとうございます。
えっと、もしかしたら自分の意図がまっすぐ伝わっていないかもしれないので念の為補足させてください。十分理解されている場合はスルーしていただければと思います...

一つの関数に余計な関心事が紛れ込んでる、ということですよね。

というより、deleteDuplicatesで入力されるheadの連結リストは必ずしもソート済みでなくてもいいと自分は思っています。

この関数はソート済みのリストが与えられないと上手く機能しませんよ、ということを伝えたかった(制約を課したかった)のですが、

そうでしょうか?必ずしもソート済みでなくても上手く機能したと思える入力は自分的にはあります。2つ例を挙げます。

  1. 例えば入力される連結リストの値が 1 -> 1 -> 3 -> 3 -> 2 だった場合、この関数はどう動くべきだと考えますか。Valが1つ前より小さくなるタイミングで元の入力が昇順ではないことがわかるので、これを異常とらえエラーを送出しますか。例えば自分は1 -> 3 -> 2となるようにduplicatesを削除して返せば十分だと思っています。このような入力は問題では想定されていないものの、自分の気持ちとしては想定内ですし、エラーを出す必要もないと思っています。(この関数が上手く機能する範疇だと思っています。)

  2. さらに、入力が1 -> 3 -> 1 -> 3 -> 3 -> 2 だった場合はどうしましょうか。例えば、1(または3)が不連続に出現することは異常でしょうか?この関数はどこまでの責任を負うべきでしょうか。例えば自分はこの入力に対してもエラーは出さず、1 -> 3 -> 1 -> 3 -> 2と返せば十分だと思います。ただこれだと、上手く機能していると言えるかは微妙かもしれませんね。あるいは1 -> 3 -> 2と返すのも1つの設計とは思います。

そしてStep 2の実装も実際このように動くと思いますが、その上でsortedと命名するのは個人的には変数に対する制約として強すぎるかなと感じた次第です。特に、1つ目は上手く機能すると言える範疇ではないでしょうか。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご返信ありがとうございます!
まったく理解が足りてませんでした。。。
仰る通り、ソートされている必要はなく、重複した値が連続していればこの関数の役割は果たせます。

また、この制約の背景としては、複数人で開発するときを想定して、制約を知らない、あるいはうっかり忘れた人がこの関数を使ったらどうなるか、を考えていました。
それなら関数が動く保証があれば、他の人も安心して使えるかなと思った次第です。
これをコードで表現したいところですが、関数名、引数名が変えられない制約があるので、変数名で表現しましたが、そもそも制約の方向が間違ってましたね...
関数の外でも制約をかけることもできたので、もし制約を課すのなら変数名で表現するのはベストと言い難かったかもしれません。

ただ、個人的には、リストがどうであれ、重複はすべて消去されてほしい(ある時は消去されて、ある時は消去されないみたいに出力がブレてほしくない)ですが、その代わり柔軟性は損なわれますね。
そこはご指摘の通り設計、文脈によると思います。

大変貴重なご意見、ありがとうございます🙇‍♂️
とても勉強になりました!

- `else`の代わりに`continue`を使用
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

elseを使うことで対比的にかけるのはcontinueにない良さと思います。

ケースバイケースですが、本問についてはどっちでもいいかなと感じます。

if sorted.Val == sorted.Next.Valが、例外的であり、先に処理しておく、と考えるならcontinueであり、==の場合と!=の場合の、場合分けをしているという気持ちならelseですね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あとは、以下のようにも書けます。

func deleteDuplicates(head *ListNode) *ListNode {
    sorted := head

    for sorted != nil {
        for sorted.Next != nil && sorted.Val == sorted.Next.Val {
            sorted.Next = sorted.Next.Next
        }
        sorted = sorted.Next
    }

    return head
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

例を挙げていただきありがとうございます。
elsecontinueがない書き方もできるんですね。
目からうろこでした!


```Go
func deleteDuplicates(head *ListNode) *ListNode {
sorted := head

for sorted != nil && sorted.Next != nil {
if sorted.Val == sorted.Next.Val {
sorted.Next = sorted.Next.Next
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

インデントにタブとスペースが混在しているように思います。統一されることをおすすめします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

レビューいただき、ありがとうございます。
Effective Goではインデントにはタブを使用するとありましたので、
こちらに沿うよう修正いたします。

continue
}
sorted = sorted.Next
}

return head
}
```

順序が絶対保証されているのであれば、この方法がシンプルで覚えやすいと思いました。
「sorted.Next」がポインタであることが抜けてましたので、そこに気が付けば、headを返す理由の理解が進みました。


## STEP3
10分以内に3回何も見ないでコードを書けました。
```Go
func deleteDuplicates(head *ListNode) *ListNode {
sorted := head
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

メソッド名のdeleteDuplicatesという文脈からsortedという単語が出てきたときに混乱してしまいました。
変数の使われ方としてはある時点で処理しているノードだと思います。
自分はnode,target_nodeなどにするかなと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

レビューいただき、ありがとうございます。
ご指摘の通り、この関数での処理は与えられたnodeを処理しているだけなので、
制約を中に持ち込むべきではなかったかもしれません。


for sorted != nil && sorted.Next != nil {
if sorted.Val == sorted.Next.Val {
sorted.Next = sorted.Next.Next
continue
}
sorted = sorted.Next
}

return head
}
```


## STEP4
テストを作成するのに時間がかかりました。
特に、ランダムに数を生成しその値をソートしてListNodeの形にするところが、
難航しました。



- 時間計算量:O(n)
- 空間計算量:O(1)

**Constraints**
> - The number of nodes in the list is in the range [0, 300].
> - 100 <= Node.val <= 100
> - The list is guaranteed to be sorted in ascending order.



```
Benchmark

goos: windows
goarch: amd64
pkg: bench
cpu: Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
BenchmarkDeleteDuplicates-8 5955951 181.4 ns/op 0 B/op 0 allocs/op
PASS
ok bench 1.633s
```

- BenchmarkDeleteDuplicates-8
ベンチマーク名 - ベンチマークのGOMAXPROCSの値



21 changes: 21 additions & 0 deletions 83-remove-duplicates-from-sorted-list/bench.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package bench

// Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}

func DeleteDuplicates(head *ListNode) *ListNode {
sorted := head

for sorted != nil && sorted.Next != nil {
if sorted.Val == sorted.Next.Val {
sorted.Next = sorted.Next.Next
continue
}
sorted = sorted.Next
}

return head
}
54 changes: 54 additions & 0 deletions 83-remove-duplicates-from-sorted-list/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package bench

import (
"math/rand"
"slices"
"testing"
"time"
)

var blackhole *ListNode

const (
MAXVALUE = 100
MINVALUE = -100
MAXRANGE = 300
HEADPOSITION = 0
)

func makeList(size int) *ListNode {
var nodeValue int
nodes := make([]*ListNode, size)
list := make([]int, 0)

// シード値の設定
rand.New(rand.NewSource(time.Now().UnixNano()))

for i := 0; i < size; i++ {
// 半開区間[0,201)の乱数(0~200の間) - 100
nodeValue = rand.Intn(MAXVALUE-MINVALUE+1) + MINVALUE
list = append(list, nodeValue)
}

slices.Sort(list)

for i, v := range list {
nodes[i] = &ListNode{Val: v}
if 0 < i {
// 一個前のnodeのNextは、現在のnode
nodes[i-1].Next = nodes[i]
}
}

return nodes[HEADPOSITION]
}

func BenchmarkDeleteDuplicates(b *testing.B) {
nodes := makeList(MAXRANGE)

b.ResetTimer()
for i := 0; i < b.N; i++ {
result := DeleteDuplicates(nodes)
blackhole = result
}
}
3 changes: 3 additions & 0 deletions 83-remove-duplicates-from-sorted-list/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module bench

go 1.24.2