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

/*
問題の理解
- 符号付き整数を値として持つソート済の単方向リンクリストが与えられる。
単方向リンクリストの値に着目して一意の値を持つノードのみを抽出して返す。

何がわからなかったか
-
- 入力をインプレイスで変更して返す方法。できそうな気はするが5分では具体的な方法まで思いつかなかった。

何を考えて解いていたか
-

想定ユースケース
-
- リンクリストを全走査しながら、ノードの値を配列に集める。
配列からcontainsのカウントが1になる値のみを抽出してソート後、リンクリストを生成して返す。

正解してから気づいたこと
-
- Rustのイテレータメソッドのcontainsではカウントを返さないことを知った。
- 問題を見たときに思いついた解法では時間計算量がO(N^2)になっているが、O(N)にはできそうな感じはする。
- HashMapを利用してキーにノードの値、値に出現回数を入れていき、出現回数が1の値で返却用のリンクリストを生成する方法でできる。
Copy link
Copy Markdown

@TrsmYsk TrsmYsk Oct 21, 2025

Choose a reason for hiding this comment

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

この方法はリストがソート済みのときにしか使えないので、ソートされてないときも使える方法を考えてみてもいいかもしれません。
inplaceでつなぎかえる方法もソート済み前提ですね。忘れてください。

*/

#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>,
}

impl ListNode {
#[inline]
fn new(val: i32) -> Self {
ListNode { next: None, val }
}
}
pub struct Solution {}
impl Solution {}
impl Solution {
pub fn delete_duplicates(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut node_values: Vec<_> = Vec::new();
let mut target_node = head;

while let Some(node) = target_node {
node_values.push(node.val);
target_node = node.next;
}

let single_occurrence_values = node_values
.iter()
.filter_map(|v| {
if node_values.iter().filter(|x| v == *x).count() == 1 {
return Some(v);
}
None
})
.collect::<Vec<_>>();

single_occurrence_values
.iter()
.rev()
.fold(None, |child, v| {
let mut parent = Box::new(ListNode::new(**v));
parent.next = child;
Some(parent)
})
}
}

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

fn vec_to_list_node(values: &Vec<i32>) -> Option<Box<ListNode>> {
values.into_iter().rev().fold(None, |child, v| {
let mut parent = Box::new(ListNode::new(*v));
parent.next = child;
Some(parent)
})
}

fn list_node_to_vec(head: &Option<Box<ListNode>>) -> Vec<i32> {
let mut out = Vec::new();
let mut current_node = head;

while let Some(node) = current_node {
out.push(node.val);
current_node = &node.next;
}

out
}

#[test]
fn step1_test() {}
fn step1_test() {
let source_vec = vec![1, 1, 2];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 2, 3, 3];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 1];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(list_node_to_vec(&Solution::delete_duplicates(head)), vec![]);
}
}
117 changes: 108 additions & 9 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,125 @@
// 改善する時に考えたこと

/*
講師陣はどのようなコメントを残すだろうか?
-

他の人のコードを読んで考えたこと
-
- 様々な解法が検討されており、非常に参考になる。
https://github.com/yas-2023/leetcode_arai60/pull/4/files

- LeetCodeでVimモードがあるの知らなかったので助かった。
https://github.com/yas-2023/leetcode_arai60/pull/4/files#diff-a2c7d2692707bbe67e316042ed9e36c45f92db461cb104915bedc6638dd9aa26R41-R43

他の想定ユースケース
-
- インプレイスでの実装を理解するのに以下の実装を参考にすることにしたが、結局理解ができなかった。
dummy,tailのうちtailがよくわからないのが原因だと考えられる。(コードが悪いということではない)
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://discord.com/channels/1084280443945353267/1195700948786491403/1197102971977211966
コメント集漁るともう少しある気がしますがとりあえずこれを。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

番兵がむしろ分からないのかもしれません。

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.

ありがとうございます。他の方のコードやレビューを確認しながら別の問題で実装に利用できたので理解できたと思います。
https://github.com/t9a-dev/LeetCode_arai60/pull/11/files#diff-02d815381f4af9ab10091ec503f6ff8fc63efd9ba6a1d06f2743d213c1a4879b

https://github.com/docto-rin/leetcode/pull/4/files#diff-2bdfe1140252df4bf36c06f29251eeade51991cec8bee544c92c5b27c63cfc7aR147-R197
インプレイスで処理する方法もstep2_1_in-place.rsで実装してみる

改善する時に考えたこと
-
- step1で二重の全走査部分をHashMapを用いた方法に書き換えて処理全体の時間計算量をO(N^2) -> O(N)に改善した。
- ナイーブな実装だが何をしているのかは分かりやすいと思う。
*/

/*
Nは入力のサイズとする
時間計算量: O(N)
空間計算量: O(N)
*/

use std::collections::HashMap;

#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>,
}

impl ListNode {
#[inline]
fn new(val: i32) -> Self {
ListNode { next: None, val }
}
}

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn delete_duplicates(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut frequency_by_node_value: HashMap<_, _> = HashMap::new();
let mut target_node = head;

while let Some(node) = target_node {
frequency_by_node_value
.entry(node.val)
.and_modify(|frequency| *frequency += 1)
.or_insert(1);

target_node = node.next;
}

let mut single_occurrence_values = frequency_by_node_value
.keys()
.filter_map(|node_value| match frequency_by_node_value.get(node_value) {
Some(frequency) if *frequency == 1 => Some(node_value),
_ => None,
})
.collect::<Vec<_>>();

single_occurrence_values.sort();

single_occurrence_values
.into_iter()
.rev()
.fold(None, |child, node_value| {
let mut parent = Box::new(ListNode::new(*node_value));
parent.next = child;
Some(parent)
})
}
}

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

fn vec_to_list_node(values: &Vec<i32>) -> Option<Box<ListNode>> {
values.into_iter().rev().fold(None, |child, v| {
let mut parent = Box::new(ListNode::new(*v));
parent.next = child;
Some(parent)
})
}

fn list_node_to_vec(head: &Option<Box<ListNode>>) -> Vec<i32> {
let mut out = Vec::new();
let mut current_node = head;

while let Some(node) = current_node {
out.push(node.val);
current_node = &node.next;
}

out
}

#[test]
fn step2_test() {}
fn step2_test() {
let source_vec = vec![1, 1, 2];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 2, 3, 3];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 1];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(list_node_to_vec(&Solution::delete_duplicates(head)), vec![]);
}
}
142 changes: 142 additions & 0 deletions src/bin/step2_1_in-place.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Step2_1_in-place
// 目的: インプレイスによる解法を実装しておく

/*
- インプレイスでの実装を理解するのに以下の実装を参考にすることにして少し手を加えた。
https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/solutions/6940535/somehow-elegant-iterative-solution-without-unwrap-and-clone-100/

- 先頭にダミーのノードを入れることで先頭から重複しているときに対応している。
このようにダミーの要素を計算のために追加することを「番兵」と呼ぶことが分かった。

- 問題の理解
cursor = sentinel_head(先頭番兵ノード)
cursor.next = node[i](初回はhead)
node[i].next = node[i+1](初回はheadの次ノード)
-カーソルが指しているnode[i].valとnode[i+1].valが重複している間ループに入る。
- 重複検知した時点でフラグを立てておく。
- 次ノードの次ノードをカーソルノードの次ノードの参照先に更新する。
- node[i].next = node[i+1].next
- ループの先頭に戻る
- 重複が発生していたかをフラグから判断
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

フラグは使わないほうが無難なので、処理を関数化できるかどうか検討してみてください。
Kaichi-Irie/leetcode-python#20 (comment)

- 重複が発生していたら、カーソルの指し先の次ノードをカーソルの指し先にする
- cursor.next = node[i].next
- 重複が発生していなければ、カーソルの指し先を一つ次に進める。
- cursor = node[i]

- 感想
- 他の人の実装を見ているときにtailという変数名に違和感を覚えてしまい、解法のロジックが理解できない現象に遭遇した。
- 自分の中でtailは末尾を想起するものである。
- 入力として与えられたリンクリストのあるノードを指してtailといっているのは、末尾ではないだろうと考えてしまう。
- どうしても納得できなかったので、ChatGPTに相談して変数名をwrite_cursorにすることにした。
Comment on lines +27 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tailに違和感があるんですね。

in-place(というよりストリーミングといった方が正確でしょうか)に解く場合、入力のリストを線形に1回だけ舐めていき、解答に含めるかどうかをその場で判断していきます。走査の各時点において、出力することが確定したノード一覧(リスト)が1つに決まると思いますが、その末尾のことをtailと言っていますね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pythonで恐縮ですが、以前似たようにtail関連のレビューをしたことがあり、よければご参考ください。

https://discordapp.com/channels/1084280443945353267/1318459221204930671/1423549410855354378

(擬似コード的なノリで眺めてもらえたらと思います。)

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.

in-place(というよりストリーミングといった方が正確でしょうか)に解く場合、入力のリストを線形に1回だけ舐めていき、解答に含めるかどうかをその場で判断していきます。

ありがとうございます。頂いた関連レビューと上記の説明で理解できました。(別の問題の実装で使えたので理解できているはず。)
https://github.com/t9a-dev/LeetCode_arai60/pull/11/files#diff-26ccac330cf02071c9820a981f57e47e8d7fbc46541becba86e1be21c665ff47
ストリーミング処理していることが自覚的でないことが原因だったと思いました。

- 学習目的でなるべく理解できるまで粘って実装してみたが、基本的にはHashMapを利用した実装をすると思った。
- アルゴリズムへの対応力が不足しているだけかも知れないが、読み書きにだいぶ時間と体力を消費している。
- HashMapを利用した実装であれば思いついた解法をそのまま書いて空間計算量O(N)
- インプレイスでの実装では時間と考える体力をそれなりに消費して空間計算量O(1)
- 時間計算量はどちらもO(N)
- 今の自分にとっては空間計算量O(1)に改善するために時間と体力を使いすぎるので、HashMapを利用した実装を採用すると思った。
- 選択肢としてより空間計算量が優れる実装方法を調べながら実装できること自体には大きな意義があると思った。
- インプレイスでの実装を時間と体力をあまり使わずに実装できることがソフトウェアエンジニアとしての常識なのかは気になるところ。
*/

/*
Nは入力のサイズとする
時間計算量: O(N)
空間計算量: O(1)
*/

#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>,
}

impl ListNode {
#[inline]
fn new(val: i32) -> Self {
ListNode { next: None, val }
}
}

pub struct Solution {}
impl Solution {
pub fn delete_duplicates(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut sentinel_head = Box::new(ListNode { val: 0, next: head });
let mut write_cursor = sentinel_head.as_mut();

while let Some(mut node) = write_cursor.next.take() {
let mut has_duplicate = false;

while let Some(next_node) = node.next.as_mut() {
// 重複していないのでループを抜ける
if node.val != next_node.val {
break;
}

has_duplicate = true;
// 次のノードと重複しているので飛ばして、次の次のノードを次のノードにする。
node.next = next_node.next.take();
Copy link
Copy Markdown

@TrsmYsk TrsmYsk Oct 21, 2025

Choose a reason for hiding this comment

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

nextで形容される変数が2つ出てきているため、それぞれの変数についてどんな情報を伝えたいのかが良く分かりません。変数の名前を工夫できないか検討してみてください。

continue;
}

if has_duplicate {
// 現在のノードは重複している値を持つので飛ばして次のノードと入れ替える。
write_cursor.next = node.next;
continue;
}

// 重複していないのでwrite_cursorを次のノードに移動する。
write_cursor = write_cursor.next.insert(node);
}

sentinel_head.next
}
}

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

fn vec_to_list_node(values: &Vec<i32>) -> Option<Box<ListNode>> {
values.into_iter().rev().fold(None, |child, v| {
let mut parent = Box::new(ListNode::new(*v));
parent.next = child;
Some(parent)
})
}

fn list_node_to_vec(head: &Option<Box<ListNode>>) -> Vec<i32> {
let mut out = Vec::new();
let mut current_node = head;

while let Some(node) = current_node {
out.push(node.val);
current_node = &node.next;
}

out
}

#[test]
fn step2_1_test() {
let source_vec = vec![1, 1, 2];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 2, 3, 3];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(
list_node_to_vec(&Solution::delete_duplicates(head)),
vec![2]
);

let source_vec = vec![1, 1, 1];
let head = vec_to_list_node(&source_vec);
assert_eq!(list_node_to_vec(&head), source_vec);
assert_eq!(list_node_to_vec(&Solution::delete_duplicates(head)), vec![]);
}
}
Loading