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
126 changes: 126 additions & 0 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Step1
// 目的: 方法を思いつく

// 方法
// 5分考えてわからなかったら答えをみる
// 答えを見て理解したと思ったら全部消して答えを隠して書く
// 5分筆が止まったらもう一回みて全部消す
// 正解したら終わり

/*
問題の理解
- 文字列sが与えられる。文字列sを符号付き32ビット整数に変換して返す。
変換アルゴリズムの内容は以下
- 先頭の空白文字は無視する
- 符号の判定「-」「+」を行う。符号なしの場合は正の値として扱う
- 先頭の0はスキップしながら、非数字文字又は文字列末尾に到達するまで正数を読み取る。
- 文字列から数字文字を読み取れなかったとき、0を返す。
- 整数が符号付き32ビット整数を超える時は丸め処理を行って返す。

何を考えて解いていたか
- 問題の制約に空間計算量の制約はなく、入力の文字列長は最大200文字なので入力の文字列をVecDeque<char>に変換して扱う。
- 文字列先頭から見ていく
- if 空白 or or - or + or 0-9 then 整数パース処理に入る
else return 0
- 整数バース処理
- 文字が数字文字列の場合そのままかえす。数字文字でなければbreak
- 戻り値のチェックを行いi32::MAX,i32::MINの境界を超えるようならそれぞれ丸めて返す
- 文字列から生成した数値はi64で扱う必要がある

n = s.len
時間計算量: O(n)
空間計算量: O(n)
この内容で実装する。
s=" -042"でWrong Answerとなった。先頭の空白が連続した時に全て取り除いて良いことに気づかなかった。入力の先頭空白全てをtrimすることで対応。
s="20000000000000000000"でWrong Answerとなった。文字列に数字以外が含まれていることに着目してパースするだけだと思っていたが、そのまま数値型にパースするとオーバーフローするような数字文字列を正しく扱えていないことに気付いた。
そのまま数字文字列をi64型にパースするとオーバーフローするので、パースが失敗した時にそのままi32::MAXまたはi32::MINでreturnすれば良い。
この修正でAcceptedとなった。

何がわからなかったか
- 先頭の空白はスキップするというルールで空白1文字だと思いこんでいた。普通に考えて空白1文字より先頭の空白は全て無視する方が自然なので、問題を解くことに囚われすぎていて視野が狭くなっていたなと思った。
- 入力の文字列長が200ということで、時間計算量O(200)なら問題ないと考えていた。この問題では数字文字列から符号付き32ビット整数に変換する必要があるので、ここでオーバーフローする可能性がある文字列が入力として与えられる可能性を見落としていた。

正解してから気づいたこと
- 問題を見た時に自力で解けるか解けないかギリギリそうだなという感覚があった。余裕がなく視野が狭くなってエッジケースでWrong Answerとなるようなコードを提出する結果になったなと思った。
- match chars.front()で符号を判定するためだけに重複するコードが発生しているのが気になる。
- parseで文字列先頭にある0は無視してくれるのでmatchで'0'を見る必要がない。
*/

use std::{collections::VecDeque, i32};

pub struct Solution {}
impl Solution {
pub fn my_atoi(s: String) -> i32 {
if s.is_empty() {
return 0;
};

let mut chars = s.trim_start().chars().collect::<VecDeque<_>>();
let mut is_minus = false;
let mut num_chars = Vec::new();

match chars.front() {
Some('-') => {
is_minus = true;
let _ = chars.pop_front();
Self::collect_num_chars(&mut chars, &mut num_chars);
}
Some('+' | '0') => {
let _ = chars.pop_front();
Self::collect_num_chars(&mut chars, &mut num_chars);
}
Some('1'..='9') => Self::collect_num_chars(&mut chars, &mut num_chars),
_ => return 0,
}

if num_chars.is_empty() {
return 0;
}

match num_chars.iter().collect::<String>().parse::<i32>() {
Ok(num) => {
if is_minus {
return -num;
}
num
}
Err(_) => {
if is_minus {
return i32::MIN;
}
i32::MAX
}
}
}

fn collect_num_chars(chars: &mut VecDeque<char>, num_chars: &mut Vec<char>) {
let Some(c) = chars.pop_front() else {
return;
};

if c.is_ascii_digit() {
num_chars.push(c);
Self::collect_num_chars(chars, num_chars);
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step1_test() {
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(Solution::my_atoi("1337c0d3".to_string()), 1337);
assert_eq!(Solution::my_atoi("0-1".to_string()), 0);
assert_eq!(Solution::my_atoi("words and 987".to_string()), 0);

assert_eq!(Solution::my_atoi("".to_string()), 0);
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(
Solution::my_atoi("20000000000000000000".to_string()),
i32::MAX
);
}
}
123 changes: 123 additions & 0 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Step2
// 目的: 自然な書き方を考えて整理する

// 方法
// Step1のコードを読みやすくしてみる
// 他の人のコードを2つは読んでみること
// 正解したら終わり

// 以下をメモに残すこと
// 講師陣はどのようなコメントを残すだろうか?
// 他の人のコードを読んで考えたこと
// 改善する時に考えたこと

/*
他の人のコードを読んで考えたこと
https://github.com/Ryotaro25/leetcode_first60/pull/64#discussion_r2014577471
> 漢字などでも True になるのは Python の isdigit の話ですね。
- isdigitでTrueになるのは罠すぎると思ったが、言語によっては数値文字であるという当たり前の前提が異なるから、日本語話者の自分の直感と違うことは当たり前でもあるかと思った。
Stack Overflowで関連する質問があった。深追いはしないが、文字周りの判定はややこしいので入力文字種に制限が無いような環境では慎重になるべきポイントだと理解した。
https://stackoverflow.com/questions/44891070/whats-the-difference-between-str-isdigit-isnumeric-and-isdecimal-in-pyth

https://github.com/shining-ai/leetcode/pull/59#discussion_r1577212861
- 符号付き32ビットから桁溢れするかどうかを計算して求めている。
自分はparseできるかどうかで判定していたのでコーディング練習の観点では、オーバーフローするかを桁数に思いを馳せながら考えるのも良いなと思った。

https://github.com/hayashi-ay/leetcode/pull/69#discussion_r1548091225
> long はデータモデルによってサイズが異なります。
> https://ja.wikipedia.org/wiki/64%E3%83%93%E3%83%83%E3%83%88
> LP64 LLP64 等でお調べください。
- プログラミング言語のデータ型・ビットサイズという文脈でデータモデルという言葉を始めて聞いた。
https://www.ibm.com/docs/ja/zos/3.2.0?topic=dbile-ilp32-lp64-data-models-data-type-sizes
Rustを書いている中ではあまり気にする場面が思いつかなかったのでGPT-5.3に聞いたところ、Rustの外側との境界で問題が表面化することが分かった。
- 例としてC言語で書かれた既存ライブラリを活用(FFI)するときに、C言語側でlongと書かれているシグネチャの部分をRust側でi64と決め打ちすると問題が発生する場合がある。
- LLP64データモデルにおいてlong型は長さが32bitだが、Rust側では符号付き64bitとして扱っているため。
歴史的な経緯までは追いきれなかったが、64bitアーキテクチャにおいてWindowsカーネルはLLP64を採用していることが分かった。
- Windowsカーネル(LLP64)ではlong型は32bitになる。
- Linuxカーネル(LP64)ではlong型は64bitになる。
https://learn.microsoft.com/ja-jp/windows/win32/winprog64/abstract-data-models
> ほとんどのアプリケーションではサイズを増やす必要がないため、すべてのデータ型を 64 ビット長にすると、領域が無駄になります。 ただし、アプリケーションには 64 ビット データへのポインターが必要であり、選択したケースでは 64 ビットのデータ型を持つ機能が必要です。 これらの考慮事項により、LLP64 (または P64) と呼ばれる抽象データ モデルが選択されました。 LLP64 データ モデルでは、ポインターのみが 64 ビットに拡張されます。他のすべての基本データ型 (整数と long) は、長さが 32 ビットのままです。

https://github.com/mamo3gr/arai60/pull/54/changes#r2882808316
> 仮に自分がこの問題を面接で出題するとしたら、 int() の実装をするようお願いすると思います。おそらくここがこの問題のポイントの一つなのではないかと思います。
- step1.rsの自分のコードにも当てはまる指摘だと思った。i32に収まるかどうかの判定を自分で実装した方が良さそう。step2a.rsでやる。

改善する時に考えたこと
- match chars.front()の+,-マッチ条件で実行しているコードが重複しているのを改善する
- 関数冒頭の空文字チェックは必要なさそう
- is_minusよりはis_negativeの方が英語として自然そう

所感
- parse::<i32>()でオーバーフローするのかどうか、先頭についている0の処理などを丸投げしている罪悪感(コーディング練習になっていない)があるのでstep2a.rsでこのあたりを実装する。
- VecDequeに詰め込まずにin-placeでも実装できそうなのでやってみる
*/

use std::collections::VecDeque;

pub struct Solution {}
impl Solution {
pub fn my_atoi(s: String) -> i32 {
let mut chars = s.trim_start().chars().collect::<VecDeque<_>>();
let mut is_negative = false;

match chars.front() {
Some('-') => {
is_negative = true;
chars.pop_front();
}
Some('+') => {
chars.pop_front();
}
Some('0'..='9') => (),
_ => return 0,
}

let digits = chars
.iter()
.map_while(|c| c.is_ascii_digit().then_some(c))
.collect::<String>();
if digits.is_empty() {
return 0;
}

match digits.parse::<i32>() {
Ok(num) => {
if is_negative {
return -num;
}
num
}
Err(_) => {
if is_negative {
return i32::MIN;
}
i32::MAX
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn play_ground() {
assert_eq!("00042".parse::<i32>().unwrap(), 42);
}

#[test]
fn step2_test() {
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(Solution::my_atoi("1337c0d3".to_string()), 1337);
assert_eq!(Solution::my_atoi("0-1".to_string()), 0);
assert_eq!(Solution::my_atoi("words and 987".to_string()), 0);

assert_eq!(Solution::my_atoi("".to_string()), 0);
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(
Solution::my_atoi("20000000000000000000".to_string()),
i32::MAX
);
}
}
104 changes: 104 additions & 0 deletions src/bin/step2a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Step2a
// 目的: 別の解法を練習する

/*
参考にした解法
https://github.com/hayashi-ay/leetcode/pull/69/changes#diff-f2b395d63173ac2f2d3c547e8f9e8e07c9acfbeb6b5da935bb28d83d8bcc7a04R145
https://github.com/Yoshiki-Iwasa/Arai60/pull/64/changes#diff-dbf5e75b50aa4257946026d6539860be248a58856e1b457b4d37445059b05857R21
if num > i32::MAX / 10 || num == i32::MAX / 10 && digit > i32::MAX % 10
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここは自分で解いた時あまり深く理解できずに飛ばしてしまったところだったので、勉強になりました!

この条件分岐でなぜオーバーフローを判定できているのかわからないので整理する。
i32::MAX = 2 147 483 647
i32::MAX / 10 = 214 748 364
i32::MAX % 10 = 7
- num > i32::MAX / 10
- for-loopの最後で num *= 10, num += digitしている
- numが i32::MAX / 10 = 214 748 364 を超えるようであれば、オーバーフローすることが確定している
- num = 214 748 365 の場合を考えると、桁を上げた時点(2 147 483 650)でオーバーフローとなる
- num == i32::MAX / 10 && digit > i32::MAX % 10
- num == i32::MAX / 10
- 一桁分は余裕があるもののi32::MAXの10進数の最下位桁(1の位)は7なのでdigitが7を超えるとオーバーフローする
- digit > i32::MAX % 10
- i32::MAX % 10 = 7となる
- digitが7を超えるようだとオーバーフローするので、丸める必要がある

所感
- オーバーフローの判定部分で何をしているのか最初分からなかったが、落ち着いて分解してみるとどのような気持ちで条件分岐が書かれているのか理解できたので良かった。
*/

pub struct Solution {}
impl Solution {
pub fn my_atoi(s: String) -> i32 {
let mut index = 0usize;
let chars = s.chars();

// skip white space
for c in chars {
if c.is_ascii_whitespace() {
index += 1;
continue;
}
break;
}
if index == s.chars().count() {
return 0;
}

// determine sign
let mut is_negative = false;
match s.chars().nth(index) {
Some('-') => {
is_negative = true;
index += 1;
}
Some('+') => index += 1,
_ => (),
}
if index == s.chars().count() {
return 0;
}

// to i32
let mut num = 0i32;
for i in index..s.chars().count() {
let c = s.chars().nth(i).unwrap();
let Some(digit) = c.to_digit(10).and_then(|v| Some(v as i32)) else {
break;
};

if num > i32::MAX / 10 || num == i32::MAX / 10 && digit > i32::MAX % 10 {
Copy link
Copy Markdown

@huyfififi huyfififi Mar 15, 2026

Choose a reason for hiding this comment

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

代替案が提示できるわけではないのですが、positive に対しては単純に overflow をチェックしているように読める一方で、negative 側は-2147483648 を扱う関係で同じ意味では読めず、少し意図が分かりにくいように感じました。
ここの条件をoverflowチェックだと読み飛ばしてしまってから最後まで進んだ際、 positiveの場合 i32::MAXが最後まで許容されている一方で、negativeの場合 i32::MIN が early return される形になっており、挙動が対称でないので少し混乱しやすいように感じました。
(私の読解に問題があるような気もしますが😓)

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 is_negative {
return i32::MIN;
}
return i32::MAX;
}

num *= 10;
num += digit;
}

if is_negative {
return -num;
}
num
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step2a_test() {
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(Solution::my_atoi("1337c0d3".to_string()), 1337);
assert_eq!(Solution::my_atoi("0-1".to_string()), 0);
assert_eq!(Solution::my_atoi("words and 987".to_string()), 0);

assert_eq!(Solution::my_atoi("".to_string()), 0);
assert_eq!(Solution::my_atoi(" -042".to_string()), -42);
assert_eq!(
Solution::my_atoi("20000000000000000000".to_string()),
i32::MAX
);
}
}
Loading