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

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

/*
問題の理解
- 文字列sと行数を表す整数num_rowsが与えられる。指定された行数に渡ってジグザグに並べ替えた文字列を返す。
s="PAYPALISHIRING" num_rows=3 out="PAHNAPLSIIGYIR"

何を考えて解いていたか
- 紙に書いて規則性を見るのが良さそう。
- 入出力例は以下のようになっている。
- num_rows=3のとき、に0列目に3文字・1列目に1文字・2列目に3文字・3列目に1文字...となっている。
- num_rows=4のとき、0列目に4文字・1列目に1文字・2列目に1文字・3列目に4文字・4列目に1文字・5列目に1文字・6列目に残りの文字...となっている。
- i + num_rows - 1 列目の列でnum_rows文字出力している。この列以外は1文字だけ出力している
ここで手が止まったので答えを見る。

何がわからなかったか
- 規則性のようなものを見つけたが、手作業でやる方法まではたどり着けなかった。

解法の理解
https://www.youtube.com/watch?v=Q2Tw6gcVEwc&t=1s
- NeetCodeの解説動画を見たがマジックナンバーだらけで分かりづらいと感じる。
https://neetcode.io/solutions/zigzag-conversion
- このページに掲載されている「2. Iteration - II」の方が分かりやすそう
- LeetCode問題文でZigzagになった文字列の並びが示されているとおりに、0行目0列目からスタートしていくイメージだと理解した。
- 0行目〜(num_rows - 1)行目まで進みながらs[i]の文字を配列にpushしていくと、i行目に対応する文字を配列に格納できる。
- (num_rows - 1)に到達したら、逆順(num_rows - 1)行目~0行目に進みながら、i行目に対応する文字を配列に格納できる。
- directionで行の進む方向を制御している。
- 最終的に["abc","def","ghi"]のような配列が得られるので、concatで1つの文字列にまとめている。

所感
- 文字列の並びの規則性を見つけることにばかり気を取られて、手作業で問題文に示されている通りZigzagに文字をなぞっていく選択肢を思いつかなかった。
問題を解く時にいきなりきれいなアルゴリズムを考えるのではなくて、シンプルに手作業で解くということを実践したいなと思った。
感覚としてスマートな解法を考えることにばかり気を取られていて、問題に対して素直に考えられていないという感じがする。
*/

pub struct Solution {}
impl Solution {
pub fn convert(s: String, num_rows: i32) -> String {
let num_rows: usize = match num_rows.try_into() {
Ok(v) => v,
Err(_) => panic!("num_rows must be positive value. num_rows: {}", num_rows),
};

if num_rows == 1 || s.len() <= num_rows {
return s;
}

let mut rows = vec![String::new(); num_rows];
let mut row = 0usize;
let mut direction = 1;

for c in s.chars() {
rows[row].push(c);
row = ((row as i32) + direction) as usize;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

row を s のインデックスから計算式で求める方法もあります。興味があれば探してみてください。
ただ、ぱっと見で理解しやすい計算式ではないため、もし書いたとしても、コメントで内容を説明したほうが良いと思います。

if row == 0 || row == num_rows - 1 {
direction *= -1;
}
}

rows.concat()
}
}

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

#[test]
#[should_panic]
fn step1_num_rows_negative_value_test() {
Solution::convert("PAYPALISHIRING".to_string(), -3);
}

#[test]
fn step1_test() {
assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 3),
"PAHNAPLSIIGYIR"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 4),
"PINALSIGYAHRPI"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 1),
"PAYPALISHIRING"
);

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

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

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

/*
他の人のコードを読んで考えたこと
https://github.com/olsen-blue/Arai60/pull/61/changes#diff-ccfa5b3e70552f7a1aea1f6a719cd00d74a1035c89cfab6426fddc4696ed3e21R56
- 行を進める方向(direction)を1・-1で管理するのかboolによって管理するのかで結構別れている様子。boolとdirectionをenumで定義する方向で実装してみるのも良さそうだと思った。
書いていて思ったが、direction自体は関数の外側に露出しないのでenumは過剰な気がしてきた。boolによるフラグ管理で十分そう。

https://github.com/saagchicken/coding_practice/pull/22/changes#r2009508424
> この問題、出題意図は、お手玉できるか、な気もします。
- 同じようなことを思った。何をしようとしているかを理解して、これを素直にプログラムに落とし込めるかという感じ。

https://github.com/naoto-iwase/leetcode/pull/61#discussion_r2529679629
> row_index など、行番号のニュアンスがあっても良いかなと思いました。row の場合、内容を読む前だと rows との対応関係がありそうにも見えます。
- 確かにrowsに対してrowだと row = rows[i]のようにも見えるなと思った。

https://leetcode.com/problems/zigzag-conversion/solutions/333761/rust-0ms-4ms-by-obliquemotion-ceg6/
LeetCode Solutionのトップにあった解法。読みづらいので練習としては良さそう。

改善する時に考えたこと
- for-loopの中で row = ((row as i32) + direction) as usize; の部分がキャストでごちゃごちゃしているのをなんとかしたい。
- rowをi32として扱えば少し良くなりそう。

所感
- step1.rsと比べて少し行数は増えたものの読み手の認知負荷が下がったように感じる。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

おっしゃるとおりで、step1に比べて読みやすかったです。

*/

pub struct Solution {}
impl Solution {
pub fn convert(s: String, num_rows: i32) -> String {
let num_rows: usize = match num_rows.try_into() {
Ok(v) => v,
Err(_) => panic!("num_rows must be positive value: num_rows: {}", num_rows),
};

if num_rows == 0 {
return "".to_string();
}
if num_rows == 1 || s.chars().count() <= num_rows {
return s;
}

let mut rows = vec![String::new(); num_rows];
let mut i = 0;
let mut is_down_direction = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

英語としてこなれた命名としては is_downward, is_descending, is_going_down あたりが思いつきました。現状でも意図は通じるので問題ありません。


for c in s.chars() {
rows[i as usize].push(c);

if is_down_direction {
i += 1;
} else {
i -= 1;
}

if i == 0 || i == num_rows - 1 {
is_down_direction = !is_down_direction;
}
}

rows.concat()
}
}

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

#[test]
#[should_panic]
fn step2_num_rows_negative_value_test() {
Solution::convert("PAYPALISHIRING".to_string(), -3);
}

#[test]
fn step2_test() {
assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 3),
"PAHNAPLSIIGYIR"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 4),
"PINALSIGYAHRPI"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 1),
"PAYPALISHIRING"
);

assert_eq!(Solution::convert("P".to_string(), 3), "P");
}
}
94 changes: 94 additions & 0 deletions src/bin/step2a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Step2a
// 目的: 別の解法の写経と理解

/*
https://leetcode.com/problems/zigzag-conversion/solutions/333761/rust-0ms-4ms-by-obliquemotion-ceg6/
LeetCode Solutionのトップにあった解法。読みづらいので練習としては良さそう。

解法の理解
入力例: s="PAYPALISHIRING" num_rows=3 out="PAHNAPLSIIGYIR"
- (0..num_rows).chain((1..num_rows - 1).rev())
- [0,1,2,1]
- cycleで[0,1,2,1]最後尾の要素まで到達すると先頭の要素に戻るイテレータを作っている
- zipでcycleのイテレータと文字のタプルのセットを生成している
zip(s.chars()); (row_index,c) -> (0,P)
zip(s.chars()); (row_index,c) -> (1,A)
zip(s.chars()); (row_index,c) -> (2,Y)
zip(s.chars()); (row_index,c) -> (1,P)
zip(s.chars()); (row_index,c) -> (0,A)
zip(s.chars()); (row_index,c) -> (1,L)
zip(s.chars()); (row_index,c) -> (2,I)
zip(s.chars()); (row_index,c) -> (1,S)
zip(s.chars()); (row_index,c) -> (0,H)
zip(s.chars()); (row_index,c) -> (1,I)
zip(s.chars()); (row_index,c) -> (2,R)
zip(s.chars()); (row_index,c) -> (1,I)
zip(s.chars()); (row_index,c) -> (0,N)
zip(s.chars()); (row_index,c) -> (1,G)
- zigzags.sort_by_keyの行位置のみでソートしている
zigzags.sort_by_key(); (row_index,c) -> (0,P)
zigzags.sort_by_key(); (row_index,c) -> (0,A)
zigzags.sort_by_key(); (row_index,c) -> (0,H)
zigzags.sort_by_key(); (row_index,c) -> (0,N)
zigzags.sort_by_key(); (row_index,c) -> (1,A)
zigzags.sort_by_key(); (row_index,c) -> (1,P)
zigzags.sort_by_key(); (row_index,c) -> (1,L)
zigzags.sort_by_key(); (row_index,c) -> (1,S)
zigzags.sort_by_key(); (row_index,c) -> (1,I)
zigzags.sort_by_key(); (row_index,c) -> (1,I)
zigzags.sort_by_key(); (row_index,c) -> (1,G)
zigzags.sort_by_key(); (row_index,c) -> (2,Y)
zigzags.sort_by_key(); (row_index,c) -> (2,I)
zigzags.sort_by_key(); (row_index,c) -> (2,R)
- ソートする時にタプルで指定すると、同じ行でアルファベットで並び替えされておかしな結果になるので、sort_by_keyによって明示的に行位置のみを対象にしてソートしている

所感
- cycle(),zip()メソッドの動き方を理解するのに時間がかかった。
関数型の考え方?に慣れていないせいか、cycle()で生成したイテレータをzip()メソッドで文字列の各文字とタプルにしている部分が難しく感じた。inspect()を利用して何が行われているかを理解できた。
https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.inspect

- https://github.com/olsen-blue/Arai60/pull/61#discussion_r2040670667
Pythonだが、近い発想で書かれているコードだなと思った。
*/

pub struct Solution {}
impl Solution {
pub fn convert(s: String, num_rows: i32) -> String {
let mut zigzags = (0..num_rows)
.chain((1..num_rows - 1).rev())
.cycle()
.zip(s.chars())
// 複数のイテレータを連結しているコードのデバッグ手法の例として意図的にinspect()のコードを残しています
.inspect(|(row_index, c)| {
println!("zip(s.chars()); (row_index,c) -> ({},{})", row_index, c)
})
.collect::<Vec<_>>();
Comment on lines +57 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

メソッドチェーンでまとめて書けると美しい一方で、Rustに不慣れな私としてはインデックス作成を分けていただけると読みやすかったです。

Suggested change
let mut zigzags = (0..num_rows)
.chain((1..num_rows - 1).rev())
.cycle()
.zip(s.chars())
// 複数のイテレータを連結しているコードのデバッグ手法の例として意図的にinspect()のコードを残しています
.inspect(|(row_index, c)| {
println!("zip(s.chars()); (row_index,c) -> ({},{})", row_index, c)
})
.collect::<Vec<_>>();
// Zig-zag order indices. e.g. [0,1,2,1,0,1, ...] for num_rows = 3
let row_indices = (0..num_rows)
.chain((1..num_rows - 1).rev())
.cycle();
let mut zigzags: Vec<(i32, char)> = row_indices
.zip(s.chars())
.collect();

zigzags.sort_by_key(|(row_index, _)| *row_index);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://doc.rust-lang.org/std/vec/struct.Vec.html#method.sort_by_key

This sort is stable
を利用していますね。

この解法きれいですね。

zigzags.into_iter().map(|(_, c)| c).collect()
}
}

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

#[test]
fn step2a_test() {
assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 3),
"PAHNAPLSIIGYIR"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 4),
"PINALSIGYAHRPI"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 1),
"PAYPALISHIRING"
);

assert_eq!(Solution::convert("P".to_string(), 3), "P");
}
}
92 changes: 92 additions & 0 deletions src/bin/step3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Step3
// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する

// 方法
// 時間を測りながらもう一度解く
// 10分以内に一度もエラーを吐かず正解
// これを3回連続でできたら終わり
// レビューを受ける
// 作れないデータ構造があった場合は別途自作すること

/*
n = s.chars().count()
時間計算量: O(n)
空間計算量: O(n)
*/

/*
1回目: 5分18秒
2回目: 3分15秒
3回目: 2分54秒
*/

/*
所感
- 行の移動方向制御はフラグ管理の実装にした。少し冗長になるものの、iが単調増加・減少することが一目で分かるので読みやすいという感覚。
*/

pub struct Solution {}
impl Solution {
pub fn convert(s: String, num_rows: i32) -> String {
let num_rows: usize = num_rows
.try_into()
.expect("num_rows must be positive value");

if num_rows == 0 {
return "".to_string();
}
Comment on lines +35 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本筋ではありませんが、ユーザーがうっかり0を渡して(失敗せずに)空文字が返ってくると困るケースがありそうです。誤ったパラメータを渡していることを気づかせてあげるほうが親切に思いました。

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.

確かにnum_rowsは 1 <= num_rows <= 1000 となっているので、誤ったパラメーター0を渡してきていることを知らせるという観点は良いなと思いました。
Leet Codeの採点システムを通らなくなってしまいますが、型で表すことも可能なのでそもそも符号付き32ビット整数i32ではなく、非ゼロの32ビット整数NonZeroU32などを活用できるなと思いました。

pub fn convert(s: String, num_rows: NonZeroU32) -> String

https://doc.rust-lang.org/std/num/type.NonZeroU32.html

if num_rows == 1 || s.chars().count() <= num_rows {
return s;
}

let mut rows = vec![String::new(); num_rows];
let mut is_down_direction = true;
let mut i = 0;
for c in s.chars() {
rows[i].push(c);

if is_down_direction {
i += 1;
} else {
i -= 1;
}

if i == 0 || i == num_rows - 1 {
is_down_direction = !is_down_direction;
}
}

rows.concat()
}
}

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

#[test]
#[should_panic]
fn step3_num_rows_negative_value_test() {
Solution::convert("PAYPALISHIRING".to_string(), -3);
}

#[test]
fn step3_test() {
assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 3),
"PAHNAPLSIIGYIR"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 4),
"PINALSIGYAHRPI"
);

assert_eq!(
Solution::convert("PAYPALISHIRING".to_string(), 1),
"PAYPALISHIRING"
);

assert_eq!(Solution::convert("P".to_string(), 3), "P");
}
}