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
127 changes: 120 additions & 7 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,139 @@
// 正解したら終わり

/*
問題の理解
141.Linked-List-Cycle
- リンクリストの先頭が与えられるので循環しているかを判定する。
リンクリスト先頭からnextを見ていってnextが存在しなければ、falseを返す。
リンクリストのnextに入っているアドレスを取得しておいて、nextで辿っていったとき同じアドレスを発見したら一周してきたことがわかるのでfalse。

何がわからなかったか
-
- Rustにおける循環参照するような型の定義や扱い。

何を考えて解いていたか
-
- この問題はLeetCodeでRust実装の回答を判定できないのでどうやってテストするか。
ChatGPTを活用してテストコードを作成して、テストコードがパスするかで判断する。

- テストを行うにはリンクリストで循環しているものとそうでないものを自分で生成する必要がある
とりあえず実装方法が思いつかないのでChatGPTに作ってもらって、答えを理解したと思ったら答えを隠して自分で実装してみる。

想定ユースケース
-
- headが渡された時点ではどこまでノードがつながっているがわからないので、全走査(nextを辿る)する必要がある。
- nextで次への参照を見つけるたびにアドレスをハッシュセットに記録していき、これまでに出現したアドレスを見つけたら循環していると判定する。
Box<T>はTを格納したヒープ領域へのスマートポインタを返すとあるので、ここからアドレスを取得して比較すればよいと思ったが、ある時点で取得したアドレス位置に
ずっと同じデータが格納されていることが保証できないこととの兼ね合いで所有権を要求するので、安易にアドレスでの比較ができないようになっている模様。
なのでBox<T>をclone()するようにコンパイラはヒントを出してくるが、Box<T>をclone()すると同じ値で別のヒープ領域にデータを配置するような動きになり、
すでに見つけたアドレスなのかの判定ができなくなる。
- Rc(reference count)でラップするとclone()しても値の複製は行われず、clone()元と同じ参照が得られる。内部的には参照カウントをインクリメントして、
参照カウントが0(誰も値を見ていなくなった)になったら値をdropしているそう。(他の場所で参照されているのにdropしないように)
プログラム実行中にどこかで参照されている間は同じメモリ領域を指し続けるので今回利用できる。
- Box<T>,Rc<T>,Cell<T>,RefCell<T>について実装過程で調べることになったので、今まで曖昧なままにしていた部分が大分クリアになって良かった。

正解してから気づいたこと
-
- 変数名についてtraced_nodeよりはvisited_nodeほうがより直感的だと思った。
- RwLock<T>よりもRefCell<T>の方が適切。ListNodeのnextを更新するために内部可変性が必要だが、RwLock<T>はマルチスレッドで利用するものなので、シングルスレッドで内部可変性を得るのにはRefCell<T>の方が適切である。
Cell<T>はTがCopyトレイトを実装していることを要求するのでTがプリミティブ型である場合かつ、Tを丸ごと入れ替えるような場合に利用する。
RefCell<T>は参照を返すので今回のように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<T>は必要ない。 今回の実装過程で初めて知ったが、Rc<T>もヒープ領域にデータを確保するため。ListNodeが自分自身の型をフィールドに持つようなコンパイル時にサイズが確定しない型であったので、Box<T>でラップしたがアドレスを比較するために
HashSetにポインタ保存しておくという用途ではポインタの所有者がHashSetとListNodeと複数になるのでRc<T>の方が適切だった。
Box<T>ではポインタの所有権が単独になるので、HashSetにポインタを保持しておくことができない。
https://doc.rust-jp.rs/book-ja/ch15-04-rc.html

- Rc<T>に対しては.clone()ではなく、Rc::clone()という書き方が推奨されている。
どちらも動き方は同じだがRc<T>は値の複製を行わず参照カウンタのインクリメントのみの軽量な操作なので、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<Rc<RwLock<Box<ListNode>>>>,
}

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn has_cycle(head: Option<Rc<RwLock<Box<ListNode>>>>) -> 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<usize>,
list_len: usize,
) -> Option<Rc<RwLock<Box<ListNode>>>> {
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::<Vec<_>>();
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);
}
}
109 changes: 99 additions & 10 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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にすることで、曖昧さを軽減した。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

変数名ですがnodeのままでもいいかなと思います。
current_nodeだとcurrentと対になるようなノード(previous, nextなど)があるかもしれないという連想が働くので、ちょっとだけ読み手のコストが増す気がします。
Zun-U/coding-practice-mochi0123#2 (comment)

- よく理解せずに混乱しながら利用していたRwLock<T>,Box<T>の利用をやめた。
- Rc<T>のデータに対しては.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<Rc<RefCell<ListNode>>>,
}
Comment on lines +40 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Option<Rc<RefCell<ListNode>>>がボイラープレートっぽいので、型エイリアスを使うのはどうでしょうか。

Suggested change
pub struct ListNode {
pub next: Option<Rc<RefCell<ListNode>>>,
}
type Link = Option<Rc<RefCell<ListNode>>>;
pub struct ListNode {
next: Link,
}

テストとか書くときも少し気が楽になるかもです。

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.

ありがとうございます。型エイリアスは活用したことがなく提案いただくまで選択肢にもなかったので、すごく勉強になりました。(アハ体験でした。)


pub struct Solution {}
impl Solution {}
impl Solution {
pub fn has_cycle(head: Option<Rc<RefCell<ListNode>>>) -> bool {
let mut visited_node: HashSet<_> = HashSet::new();
let mut next_node = head;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

レビューが遅くなり、申し訳ありません。
next_nodeの命名がなるほど、と思った反面、「next」という単語の意味に「現時点から次」と連想してしまう場合もあると思いますので、target_nodeにする案はどうでしょうか。


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<usize>,
list_len: usize,
) -> Option<Rc<RefCell<ListNode>>> {
if list_len == 0 {
panic!("invalid list_len require 1");
}

let nodes = (0..list_len)
.map(|_| Rc::new(RefCell::new(ListNode { next: None })))
.collect::<Vec<_>>();
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);
}
}
117 changes: 117 additions & 0 deletions src/bin/step2_1_floyds_cycle_detection.rs
Original file line number Diff line number Diff line change
@@ -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<Rc<RefCell<ListNode>>>,
}

pub struct Solution {}
impl Solution {
pub fn has_cycle(head: Option<Rc<RefCell<ListNode>>>) -> bool {
let (mut slow_node, mut fast_node) = {
let current = head.as_ref().map(Rc::clone);
let next = head.and_then(|head_node| head_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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

うーん、こういうヘルパーを用意すると少しきれいになりますかね。

fn next_node(node: Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> {
    node.and_then(|n| n.borrow().next.clone())
}

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.

ありがとうございます。メソッドチェーンが長いなとは書いていて思ったのですが、言語仕様的に必要な呼び出しだから仕方ないかと違和感をスルーしていた部分でした。
ご指摘いただいてから改めて考えみると、ロジックに直接関係ない冗長な部分をヘルパーに切り出すのは納得できる理由だと思いました。
だいぶすっきり書けるなと思いました。

pub fn has_cycle(head: Option<Rc<RefCell<ListNode>>>) -> bool {
    let (mut slow_node, mut fast_node) = {
        let current = head.as_ref().map(Rc::clone);
        let next = Self::next_node(head);

        (current, next)
    };

    while let (Some(slow), Some(fast)) = (slow_node, fast_node) {
        if Rc::ptr_eq(&slow, &fast) {
            return true;
        }

        slow_node = Self::next_node(Some(slow));
        fast_node = Self::next_node(Self::next_node(Some(fast)));
    }

    false
}

fn next_node(node: Option<Rc<RefCell<ListNode>>>) -> Option<Rc<RefCell<ListNode>>> {
    node.and_then(|n| n.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<usize>,
list_len: usize,
) -> Option<Rc<RefCell<ListNode>>> {
if list_len == 0 {
panic!("invalid list_len require 1");
}

let nodes = (0..list_len)
.map(|_| Rc::new(RefCell::new(ListNode { next: None })))
.collect::<Vec<_>>();
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);
}
}
Loading