From 2463a92818622443fb74bf9e12746f87a22ff556 Mon Sep 17 00:00:00 2001 From: t9a Date: Sun, 15 Mar 2026 13:29:17 +0900 Subject: [PATCH] solve: 8.String to Integer (atoi) --- src/bin/step1.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++ src/bin/step2.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++ src/bin/step2a.rs | 104 ++++++++++++++++++++++++++++++++++++++ src/bin/step3.rs | 106 ++++++++++++++++++++++++++++++++++++++ src/bin/step4.rs | 78 ++++++++++++++++++++++++++++ 5 files changed, 537 insertions(+) create mode 100644 src/bin/step1.rs create mode 100644 src/bin/step2.rs create mode 100644 src/bin/step2a.rs create mode 100644 src/bin/step3.rs create mode 100644 src/bin/step4.rs diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..f00da0e --- /dev/null +++ b/src/bin/step1.rs @@ -0,0 +1,126 @@ +// Step1 +// 目的: 方法を思いつく + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 文字列sが与えられる。文字列sを符号付き32ビット整数に変換して返す。 + 変換アルゴリズムの内容は以下 + - 先頭の空白文字は無視する + - 符号の判定「-」「+」を行う。符号なしの場合は正の値として扱う + - 先頭の0はスキップしながら、非数字文字又は文字列末尾に到達するまで正数を読み取る。 + - 文字列から数字文字を読み取れなかったとき、0を返す。 + - 整数が符号付き32ビット整数を超える時は丸め処理を行って返す。 + + 何を考えて解いていたか + - 問題の制約に空間計算量の制約はなく、入力の文字列長は最大200文字なので入力の文字列をVecDequeに変換して扱う。 + - 文字列先頭から見ていく + - 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::>(); + 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::().parse::() { + Ok(num) => { + if is_minus { + return -num; + } + num + } + Err(_) => { + if is_minus { + return i32::MIN; + } + i32::MAX + } + } + } + + fn collect_num_chars(chars: &mut VecDeque, num_chars: &mut Vec) { + 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 + ); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..df30949 --- /dev/null +++ b/src/bin/step2.rs @@ -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::()でオーバーフローするのかどうか、先頭についている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::>(); + 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::(); + if digits.is_empty() { + return 0; + } + + match digits.parse::() { + 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::().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 + ); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..a2df43d --- /dev/null +++ b/src/bin/step2a.rs @@ -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 + この条件分岐でなぜオーバーフローを判定できているのかわからないので整理する。 + 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 { + 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 + ); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..83b4947 --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,106 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n = s.len + 時間計算量: O(n) + 空間計算量: O(1) +*/ + +/* + 1回目: 6分28秒 overflowチェックの条件分岐をミスしてWrong Answerとなった + 2回目: 4分39秒 Accepted + 3回目: 5分19秒 Accepted + 4回目: 5分06秒 Accepted +*/ + +/* + 所感 + - s.chars()が何度も出現しているのが気になるものの特に代替案が思い浮かばないので、GPT-5.3に聞いてみる。 + - let mut iter = s.chars().peekable()が使えそうなことが分かったのでstep4.rsで書いてみる。 + - is_negativeはsignにして num * signとすると条件分岐を減らせるという指摘もあった。 + - as_bytes()のコードを提案された。確かに入力の制約上はマルチバイト文字は考慮しなくてよいが、文字列を文字に分解するという目的ではchars()の方が適切な気がするのでこのままとする。 +*/ + +pub struct Solution {} +impl Solution { + pub fn my_atoi(s: String) -> i32 { + let mut index = 0; + + // skip white space + for c in s.chars() { + if !c.is_ascii_whitespace() { + break; + } + index += 1; + } + if index == s.chars().count() { + return 0; + } + + // parse 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 = 0; + 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 { + 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 step3_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 + ); + } +} diff --git a/src/bin/step4.rs b/src/bin/step4.rs new file mode 100644 index 0000000..6e1976d --- /dev/null +++ b/src/bin/step4.rs @@ -0,0 +1,78 @@ +// Step4 +// 目的: より良い書き方を試す + +/* + n = s.len + 時間計算量: O(n) + 空間計算量: O(1) +*/ + +/* + 改善点 + - step3.rsでs.chars()が何度も出てくるのでまとめる。(let mut s_iter = s.chars().peekable()) + - is_negative は sign = -1 or 1で持っておくと値を返す時の条件分岐が不要になる。(num * sign) + + 所感 + - peekableなイテレータは便利だなと思った。次の要素を確認だけしたい(peek)ときにイテレータを進めずに見れるので。 +*/ + +pub struct Solution {} +impl Solution { + pub fn my_atoi(s: String) -> i32 { + // skip white space + let mut s_iter = s.trim_start().chars().peekable(); + + // set sign + let mut sign = 1; + match s_iter.peek() { + Some('-') => { + sign = -1; + s_iter.next(); + } + Some('+') => { + s_iter.next(); + } + _ => (), + } + + // to i32 + let mut num = 0i32; + for c in s_iter { + 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 { + if sign == -1 { + return i32::MIN; + } + return i32::MAX; + } + + num *= 10; + num += digit; + } + + num * sign + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step4_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 + ); + } +}