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

/*
問題の概要: 2つの整数を持つ配列が与えられる。2つのどちらにも含まれる整数列を戻り値として返す。
例:
input: [1,2,2,1],[2,2], output: [2]
input: [4,9,5],[9,4,9,8,4], output: [4,9]又は[9,4]

何がわからなかったか
-
- 特になし

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

想定ユースケース
-
- 愚直に配列を全て見ていって自身と同じ値がもう片方の配列にあれば結果の配列に追加して最後に返す。
配列を全て見るので時間計算量: O(N^2),空間計算量: O(N)
- HashSetに片方の配列の値を追加する。もう片方の配列を全走査しながらHashSetに値が含まれているか探索して、含まれていれば
結果の配列に追加していく。
- HashSetのメソッドを確認していたらそのままのintersectionメソッドを見つけてしまい、
アルゴリズムについて考える必要がなくなってしまった。とりあえずHashSet::intersection()メソッドを利用しない形で書く。

正解してから気づいたこと
-
- Vec<i32>型であるnums2.contains()は線形探索になるのでO(N)になるが、HashSetであれば探索に平均O(1)となるのでHashSetにしたほうが良さそう。
入力の型がi32で固定長なのでHashSetにするときに伴うハッシュ化はO(N)になるがループの外で一度だけ行うので問題ない。
- ループで値を取り出す配列はサイズが小さい方にしたほうが効率が良い。内側のHashSetの探索時間はO(1)とすると計算量Nが支配的になる。
Nは配列のサイズなので全走査する配列はより小さい方が時間計算量が少なくなる。
- 変数名intersectionがメソッド名と被っていて違和感を感じた。

*/

use std::collections::HashSet;

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
let mut intersection = HashSet::with_capacity(nums1.len().min(nums2.len()));
for num1 in nums1 {
if nums2.contains(&num1) {
intersection.insert(num1);
}
}

intersection.into_iter().collect()
}
}

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

#[test]
fn step1_test() {}
fn step1_test() {
let mut result = Solution::intersection(vec![1, 2, 2, 1], vec![2, 2]);
result.sort();
assert_eq!(result, vec![2]);

let mut result = Solution::intersection(vec![4, 9, 5], vec![9, 4, 9, 8, 4]);
result.sort();
assert_eq!(result, vec![4, 9]);
}
}
53 changes: 46 additions & 7 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,64 @@

/*
講師陣はどのようなコメントを残すだろうか?
-
- if文周りの書き方が冗長なので、もう少しすっきり書ける方法がある

他の人のコードを読んで考えたこと
-
- HashSetを利用したワンライナーの例。mapで参照を返すだけの部分は書き直せそう。
https://leetcode.com/problems/intersection-of-two-arrays/solutions/4406232/one-liner-using-set-operations/

他の想定ユースケース
-
- なぜbinary_searchを使っているのか、HashSet自体は使っているのに、filterでの重複判定に使っている理由などよくわからなかった。
Copy link

Choose a reason for hiding this comment

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

これはnums1のサイズが小さくnums2が比較的大きいがソートはされているという条件のもとで解くとしたら、こういう解き方もあるねという話だと思います。
以下のコメントログも参考になりそうです。
katataku/leetcode#12 (comment)

Runtime Beats 100%であることをタイトルで示しているが、step1.rsの自身の実装でもRuntime Beats 100%となるので、この解法は選択肢に入らないなと思った。
https://leetcode.com/problems/intersection-of-two-arrays/solutions/5678828/beats-100-in-runtime/

改善する時に考えたこと
-
- メソッド名と同名の変数名とならないようintersectedに変更
- nums1とnums2でサイズが小さい方を全走査に利用する。もう片方はHashSetに変換する。
Vec<i32>.contains() -> HashSet<i32>.contains()に変更することで時間計算量を改善する。
- そもそもRustではHashSet::intersection()メソッドが利用できるので、step3.rsではこの方法で書く。
*/

use std::collections::HashSet;

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
let linear_search_base;
let others: HashSet<i32>;
let mut intersected: HashSet<i32> = HashSet::with_capacity(nums1.len().min(nums2.len()));

// 入力のデータ型がi32と固定長なので、HashSet作成時の時間計算量はO(1)
if nums1.len() <= nums2.len() {
linear_search_base = nums1;

Choose a reason for hiding this comment

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

長さが短い方の配列をイテレートしたいということであれば以下のイメージで引数を入れ替えて関数を呼びなおすという書き方もいいかもしれません。
※rustのことをよく分かっていないので雰囲気で書いてます。

if nums1.len() < nums2.len() {
    return intersection(nums2, nums1);
}

Copy link
Owner Author

@t9a-dev t9a-dev Oct 9, 2025

Choose a reason for hiding this comment

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

ありがとうございます。ご指摘いただ方針でだいぶすっきり書けるなと思いました。
最初見たときに再帰呼び出しをあまりカジュアルに使うべきではないかなとも思ったのですが

  • 最悪でも一度しか呼び出さない
  • メソッド先頭でガード節で書いておけば、再帰関数呼び出し時に余計なデータがスタックフレームに含まれない

といった理由からパフォーマンスなどの制約から関数呼出しをなるべく行わないといった状況でなければ大丈夫だと思いました。

impl Solution {
    pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
        if nums1.len() > nums2.len() {
            return Self::intersection(nums2, nums1);
        }

        let linear_search_base = &nums1;
        let others: HashSet<i32> = HashSet::from_iter(nums2);
        let mut intersected: HashSet<i32> = HashSet::with_capacity(nums1.len());

        // ここで全走査するので、サイズが小さい方が効率が良い
        for base in linear_search_base.into_iter() {
            if others.contains(&base) {
                intersected.insert(*base);
            }
        }

        intersected.into_iter().collect()
    }
}

others = HashSet::from_iter(nums2);
} else {
linear_search_base = nums2;
others = HashSet::from_iter(nums1);
}
Comment on lines +42 to +49

Choose a reason for hiding this comment

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

入力のデータ型がi32と固定長なので、単一要素のハッシュ計算はO(1)、HashSetの構築はO(n)、だと思ったんですが、どうでしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。ご指摘頂いたとおりです。
計算量の見積もりが苦手で勘違いしたままの記述が残っていました。


// ここで全走査するので、サイズが小さい方が効率が良い
for base in linear_search_base.into_iter() {
if others.contains(&base) {
intersected.insert(base);
}
}

intersected.into_iter().collect()
}
}

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

#[test]
fn step2_test() {}
fn step2_test() {
let mut result = Solution::intersection(vec![1, 2, 2, 1], vec![2, 2]);
result.sort();
assert_eq!(result, vec![2]);

let mut result = Solution::intersection(vec![4, 9, 5], vec![9, 4, 9, 8, 4]);
result.sort();
assert_eq!(result, vec![4, 9]);
}
}
50 changes: 50 additions & 0 deletions src/bin/step2_1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Step2_1
// 目的: step2で違和感を感じたif文の部分を修正する

/*
他の人のコードを読んで考えたこと
- 標準ライブラリのHashSet::intersection()メソッド(実装はhashbrownクレート)の実装を確認していたところ、
if文でよりスッキリかける書き方を見つけた。
https://docs.rs/hashbrown/latest/src/hashbrown/set.rs.html#799-809

改善する時に考えたこと
- step2で書いたif分の部分が冗長で違和感を感じたので、より簡潔な形になるように書きなおす。
*/

use std::collections::HashSet;

pub struct Solution {}
impl Solution {
pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
let mut intersected = HashSet::<_>::with_capacity(nums1.len().min(nums2.len()));
Copy link

Choose a reason for hiding this comment

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

この行後ろに回したらいかがでしょうか。min を取る必要が無くなりそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。直後の行でifによりminを算出しているので、同じ判定をメソッド内で繰り返してしまっていると理解しました。

Suggested change
let mut intersected = HashSet::<_>::with_capacity(nums1.len().min(nums2.len()));
let (smaller_nums, larger_nums, mut intersected) = if nums1.len() <= nums2.len() {
(
&nums1,
HashSet::<_>::from_iter(nums2),
HashSet::with_capacity(nums1.len()),
)
} else {
(
&nums2,
HashSet::<_>::from_iter(nums1),
HashSet::with_capacity(nums2.len()),
)
};

let (smaller_nums, larger_nums) = if nums1.len() <= nums2.len() {
(nums1, HashSet::<_>::from_iter(nums2))
} else {
(nums2, HashSet::<_>::from_iter(nums1))
};

for small in smaller_nums {
if larger_nums.contains(&small) {
intersected.insert(small);
}
}

intersected.into_iter().collect()
}
}

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

#[test]
fn step2_1_test() {
let mut result = Solution::intersection(vec![1, 2, 2, 1], vec![2, 2]);
result.sort();
assert_eq!(result, vec![2]);

let mut result = Solution::intersection(vec![4, 9, 5], vec![9, 4, 9, 8, 4]);
result.sort();
assert_eq!(result, vec![4, 9]);
}
}
34 changes: 30 additions & 4 deletions src/bin/step3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,43 @@
// レビューを受ける
// 作れないデータ構造があった場合は別途自作すること

use std::collections::HashSet;

/*
nums1のサイズをN,nums2のサイズをMとする
時間計算量: O(N + M) HashSetの初期化(from_iter())でO(1)の操作をN回,M回行うため。
空間計算量: O(N + M) num1,num2のHashSetが同時に存在するため。
*/

/*
時間計算量:
空間計算量:
memo:
ワンライナーで書くとnums1とnums2を両方HashSetにする必要があるので、空間計算量が増える。
step2_1.rsの実装だと入力のうち、どちらか一方だけをHashSetにして、もう一方はそのまま(in-place)操作するので、
空間計算量が優れている。
*/

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn intersection(nums1: Vec<i32>, nums2: Vec<i32>) -> Vec<i32> {
HashSet::<_>::from_iter(nums1)
.intersection(&HashSet::from_iter(nums2))
.copied()
.collect()
}
}

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

#[test]
fn step3_test() {}
fn step3_test() {
let mut result = Solution::intersection(vec![1, 2, 2, 1], vec![2, 2]);
result.sort();
assert_eq!(result, vec![2]);

let mut result = Solution::intersection(vec![4, 9, 5], vec![9, 4, 9, 8, 4]);
result.sort();
assert_eq!(result, vec![4, 9]);
}
}