From 2b13a76995a8011af6edeaff9f0a2090a425f061 Mon Sep 17 00:00:00 2001 From: t9a Date: Fri, 17 Oct 2025 13:53:12 +0900 Subject: [PATCH 1/2] solve: 141.Linked List Cycle --- src/bin/step1.rs | 127 ++++++++++++++++++++-- src/bin/step2.rs | 109 +++++++++++++++++-- src/bin/step2_1_floyds_cycle_detection.rs | 117 ++++++++++++++++++++ src/bin/step3.rs | 101 +++++++++++++++-- 4 files changed, 430 insertions(+), 24 deletions(-) create mode 100644 src/bin/step2_1_floyds_cycle_detection.rs diff --git a/src/bin/step1.rs b/src/bin/step1.rs index d640da2..7aaac06 100644 --- a/src/bin/step1.rs +++ b/src/bin/step1.rs @@ -8,26 +8,139 @@ // 正解したら終わり /* + 問題の理解 + 141.Linked-List-Cycle + - リンクリストの先頭が与えられるので循環しているかを判定する。 + リンクリスト先頭からnextを見ていってnextが存在しなければ、falseを返す。 + リンクリストのnextに入っているアドレスを取得しておいて、nextで辿っていったとき同じアドレスを発見したら一周してきたことがわかるのでfalse。 + 何がわからなかったか - - + - Rustにおける循環参照するような型の定義や扱い。 何を考えて解いていたか - - + - この問題はLeetCodeでRust実装の回答を判定できないのでどうやってテストするか。 + ChatGPTを活用してテストコードを作成して、テストコードがパスするかで判断する。 + + - テストを行うにはリンクリストで循環しているものとそうでないものを自分で生成する必要がある + とりあえず実装方法が思いつかないのでChatGPTに作ってもらって、答えを理解したと思ったら答えを隠して自分で実装してみる。 - 想定ユースケース - - + - headが渡された時点ではどこまでノードがつながっているがわからないので、全走査(nextを辿る)する必要がある。 + - nextで次への参照を見つけるたびにアドレスをハッシュセットに記録していき、これまでに出現したアドレスを見つけたら循環していると判定する。 + BoxはTを格納したヒープ領域へのスマートポインタを返すとあるので、ここからアドレスを取得して比較すればよいと思ったが、ある時点で取得したアドレス位置に + ずっと同じデータが格納されていることが保証できないこととの兼ね合いで所有権を要求するので、安易にアドレスでの比較ができないようになっている模様。 + なのでBoxをclone()するようにコンパイラはヒントを出してくるが、Boxをclone()すると同じ値で別のヒープ領域にデータを配置するような動きになり、 + すでに見つけたアドレスなのかの判定ができなくなる。 + - Rc(reference count)でラップするとclone()しても値の複製は行われず、clone()元と同じ参照が得られる。内部的には参照カウントをインクリメントして、 + 参照カウントが0(誰も値を見ていなくなった)になったら値をdropしているそう。(他の場所で参照されているのにdropしないように) + プログラム実行中にどこかで参照されている間は同じメモリ領域を指し続けるので今回利用できる。 + - Box,Rc,Cell,RefCellについて実装過程で調べることになったので、今まで曖昧なままにしていた部分が大分クリアになって良かった。 正解してから気づいたこと - - + - 変数名についてtraced_nodeよりはvisited_nodeほうがより直感的だと思った。 + - RwLockよりもRefCellの方が適切。ListNodeのnextを更新するために内部可変性が必要だが、RwLockはマルチスレッドで利用するものなので、シングルスレッドで内部可変性を得るのにはRefCellの方が適切である。 + CellはTがCopyトレイトを実装していることを要求するのでTがプリミティブ型である場合かつ、Tを丸ごと入れ替えるような場合に利用する。 + RefCellは参照を返すので今回のようにTがCopyトレイトを実装しておらず構造体を丸ごと複製する必要がない場合に利用する。(構造体のフィールドのみを更新など) + https://doc.rust-jp.rs/book-ja/ch15-05-interior-mutability.html#rct%E3%81%A8refcellt%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B%E3%81%93%E3%81%A8%E3%81%A7%E5%8F%AF%E5%A4%89%E3%81%AA%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AB%E8%A4%87%E6%95%B0%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%82%92%E6%8C%81%E3%81%9F%E3%81%9B%E3%82%8B + + - Boxは必要ない。 今回の実装過程で初めて知ったが、Rcもヒープ領域にデータを確保するため。ListNodeが自分自身の型をフィールドに持つようなコンパイル時にサイズが確定しない型であったので、Boxでラップしたがアドレスを比較するために + HashSetにポインタ保存しておくという用途ではポインタの所有者がHashSetとListNodeと複数になるのでRcの方が適切だった。 + Boxではポインタの所有権が単独になるので、HashSetにポインタを保持しておくことができない。 + https://doc.rust-jp.rs/book-ja/ch15-04-rc.html + + - Rcに対しては.clone()ではなく、Rc::clone()という書き方が推奨されている。 + どちらも動き方は同じだがRcは値の複製を行わず参照カウンタのインクリメントのみの軽量な操作なので、Rc::clone()の方が他の型との違いが分かりやすいため。 + https://doc.rust-jp.rs/book-ja/ch15-04-rc.html#:~:text=%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82-,Rc,%E3%81%99 + + - Rustの標準ライブラリstd::collections::LinkedListがあるが、これは双方向リンクリストと呼ばれるもので今回の題材となっている単方向リンクリストとは別物 + https://doc.rust-lang.org/std/collections/struct.LinkedList.html# + + - テストコードで利用しているbuild_list_with_cycleについて、インデックスアクセスの方法を添字からgetによる安全な方法にする。 + - テストケースでtail_nodeが自身にリンクするような自己参照パターンも生成できるように修正する。 */ +use std::{collections::HashSet, rc::Rc, sync::RwLock}; + +pub struct ListNode { + next: Option>>>, +} + pub struct Solution {} -impl Solution {} +impl Solution { + pub fn has_cycle(head: Option>>>) -> bool { + let mut traced_node: HashSet<_> = HashSet::new(); + let mut next = head; + + while let Some(node) = next { + if !traced_node.insert(Rc::as_ptr(&node)) { + return true; + } + + next = node.read().unwrap().next.clone(); + } + + false + } +} #[cfg(test)] mod tests { use super::*; + fn build_list_with_cycle( + cycle_position: Option, + list_len: usize, + ) -> Option>>> { + if list_len == 0 { + panic!("invalid list_len require 1"); + } + + let nodes = (0..list_len) + .map(|_| Rc::new(RwLock::new(Box::new(ListNode { next: None })))) + .collect::>(); + let tail_position = list_len - 1; + + // nodeのnextに接続する + for i in 0..tail_position { + nodes[i].write().unwrap().next = Some(nodes[i + 1].clone()) + } + + // tail_nodeのnextにcycle_posで指定されたnodeを接続 + if let Some(tail_next_position) = cycle_position { + if tail_next_position < tail_position { + nodes[tail_position].write().unwrap().next = + Some(nodes[tail_next_position].clone()); + } + } + + Some(nodes[0].clone()) + } + + #[test] + fn step1_no_cycle_test() { + let expect = false; + let no_cycle = build_list_with_cycle(None, 4); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(None, 1); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(4), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(2), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(2), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + } + #[test] - fn step1_test() {} + fn step1_with_cycle_test() { + let expect = true; + let with_cycle = build_list_with_cycle(Some(1), 4); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(1), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + } } diff --git a/src/bin/step2.rs b/src/bin/step2.rs index e92520d..12ea9e2 100644 --- a/src/bin/step2.rs +++ b/src/bin/step2.rs @@ -12,26 +12,115 @@ // 改善する時に考えたこと /* - 講師陣はどのようなコメントを残すだろうか? - - - 他の人のコードを読んで考えたこと - - - - 他の想定ユースケース - - + - 「フロイドの循環検出法」というアルゴリズムでこの問題を解けそうだということが分かった。 + フロイドの循環検出法では2人の内1人が一歩先にいる状態から走査をスタートする。 + サイクルごとに2人の現在位置が同じでないかを確認していき同じであれば循環しているので走査を終了。 + サイクルごとにスタート位置が手前の方は一歩ずつ進む。一歩先にいる方は2歩進んでいく。 + 一歩先の人が次のノードを見つけられなければ循環していないので終了。 + この方法では走査しながら確認できる(ワンパス)ので追加の補助空間計算量がかからず、空間計算量がO(1)となる。 + step2_1_floyds_cycle_detection.rsで一応実装してみる。 + https://leetcode.com/problems/linked-list-cycle/solutions/4831939/brute-optimal-both-approach-full-explained-java-c-python3-rust-go-javascript/ + - いくつかコードを読んだが上記のアルゴリズムか、自分の採用したHashSetを用いた解法くらいしか見当たらなかった。 + - 問題を解くこと自体よりもLeetCodeがRustに対応していなかったので単方向リンクリスト自体を実装する必要が出てきたのが良い経験になった。 + 単方向リンクリストの循環検出方法自体はすぐに思いついた。単方向リンクリスト自体の実装に苦戦したがRustの理解が深まった。 改善する時に考えたこと - - + - 変数名について単にnodeとしていた箇所をcurrent_nodeにすることで、曖昧さを軽減した。 + - よく理解せずに混乱しながら利用していたRwLock,Boxの利用をやめた。 + - Rcのデータに対しては.clone()ではなくRc::clone()で明示的にリファレンスカウンタをインクリメントするようにした。 + 内部的には同じだが、公式ドキュメントで明示的に使い分けることが推奨されていたため。 + https://doc.rust-jp.rs/book-ja/ch15-04-rc.html#:~:text=%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82-,Rc,%E3%81%99 + + - テストコードで利用しているbuild_list_with_cycleについて、インデックスアクセスの方法を添字からgetによる安全な(エラーハンドリング可能な)方法にする。 */ +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +pub struct ListNode { + pub next: Option>>, +} + pub struct Solution {} -impl Solution {} +impl Solution { + pub fn has_cycle(head: Option>>) -> bool { + let mut visited_node: HashSet<_> = HashSet::new(); + let mut next_node = head; + + while let Some(current_node) = next_node { + if !visited_node.insert(current_node.as_ptr()) { + return true; + } + + next_node = current_node.borrow().next.as_ref().map(Rc::clone); + } + + false + } +} #[cfg(test)] mod tests { use super::*; + fn build_list_with_cycle( + cycle_position: Option, + list_len: usize, + ) -> Option>> { + if list_len == 0 { + panic!("invalid list_len require 1"); + } + + let nodes = (0..list_len) + .map(|_| Rc::new(RefCell::new(ListNode { next: None }))) + .collect::>(); + let tail_position = list_len - 1; + + // nodeのnextに接続する + for (i, node) in nodes.iter().enumerate() { + if let Some(next_node) = nodes.get(i + 1) { + node.borrow_mut().next = Some(Rc::clone(next_node)); + } + } + + // tail_nodeのnextにcycle_positionで指定されたnodeを接続 + if let Some(cycle_position) = cycle_position { + if let (Some(tail_node), Some(cycle_to_node)) = + (nodes.get(tail_position), nodes.get(cycle_position)) + { + tail_node.borrow_mut().next = Some(Rc::clone(&cycle_to_node)); + } + } + + Some(Rc::clone(&nodes[0])) + } + #[test] - fn step2_test() {} + fn step2_no_cycle_test() { + let expect = false; + let no_cycle = build_list_with_cycle(None, 4); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(None, 1); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(4), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(2), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + } + + #[test] + fn step1_with_cycle_test() { + let expect = true; + let with_cycle = build_list_with_cycle(Some(1), 4); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(1), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(2), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + } } diff --git a/src/bin/step2_1_floyds_cycle_detection.rs b/src/bin/step2_1_floyds_cycle_detection.rs new file mode 100644 index 0000000..88df6ea --- /dev/null +++ b/src/bin/step2_1_floyds_cycle_detection.rs @@ -0,0 +1,117 @@ +// Step2_1 +// 目的: floyd's cycle detectionの実装をしてみる。 + +/* + フロイドの循環検出法では2人の内1人が一歩先にいる状態から走査をスタートする。 + サイクルごとに2人の現在位置が同じでないかを確認していき同じであれば循環しているので走査を終了。 + サイクルごとにスタート位置が手前の方は一歩ずつ進む。一歩先にいる方は二歩進んでいく。 + 一歩先の人が次のノードを見つけられなければ循環していないので終了。 + この方法では走査しながら確認できる(ワンパス)ので追加の補助空間計算量がかからず、空間計算量がO(1)となる。 + https://leetcode.com/problems/linked-list-cycle/solutions/4831939/brute-optimal-both-approach-full-explained-java-c-python3-rust-go-javascript/ +*/ + +/* + 入力のリンクリストのサイズをNとする + 時間計算量: O(N) 全走査するため + 空間計算量: O(1) 全走査しながらインプレイスで重複チェックしており、HashSetなどに記録していないため +*/ + +use std::{cell::RefCell, rc::Rc}; + +pub struct ListNode { + pub next: Option>>, +} + +pub struct Solution {} +impl Solution { + pub fn has_cycle(head: Option>>) -> bool { + let (mut slow_node, mut fast_node) = { + let current = head.as_ref().map(Rc::clone); + let next = head.and_then(|next_node| next_node.borrow().next.as_ref().map(Rc::clone)); + + (current, next) + }; + + while let (Some(slow), Some(fast)) = (slow_node, fast_node) { + if Rc::ptr_eq(&slow, &fast) { + return true; + } + + slow_node = slow.borrow().next.as_ref().map(Rc::clone); + fast_node = fast + .as_ref() + .borrow() + .next + .as_ref() + .and_then(|next_node| next_node.as_ref().borrow().next.as_ref().map(Rc::clone)); + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build_list_with_cycle( + cycle_position: Option, + list_len: usize, + ) -> Option>> { + if list_len == 0 { + panic!("invalid list_len require 1"); + } + + let nodes = (0..list_len) + .map(|_| Rc::new(RefCell::new(ListNode { next: None }))) + .collect::>(); + let tail_position = list_len - 1; + + // nodeのnextに接続する + for (i, node) in nodes.iter().enumerate() { + if let Some(next_node) = nodes.get(i + 1) { + node.borrow_mut().next = Some(Rc::clone(&next_node)); + } + } + + // tail_nodeのnextにcycle_positionで指定されたnodeを接続 + if let Some(cycle_position) = cycle_position { + if let (Some(tail_node), Some(cycle_to_node)) = + (nodes.get(tail_position), nodes.get(cycle_position)) + { + tail_node.borrow_mut().next = Some(Rc::clone(&cycle_to_node)); + } + } + + Some(Rc::clone(&nodes[0])) + } + + #[test] + fn step2_1_no_cycle_test() { + let expect = false; + let no_cycle = build_list_with_cycle(None, 4); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(None, 1); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(4), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(2), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + } + + #[test] + fn step2_1_with_cycle_test() { + let expect = true; + let with_cycle = build_list_with_cycle(Some(1), 4); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(1), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(2), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs index dfbe3cb..bb7753d 100644 --- a/src/bin/step3.rs +++ b/src/bin/step3.rs @@ -9,23 +9,110 @@ // 作れないデータ構造があった場合は別途自作すること /* - 時間計算量: - 空間計算量: + 入力のリンクリストのサイズをNとする + 時間計算量: O(N) 全走査するため + 空間計算量: O(N) 全走査しながらHashSetに記録していくため */ /* - 1回目: 分秒 - 2回目: 分秒 - 3回目: 分秒 + 1回目: 1分51秒 + + 1回目で問題の解を書くだけだと2分かからなかったので、 + - 単方向リンクリストの構造体(pub struct ListNode) + - テストコードで利用する単方向リンクリスト生成処理(fn build_list_with_cycle) + - 問題の解(fn has_cycle) + 以降は上記を書く時間を計測する + + 2回目: 11分2秒 + 3回目: 8分52秒 + 4回目: 8分40秒 + 5回目: 8分6秒 */ +use std::{cell::RefCell, collections::HashSet, rc::Rc}; + +pub struct ListNode { + next: Option>>, +} + pub struct Solution {} -impl Solution {} +impl Solution { + pub fn has_cycle(head: Option>>) -> bool { + let mut visited_nodes = HashSet::new(); + let mut next_node = head; + + while let Some(current_node) = next_node { + if !visited_nodes.insert(current_node.as_ptr()) { + return true; + } + + next_node = current_node.borrow().next.as_ref().map(Rc::clone); + } + + false + } +} #[cfg(test)] mod tests { use super::*; + fn build_list_with_cycle( + cycle_position: Option, + list_len: usize, + ) -> Option>> { + if 0 == list_len { + panic!("invalid list_len require 1"); + } + + let nodes = (0..list_len) + .map(|_| Rc::new(RefCell::new(ListNode { next: None }))) + .collect::>(); + let tail_position = list_len - 1; + + for (i, node) in nodes.iter().enumerate() { + if let Some(next_node) = nodes.get(i + 1) { + node.borrow_mut().next = Some(Rc::clone(&next_node)); + } + } + + if let Some(cycle_position) = cycle_position { + if let (Some(tail_node), Some(cycle_to_node)) = + (nodes.get(tail_position), nodes.get(cycle_position)) + { + tail_node.borrow_mut().next = Some(Rc::clone(&cycle_to_node)); + } + } + + Some(Rc::clone(&nodes[0])) + } + + #[test] + fn step3_no_cycle_test() { + let expect = false; + let no_cycle = build_list_with_cycle(None, 4); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(None, 1); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(4), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + + let no_cycle = build_list_with_cycle(Some(2), 2); + assert_eq!(Solution::has_cycle(no_cycle), expect); + } + #[test] - fn step3_test() {} + fn step3_with_cycle_test() { + let expect = true; + let with_cycle = build_list_with_cycle(Some(1), 4); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(1), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + + let with_cycle = build_list_with_cycle(Some(2), 3); + assert_eq!(Solution::has_cycle(with_cycle), expect); + } } From 0df1b4fb713f13ba42cede4908431d193b7736ee Mon Sep 17 00:00:00 2001 From: t9a Date: Fri, 17 Oct 2025 17:17:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E3=82=AF=E3=83=AD=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=A3=E3=83=BC=E5=BC=95=E6=95=B0=E5=90=8D=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/step2_1_floyds_cycle_detection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/step2_1_floyds_cycle_detection.rs b/src/bin/step2_1_floyds_cycle_detection.rs index 88df6ea..9a22ab9 100644 --- a/src/bin/step2_1_floyds_cycle_detection.rs +++ b/src/bin/step2_1_floyds_cycle_detection.rs @@ -27,7 +27,7 @@ impl Solution { pub fn has_cycle(head: Option>>) -> bool { let (mut slow_node, mut fast_node) = { let current = head.as_ref().map(Rc::clone); - let next = head.and_then(|next_node| next_node.borrow().next.as_ref().map(Rc::clone)); + let next = head.and_then(|head_node| head_node.borrow().next.as_ref().map(Rc::clone)); (current, next) };