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 283_move-zeroes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 283. Move Zeroes

## 1st

### ①

非ゼロの要素を別のリストに避けておいてnumsを上書きする。
最初バブルソートっぽくO(n^2)でやろうとしたらTime Limit Exceededになり、昔この問題をやったときには通った記憶があったのでそこでなぜだと思い時間を使ってしまった (実際にそれで通したのはC++だった)。

所要時間: 14:32

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

```py
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
nonzeros = []
for num in nums:
if num != 0:
nonzeros.append(num)
for i in range(len(nonzeros)):
nums[i] = nonzeros[i]
for i in range(len(nonzeros), len(nums)):
nums[i] = 0
```

### ②

クイックソートのpartitionアルゴリズムに似た解法。コード内のコメントにあるのがループ不変条件で、非ゼロの要素を (順序を保ったまま) すべて左に持ってくるようにしているので解になる。

所要時間: 5:05

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

```py
class Solution:
Copy link

Choose a reason for hiding this comment

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

C++ の std:remove() を思い出しました。

def moveZeroes(self, nums: List[int]) -> None:
last_nonzero_index = 0 # any elements in nums[:last_nonzero_index] are not 0
for i in range(len(nums)):
if nums[i] != 0:
nums[last_nonzero_index], nums[i] = nums[i], nums[last_nonzero_index]
Copy link

Choose a reason for hiding this comment

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

last_nonzero_indexだとそこより右にnonzeroが無いように一瞬思ってしまいましたが、それを防ぐための代案が難しいですね、、、

Copy link
Owner Author

Choose a reason for hiding this comment

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

一応上のコメントでnums[:last_nonzero_index]の各要素が非ゼロだよと補足してみましたが、名前自体変えた方が良さそうですかね?

Copy link

Choose a reason for hiding this comment

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

コメントだとlast_nonzero_indexより左に言及していますが、パッと読んだ時には右にはnonzeroの要素がないように思ってしましました。
nonzero_lengthとかの方がわかりやすいかな、難しい選択ですね

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、コメントだけにして単純にleftとかにして、ループ変数をrightとする方が変に迷わせないのかもと思いました

last_nonzero_index += 1
```

### ③

例外処理の使い方が気持ち悪いが一応こうも書ける。
ループを回してpop()を使っても良い。そちらの場合try-exceptは使わず書ける。その上remove()と違って毎回頭から探索しないから、あえて書くならpop()を使う方がいいな。

所要時間: 1:16

n: len(nums)
- 時間計算量: O(n^2)
- 空間計算量: O(1)

```py
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
num_nonzeros = 0
while True:
try:
nums.remove(0)
num_nonzeros += 1
except ValueError:
break
nums.extend([0] * num_nonzeros)
```

`list.remove()` を確認した。 https://github.com/python/cpython/blob/35d8ac7cd7ed6cd3d84af721dce970da59bd5f68/Objects/listobject.c#L3260

ループでリストの頭から各要素をremoveの引数であるvalueと比較し、等しかったら[list_ass_slice_lock_held()](https://github.com/python/cpython/blob/35d8ac7cd7ed6cd3d84af721dce970da59bd5f68/Objects/listobject.c#L833)を実行してリストからその要素を削除して終了。リスト内にvalueが存在しなければValueErrorを送出する。


## 2nd

### 参考

- https://discord.com/channels/1084280443945353267/1226508154833993788/1250091281246453800
- https://github.com/nittoco/leetcode/pull/18

①の解法は別のリストを用意しなくても良い。こうするとほとんど②と同じになる。

```py
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
last_nonzero_index = 0
for num in nums:
if num == 0:
continue
nums[last_nonzero_index] = num
last_nonzero_index += 1
for i in range(last_nonzero_index, len(nums)):
nums[i] = 0
```

- https://discord.com/channels/1084280443945353267/1239148130679783424/1249699208773107733
- https://github.com/goto-untrapped/Arai60/pull/25

last_nonzero_indexとそこよりも右にある初めての非ゼロの要素を交換し続ける。考えてることは②と大体同じだが手順が違う。ごちゃついてるし②の方がいいだろう (書き方が悪いだけかもしれないが)。

```py
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
next_nonzero_index = -1
last_nonzero_index = 0 # any elements in nums[:last_nonzero_index] are not 0
while True:
while last_nonzero_index < len(nums) and nums[last_nonzero_index] != 0:
last_nonzero_index += 1
next_nonzero_index = max(next_nonzero_index, last_nonzero_index)
while next_nonzero_index < len(nums) and nums[next_nonzero_index] == 0:
next_nonzero_index += 1
Comment on lines +114 to +115
Copy link

Choose a reason for hiding this comment

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

L111~112と!= と==のみが違う操作なので、関数化してもいいかもですね

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

Choose a reason for hiding this comment

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

def find_leftmost_index_by_condition(nums, condition, index=0):
    while index < len(nums):
        if condition(nums[index]):
            return index
        index += 1
    return index

find_leftmost_index_by_condition(nums, last_nonzero_index, lambda x: x != 0)

みたいな感じですかね(思ったよりややこしかった)

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:
    def moveZeroes(self, nums: List[int]) -> None:
        def find_leftmost_index_by(condition: Callable[[int], bool], start: int):
            i = start
            while i < len(nums) and not condition(nums[i]):
                i += 1
            return i

        next_nonzero_index = -1
        last_nonzero_index = 0 # any elements in nums[:last_nonzero_index] are not 0
        while True:
            last_nonzero_index = find_leftmost_index_by(lambda x: x == 0, last_nonzero_index)
            next_nonzero_index = find_leftmost_index_by(lambda x: x != 0, max(next_nonzero_index, last_nonzero_index))
            if next_nonzero_index == len(nums):
                break
            nums[last_nonzero_index], nums[next_nonzero_index] = nums[next_nonzero_index], nums[last_nonzero_index]
            last_nonzero_index += 1

自分なりに解釈してみました!PRのコードより良くなった気がします、ありがとうございます 🙏

Copy link

@nittoco nittoco Aug 24, 2024

Choose a reason for hiding this comment

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

おお、自分のやつより良くなってますね、ありがとうございます(後少し違っていた)

Copy link

Choose a reason for hiding this comment

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

Generator を使って変なコードを書いてみました。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        eq0 = (i for i, x in enumerate(nums) if x == 0)
        neq0 = (i for i, x in enumerate(nums) if x != 0)
        try:
            while 1:
                pos0 = next(eq0)
                while (posn0 := next(neq0)) < pos0:
                    pass
                nums[pos0], nums[posn0] = nums[posn0], nums[pos0]
        except:
            pass

Copy link

@nittoco nittoco Aug 24, 2024

Choose a reason for hiding this comment

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

try: 
    # なんかのエラーがある処理
except:
    pass

でエラーを消せるのなるほどでした

Copy link

Choose a reason for hiding this comment

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

このコードは遊びで書いているので、あまり参考にしないでください。

except ですべてを握りつぶすと、SIGINT つまり、キーボード入力の ^C も捕まえます。
https://discord.com/channels/1084280443945353267/1347375192065703986/1353569957581164615

StopIteration を捕まえるのが正しいでしょうね。

if next_nonzero_index == len(nums):
break
nums[last_nonzero_index], nums[next_nonzero_index] = nums[next_nonzero_index], nums[last_nonzero_index]
last_nonzero_index += 1
```

- https://discord.com/channels/1084280443945353267/1196472827457589338/1248287760255684722
- https://github.com/Mike0121/LeetCode/pull/24
- https://discord.com/channels/1084280443945353267/1225849404037009609/1246132840152699021
- https://github.com/SuperHotDogCat/coding-interview/pull/25
- https://discord.com/channels/1084280443945353267/1201211204547383386/1230566847058018344
- https://github.com/shining-ai/leetcode/pull/54
- https://discord.com/channels/1084280443945353267/1200089668901937312/1221011776188059668
- https://github.com/hayashi-ay/leetcode/pull/58
- https://discord.com/channels/1084280443945353267/1210494002277908491/1211045822012072006


## 3rd

```py
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
last_nonzero_index = 0 # any elements in nums[:last_nonzero_index] are not 0
for i in range(len(nums)):
if nums[i] == 0:
continue
nums[last_nonzero_index], nums[i] = nums[i], nums[last_nonzero_index]

Choose a reason for hiding this comment

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

もし時間にめちゃくちゃシビアな場合は、Step2の①の別解のスワップしない実装の方が速そうですね。(そんなシビアな事をpythonでするなという感じもありますが)
ただ、この方法の方が意味がわかりやすいのと、仮に仕様変更で0ではなくて0以下を端に寄せたいとなった場合にもcontinueする条件をnums[i] <= 0とするだけでワークするので、良いと思います。

この辺、3rdで最終的にこの方法を選んだ理由や考えが明示されていると良いかもしれないですね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

3rdで最終的にこの方法を選んだ理由や考えが明示されていると良いかもしれないですね。

なるほど、これから書いてみます。ありがとうございます
ちなみに大体の場合は挙げた解法の中で自分なりに一番素直かなと思う方法で3rdを書いており、今回もそうです。動作が想像しやすいからか保守性も高くなることが多いかなと思い。

仮に仕様変更で0ではなくて0以下を端に寄せたいとなった場合にもcontinueする条件をnums[i] <= 0とするだけでワークする

こうは考えていかなったですが、たしかにそうですね

Copy link

Choose a reason for hiding this comment

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

まとめて 0 fill は、loop unrolling できたりするのでちょっと嬉しいこともあるでしょう。

last_nonzero_index += 1
```