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
232 changes: 232 additions & 0 deletions Python3/8. String to Integer (atoi).md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
## Step 1. Initial Solution

- 色々考えることが多いので順番に分けて処理する
- いくつか勘違いをしていて時間を取られた
- 1 << 31 - 1とすると 1 << 30になる
- if ‘ ‘: とするとTrue扱いになる
- 整数判定(str.isdigit())
- - 1 // 10 は 0ではなく-1を返す
- もっときれいな書き方はあるだろうが取りあえずの実装
- 特段難しいことはない処理でも多段階になるとどうしても作業効率が落ちてしまう
- どこかに全体設計を書いてから実装した方が良いのだろうか

```python
POSITIVE = 1
NEGATIVE = 0
MAX_INT = (1 << 31) - 1
MIN_INT = -(1 << 31)
NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}

class Solution:
def myAtoi(self, s: str) -> int:
sign = POSITIVE
index = 0
num = 0
while index < len(s):
if s[index] == ' ':
index += 1
continue
if s[index] == '-':
sign = NEGATIVE
index += 1
break
if s[index] == '+':
index += 1
break
if s[index] in NUMBERS:
break
return 0

while index < len(s):
if s[index] == '0':
index += 1
continue
if s[index] in NUMBERS:
break
if s[index]:
return 0

while index < len(s):
if s[index] in NUMBERS:
if sign == POSITIVE and (MAX_INT - int(s[index])) // 10 - num < 0:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Python では int は多倍長整数で実装されているため、 32/64-bit のオーバーフローしません。そのため、計算してから範囲を超えたかどうかを判定しても大丈夫です。そのほうがコードがシンプルになると思います。

num = num * 10 + int(s[index])
if sign == POSITIVE and MAX_INT < num:
    return MAX_INT
if sign == NEGATIVE and -num < MIN_INT:
    return MIN_INT
index += 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.

もちろん承知の上ですが、その処理をして欲しい問題なのかなと思って書きました
(MAX_INTの定義で越えてるという話はありそうですが)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

他の(オーバーフローが)問題になるような言語だと、標準ライブラリーにそういう関数があることが多いというのも意識してもいいかもしれません。
まあ、要は自分で作らないべきものであるということです。

return MAX_INT
if sign == NEGATIVE and (MIN_INT + int(s[index])) // 10 + 1 + num > 0:
return MIN_INT
num = num * 10 + int(s[index])
index += 1
else:
break
return num if sign == POSITIVE else -num
```

### Complexity Analysis

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

## Step 2. Alternatives

- まずは自分でリファクタリング
- 少しは見やすくなったがやはり複雑に見える

```python
MAX_INT = (1 << 31) - 1
MIN_INT = -(1 << 31)

class Solution:
def myAtoi(self, s: str) -> int:
index = 0
while index < len(s):
if s[index] == ' ':
index += 1
continue
break

is_positive = True
if index == len(s):
return 0
if s[index] == '+':
index += 1
elif s[index] == '-':
index += 1
is_positive = False

num = 0
while index < len(s) and s[index].isdigit():
if is_positive and num > (MAX_INT - int(s[index])) // 10:
return MAX_INT
if not is_positive and -num < (MIN_INT + int(s[index])) // 10 + 1:
return MIN_INT
num = num * 10 + int(s[index])
index += 1
return num if is_positive else -num
```

- https://github.com/tokuhirat/LeetCode/pull/59/files#diff-ed1cf3d46cfd4bddc1808bc5157b0cc7596de0a5858c3d1da82c0d1b1006874cR13-R29
- 細かい処理をメソッドとして定義している
- 多少読みやすいが全体として長くは見える
- https://github.com/tokuhirat/LeetCode/pull/59/files#diff-ed1cf3d46cfd4bddc1808bc5157b0cc7596de0a5858c3d1da82c0d1b1006874cR79
- 一桁のintはcase文くらいしか思いついていなかったが確かにordでも良い
- str.isdigit()についてもちゃんと理解していなかった
- `Digits include decimal characters and digits that need special handling, such as the compatibility superscript digits`
- https://github.com/tokuhirat/LeetCode/pull/59/files#diff-ed1cf3d46cfd4bddc1808bc5157b0cc7596de0a5858c3d1da82c0d1b1006874cR92
- シフト演算子の優先順位が低いことはドキュメントにもあるらしい、盲点だった
- https://github.com/naoto-iwase/leetcode/pull/60/files#diff-663ed9c661571932db8d4d60eac1099f01e58022328775ad01b28bab535523e8R335
- 最後の計算部分はvalueの方が良い名前だと感じた
- 先にsignを入れながら計算するのもこの形なら綺麗
- https://github.com/naoto-iwase/leetcode/pull/60/files#diff-663ed9c661571932db8d4d60eac1099f01e58022328775ad01b28bab535523e8R278
- このように正規表現を綺麗に書くのが一番簡潔
- ただ、コメントにもある通り認知負荷は上がるので読む時間が短くなるかは微妙
- 一か所を見て全体のルールを把握できるのは利点
- `_PATTERN = re.compile(r"\s*([+-]?)(\d+)")`
- \s*: スペース0個以上
- ([+-]?): +-のいずれか任意でグループ化
- (\d+): 0-9が一つ以上
- たまに書かないと忘れるので良い復習になった
- 少し間が空いてから再度書いたらまたかなり時間がかかった
- 関数化せずにそのまま書いてしまった方が読みやすい気はする

```python
MAX_INT = (1 << 31) - 1
MIN_INT = -(1 << 31)
class Solution:
def myAtoi(self, s: str) -> int:
def skip_whitespace() -> None:
if s[self.index] == ' ':
self.index += 1
return
self.checked_whitespace = True

def get_sign() -> None:
if s[self.index] == '+':
self.sign = 1
self.index += 1
elif s[self.index] == '-':
self.sign = -1
self.index += 1
self.checked_sign = True

def skip_zero() -> None:
if s[self.index] == '0':
self.index += 1
elif s[self.index] in string.digits:
self.checked_zero = True
else:
self.index = len(s)

def round_int() -> None:
if s[self.index] not in string.digits:
self.index = len(s)
return
if self.num > (MAX_INT - ch_to_i(s[self.index])) // 10:
self.num = MAX_INT
self.index = len(s)
return
elif self.num < (MIN_INT + ch_to_i(s[self.index])) // 10 + 1:
self.num = MIN_INT
self.index = len(s)
return
self.num = self.num * 10 + self.sign * ch_to_i(s[self.index])
self.index += 1

def ch_to_i(ch: str) -> int:
return ord(ch) - ord('0')

self.index = 0
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

Choose a reason for hiding this comment

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

ステートマシン風に書きたかったのかなと思いました。以下のような書き方もあります。

import enum


class State(enum.Enum):
    WHITESPACE = 1
    SIGN = 2
    ZERO = 3
    INT = 4
    END = 5


MAX_INT = (1 << 31) - 1
MIN_INT = -(1 << 31)


class Solution:
    def myAtoi(self, s: str) -> int:
        def parse_whitespace(index: int) -> tuple[State, int]:
            if s[index] == ' ':
                return State.WHITESPACE, index + 1
            return State.SIGN, index
        
        def parse_sign(index: int) -> tuple[State, int, int]:
            if s[index] == '+':
                return State.ZERO, index + 1, 1
            if s[index] == '-':
                return State.ZERO, index + 1, -1
            return State.ZERO, index, 1
        
        def parse_zero(index) -> tuple[State, int]:
            if s[index] == '0':
                return State.ZERO, index + 1
            if s[index] in string.digits:
                return State.INT, index
            return State.END, len(s)
        
        def parse_int(index, sign, num) -> tuple[State, int, int]:
            if s[index] not in string.digits:
                return State.END, len(s), num
            if num > (MAX_INT - ch_to_i(s[index])) // 10:
                return State.END, len(s), MAX_INT
            if num < (MIN_INT + ch_to_i(s[index])) // 10 + 1:
                return State.END, len(s), MIN_INT
            return State.INT, index + 1, num * 10 + sign * ch_to_i(s[index])

        def ch_to_i(ch: str) -> int:
            return ord(ch) - ord('0')

        index = 0
        state = State.WHITESPACE
        sign = 1
        num = 0
        while index < len(s):
            match state:
                case State.WHITESPACE:
                    state, index = parse_whitespace(index)
                case State.SIGN:
                    state, index, sign = parse_sign(index)
                case State.ZERO:
                    state, index = parse_zero(index)
                case State.INT:
                    state, index, num = parse_int(index, sign, num)
                case State.END:
                    break
        return num

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.

確かにこういう書き方もありですね。
参考になります

self.checked_whitespace = False
self.checked_sign = False
self.checked_zeros = False
self.sign = 1
self.num = 0
while self.index < len(s):
if not self.checked_whitespace:
skip_whitespace()
continue
if not self.checked_sign:
get_sign()
continue
if not self.checked_zero:
skip_zero()
continue
round_int()
return self.num
```


## Step 3. Final Solution

- 分かりやすさを失わない範囲でなるべくコンパクトに書くように意識

```python
class Solution:
def myAtoi(self, s: str) -> int:
index = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if s is None:
        return 0

Noneの時の処理を入れたくなりました

while index < len(s):
if s[index] != ' ':
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

while index < len(s) and s[index] == ' ':としてもいいと思いました。

index += 1
if index >= len(s):
return 0

sign = 1
if s[index] == '+':
index += 1
elif s[index] == '-':
index += 1
sign = -1

MAX_INT = (1 << 31) - 1
MIN_INT = -(1 << 31)
num = 0
while index < len(s):
if s[index] not in string.digits:
return num
next_digit = sign * (ord(s[index]) - ord('0'))
if num > (MAX_INT - next_digit) // 10:
return MAX_INT
if num < (MIN_INT - next_digit) // 10 + 1:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if num <= (MIN_INT - next_digit) // 10:

でも動きますかね?

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.

そうですね。ただ、自分の意図としては上と同じように超えた時に丸めるという意味合いが重要と思っているのでこうしてます

return MIN_INT
num = num * 10 + next_digit
index += 1
return num
```