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
144 changes: 144 additions & 0 deletions 283MoveZeroes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
問題: https://leetcode.com/problems/move-zeroes/description/

### Step 1
- 前から舐めるとインデックスのずれを考慮する必要があって面倒なので、後ろから回した
- in-place でという制約を見落としていて、参照透過性を考慮してコピーを作成していたが、不要だった

```Go
func moveZeroes(nums []int) {
for i := len(nums) - 1; i >= 0; i-- {
if nums[i] == 0 {
nums = slices.Delete(nums, i, i+1)
nums = append(nums, 0)
}
}
}
```

### Step 2
#### 2a
- step1 の修正
- slices.Delete の内部実装を見たところ、例外処理などのオーバーヘッドがあり、今回は不要だと思ったので、使わなかった
- よく考えたら append(nums[:i], nums[i+1:]...) は O(n-i+1)=O(n) 時間かかっているので、全体で O(n^2) になってしまっていた
Copy link

Choose a reason for hiding this comment

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

Linked List ならばこの操作は得意ですね。
移し替えてみたらどうでしょうか。
https://pkg.go.dev/container/list

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
Owner Author

Choose a reason for hiding this comment

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

書いてみました。

func moveZeroes(nums []int)  {
    numsList := list.New()
    nonzeroCount := 0
    for _, n := range nums {
        if n != 0 {
            numsList.PushBack(n)
            nonzeroCount++
        }
    }
    numsIndex := 0
    for n := numsList.Front(); n != nil; n = n.Next() {
        nums[numsIndex] = n.Value.(int)
        numsIndex++
    }
    clear(nums[nonzeroCount:])
}

Copy link

Choose a reason for hiding this comment

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

あー、いや、それでは別に移す先が []int でも同じですね。一回全部 list に移した後に、頭から n 要素を調べて、0 だったら MoveToBack をするというコードを書けば、step1 と同じアルゴリズムで O(n) でできるという話です。

Copy link
Owner Author

@hroc135 hroc135 Apr 22, 2025

Choose a reason for hiding this comment

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

あ、こういうことですね。

func moveZeroes(nums []int)  {
    numsList := list.New()
    for _, n := range nums {
        numsList.PushBack(n)
    }
    node := numsList.Front()
    for _ = range nums {
        if node.Value.(int) == 0 {
            nextNode := node.Next()
            numsList.MoveToBack(node)
            node = nextNode
        } else {
            node = node.Next()
        }
    }
    node = numsList.Front()
    for i := range nums {
        nums[i] = node.Value.(int)
        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.

はい、そういうことです。


```Go
func moveZeroes(nums []int) {
for i := len(nums) - 1; i >= 0; i-- {
if nums[i] == 0 {
nums = append(nums[:i], nums[i+1:]...)
nums = append(nums, 0)
}
}
}
```

#### 2b
- https://github.com/fhiyo/leetcode/pull/54/files#diff-2f8b85074aa38861aa9dd6fbe0c5f1b540a06f8618d7552b4ffd05da21f795d3R17
- 時間計算量: O(2n) = O(n)
- 空間計算量: O(n)

```Go
func moveZeroes(nums []int) {
nonzeroes := []int{}
for _, n := range nums {
if n != 0 {
nonzeroes = append(nonzeroes, n)
}
}
copy(nums, nonzeroes)
for i := len(nonzeroes); i < len(nums); i++ {
nums[i] = 0
}
}
```

#### 2c
- 2b を in-place に
- https://github.com/fhiyo/leetcode/pull/54/files#diff-2f8b85074aa38861aa9dd6fbe0c5f1b540a06f8618d7552b4ffd05da21f795d3R88
- 時間計算量: O(2n) = O(n)
- 空間計算量: O(1)
- https://github.com/fhiyo/leetcode/pull/54/files#r1729230640
- コンパイラでループ展開(loop unrolling)してくれる場合もある
- ここまで頭が回っていなかった
- https://discord.com/channels/1084280443945353267/1201211204547383386/1230568276690468917
- たしかにこっちのほうがしっくりくる
- erase-remove idiom というのがあるらしい
- C++ の remove と大体同じ実装
- https://cplusplus.com/reference/algorithm/remove/

```Go
func moveZeroes(nums []int) {
nonzeroCount := 0
for _, n := range nums {
if n != 0 {
nums[nonzeroCount] = n
nonzeroCount++
}
}
for i := nonzeroCount; i < len(nums); i++ {
nums[i] = 0
}
}
```

#### 2d
- https://github.com/fhiyo/leetcode/pull/54/files#diff-2f8b85074aa38861aa9dd6fbe0c5f1b540a06f8618d7552b4ffd05da21f795d3R136
- https://github.com/rihib/leetcode/pull/50/files#diff-6dd7d1270e29a07842912b50d2236807871f48d1b90d26c0bc9ad4d0ecd8f05dR18
- 0 でないものを前に持ってくることで、0 がバブルソートぽく後ろに押し出される方法
- > 仮に仕様変更で0ではなくて0以下を端に寄せたいとなった場合にもcontinueする条件をnums[i] <= 0とするだけでワークするので、良いと思います。
- https://github.com/fhiyo/leetcode/pull/54/files#r1720791348
- この視点はなかった。コードの保守性を考慮するというのはこういうことなのか。奥が深い、、
Copy link

@Yoshiki-Iwasa Yoshiki-Iwasa Apr 27, 2025

Choose a reason for hiding this comment

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

個人的には、達成したい目的に依存しない複雑性の排除のほうが保守性には大切かなーと思いました

「もしかしたら役立つかも」という想定は大抵実現しないので、目的を達成するために選択できる手段の中から最もシンプルなものを選ぶほうが保守性は高くなると思います

Choose a reason for hiding this comment

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

もちろん、「シンプル」は実装の容易さや人間の可読性などさまざまな要素があるのでどれが一番シンプルかは単純には決まらないと思いますが。。。

- Go は `nums[0], nums[0] = nums[0], nums[0]` でも動くが、気持ち悪いので、`i == nonzeroCount` の場合は弾いた
- Goの場合は特別に弾かなくてもよいが、C++ だと未定義動作になるらしい
Copy link

@Yoshiki-Iwasa Yoshiki-Iwasa Apr 27, 2025

Choose a reason for hiding this comment

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

チームの状況次第(C++に慣れている人が多くて驚きがあるなど)だと思いますが、Goで許されているなら気にせずnums[0], nums[0] = nums[0], nums[0]を受け入れてもいい気がします
意味的には別に変なことしてない気がします


```Go
func moveZeroes(nums []int) {
nonzeroCount := 0
for i, n := range nums {
if n == 0 {
continue
}
if i == nonzeroCount {
nonzeroCount++
continue
}
nums[nonzeroCount], nums[i] = nums[i], nums[nonzeroCount]
nonzeroCount++
}
}
```

### Step 3
- 2c が一番素直な理解だと思うし、ループ展開のメリットもありそう
- slices.Delete の内部実装を調べていたらビルトインの clear を見つけ、調べたら最後の 0 埋めに使えそうだった
- ただし、要件が変わったときに潰しが効かなさそうではある

```Go
func moveZeroes(nums []int) {
nonzeroCount := 0
for _, n := range nums {
if n != 0 {
nums[nonzeroCount] = n
nonzeroCount++
}
}
clear(nums[nonzeroCount:])
}
```

- slices.Delete

```Go
func Delete[S ~[]E, E any](s S, i, j int) S {
_ = s[i:j:len(s)] // 0 ≤ i ≤ j ≤ len(s) を満たしていなければ run time panic する
if i == j {
return s
}
oldLen := len(s)
s = append(s[:i], s[j:]...)
clear(s[len(s):oldLen])
return s
}
```

### CS