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分筆が止まったらもう一回みて全部消す
// 正解したら終わり

/*
問題の理解
- 整数からなる配列numsと整数targetが与えられる。numsにtargetが含まれる場合はその位置を返す。見つからなければ-1を返す。
numsはソートされた配列をk回転させたものが与えられる。[0,1,2,4,5,6,7] を3回転すると[4,5,6,7,0,1,2]となる。kの位置で配列を分割して左右を入れ替えるイメージ。
numsに含まれる値は一意である。
制約として時間計算量O(log n)を満たす必要がある。

何を考えて解いていたか
- lower_boundを探して、探索終了後にtargetと等しいか一度確認して答えを返す方針で解く。
この実装が難しそうなら、探索中にtarget == nums[i] を見つけたら早期リターンする。
[start,end)半開区間で扱う。
この考え方だと、middleの左右にmiddleよりも大きい値が存在するケースを処理できない。不変条件が壊れる。
[start,end]な閉区間にしてtargetがありそうな範囲に絞り込んでいって、探索終了時にnums[end] == targetとなるかを確認するほうが良さそう
ここまで考えて手が止まってので答えを見る。

何がわからなかったか
- 回転後の配列でmiddleの左右どちら側にtargetがあるのかどう判断すればよいか分からなかった。

解答の理解
https://leetcode.com/problems/search-in-rotated-sorted-array/solutions/3879263/100-binary-search-easy-video-ologn-optim-yp2k/
- start ~ middle , middle ~ end どちらがソートされているかを判定する。
- 元々ソートされていた配列をk回転させた後の配列は中央位置から見て左右どちらかの区間は必ずソートされている性質を持つ。
- ソートされている方の範囲で値を探す。
- targetがソートされている範囲に含まれているかを判定する。
- ソートされている範囲にtargetが含まれていれば、探索範囲をソートされている範囲に縮小する。
- ソートされている範囲にtargetが含まれていなければ、ソートされている範囲を探索範囲外にする。

正解してから気づいたこと
- この解法で解くにしても、if-elseのネストはもう少しなんとかしたい。
- continueにしても良いが、場合分けなのでelseの方がより意図に対応している感じがする。
- 早期リターンしない解法はstep2で他の人のコードを見つつ練習する。
*/

pub struct Solution {}
impl Solution {
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
if nums.is_empty() {
return -1;
}

let mut start = 0;
let mut end = nums.len() - 1;

while start <= end {
let middle = start + (end - start) / 2;

if nums[middle] == target {
return middle as i32;
}
Comment on lines +56 to +58
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はいわゆる探索範囲が閉区間[start, end]になっているタイプですが、ここのnums[middle] == targetの早期returnを外してstep2, 3のようにループ終了後に返り値を決める書き方に変えると、targetが配列numsの先頭にある場合などに、usize型のendで0 - 1を計算してオーバーフローする可能性がある、ということに注意が必要ですね。


let is_start_side_sorted = nums[start] <= nums[middle];
let is_target_in_start_side = nums[start] <= target && target <= nums[middle];
let is_target_in_end_side = nums[middle] < target && target <= nums[end];

if is_start_side_sorted {
if is_target_in_start_side {
end = middle - 1;
} else {
start = middle + 1;
}
} else {
if is_target_in_end_side {
start = middle + 1;
} else {
end = middle - 1;
}
}
}

-1
}
}

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

#[test]
fn step1_test() {
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 0), 4);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 3), -1);
assert_eq!(Solution::search(vec![1], 0), -1);
assert_eq!(Solution::search(vec![], 0), -1);

assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 2), 6);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 4), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 1), 1);
assert_eq!(Solution::search(vec![5, 1, 3], 5), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 3), 2);
}
}
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/h1rosaka/arai60/pull/45#discussion_r2529537136
- 探索中に早期リターンせずに、探索終了時のポインタ位置の値が等しいかどうかで結果を返す方式。練習しておきたい解法。

https://github.com/t9a-dev/LeetCode_arai60/pull/42/changes/BASE..ba5b009c3fd1c49d89c5d2e1c2c09e7302f3b7d4#r2650867711
- 自分がもらったレビューコメント。
回転済みの配列において、nums.last()は右区間に含まれる値になる。
nums[i] <= nums.last()と比較することで、nums[i]が右区間に属するかを判定している。
完璧に理解できているとは言えない(自分の道具として思い通りに扱えない)ものの、何を言っているのかは分かる感じ。

https://github.com/Yoshiki-Iwasa/Arai60/pull/36#discussion_r1712955053
ペア?にするという別の解法。
ぱっと見よく分からないが二分探索の理解が危ういという自覚があるのでなるべく別の解法も試したい。step2a.rsでやってみる。

改善する時に考えたこと
- 早期リターンせずに探索終了時のポインタから答えを求める解法を実装して理解を深める。
- 回転範囲の判定をフラグ変数にすると文脈が分断される気がしたので今回は止めた。一度しか利用しないのでその場で条件を読んだ方が良いという感覚。

所感
- step1の解法と比べて、回転している区間の処理とソートされている区間の単純な二分探索処理に分けられている。
問題を細分化して、個々に対応できているので良い解法だと思った。
- 回転の処理でendの範囲外アクセスが起きないようになっている。
必要ないが、戻り値のチェックで配列アクセスするときに、end < nums.len() としておくことで自分を含めた読み手の不安を取り除きたいと思った。
ここに不安を感じること自体が二分探索を理解しきれていないという感じもする。(コードを見て自信を持って範囲外アクセスは起きないと言えないところ)
https://github.com/hayashi-ay/leetcode/pull/49/changes#r1527147960
同じようなことを考えている人がいた。人が読むことを考えると絶対に削るべきというほどでも無いという感じだと思った。
*/

pub struct Solution {}
impl Solution {
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
if nums.is_empty() {
return -1;
}

let mut start = 0;
let mut end = nums.len();

while start < end {
let middle = start + (end - start) / 2;
let last_num = *nums.last().unwrap();

// middle ~ last_num はソートされているが、targetがlast_numを超えるので、この範囲にtargetは無い。
// end側を捨てる。
if nums[middle] <= last_num && target > last_num {
end = middle;
continue;
}

// middle が last_numを超えているので回転している。targetはlast_num以下なので、end側にtargetがある。
// start側を捨てる。
if nums[middle] > last_num && target <= last_num {
start = middle + 1;
continue;
}

// ここまで到達すると、探索範囲が回転していない(ソートされている)のでそのまま二分探索を行う。
// endがlower_boundになる。
if target <= nums[middle] {
end = middle;
} else {
start = middle + 1;
}
}

if end < nums.len() && nums[end] == target {
return end as i32;
}

-1
}
}

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

#[test]
fn step2_test() {
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 0), 4);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 3), -1);
assert_eq!(Solution::search(vec![1], 0), -1);
assert_eq!(Solution::search(vec![], 0), -1);

assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 2), 6);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 4), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 1), 1);
assert_eq!(Solution::search(vec![5, 1, 3], 5), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 3), 2);
}
}
71 changes: 71 additions & 0 deletions src/bin/step2a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Step2a
// 目的: 別の解法を練習する

/*
https://github.com/Yoshiki-Iwasa/Arai60/pull/36#discussion_r1712955053
ペア?にするという別の解法を実装してみる。

解法の理解
- 結局よくわからなかったのでGPT-5.2に聞いて写経した。
- binary_search_by_key(b, f)を引数に取る。
https://doc.rust-lang.org/std/primitive.slice.html#method.binary_search_by_key
- bは検索キー。検索したいものそのもの。
- fはキー抽出関数。bと比較できるように、比較に利用するキーを生成する。
- (bool,i32)のタプルをb(検索キー)としている。
- boolでどちらの区間かが分かるので、探索範囲を半分にできる。
- target=0,last=2
- [4, 5, 6, 7, 0, 1, 2]
- [F, F, F, F, T, T, T] (nums[i] <= last)
F=false,T=true
- target <= last は T となるので、右区間に含まれることが分かる。一度に半分に探索範囲を絞れる。
- fが生成するキーとbの検索キーを比較して等しいものが見つかるかどうか二分探索する。
https://github.com/t9a-dev/LeetCode_arai60/pull/42/changes/BASE..ba5b009c3fd1c49d89c5d2e1c2c09e7302f3b7d4#r2650867711
ここでもらったコメントの理解が進んだ気がする。

所感
https://doc.rust-lang.org/src/core/slice/mod.rs.html#2932-2934
- binary_search_byの実装を見てみたら配列の境界にかなり気を使っている様子がコメントから伺えて面白かった。
- 戻り値のインデックスを返す前にインデックスが範囲内であるかどうかを判定しているコードがある。
https://doc.rust-lang.org/src/core/slice/mod.rs.html#2975
- コンパイラに対してインデックスが範囲内であるという仮定を伝えている。
https://doc.rust-lang.org/stable/std/hint/fn.assert_unchecked.html
- 最適化ヒントと呼ばれるものらしい。(C++日本語リファレンスより)
https://cpprefjp.github.io/lang/cpp23/portable_assumptions.html
*/

pub struct Solution {}
impl Solution {
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
if nums.is_empty() {
return -1;
}

let last = nums.last().unwrap();
//(bool ,i32) -> (targetが左右どちらの区間に属するか, 探している値)
let target_key = (target <= *last, target);

match nums.binary_search_by_key(&target_key, |num| (num <= last, *num)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

|num| (num <= last, *num) を target_key と合わせて二回書いている印象なので、一回変数においてはどうでしょうか。

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 search(nums: Vec<i32>, target: i32) -> i32 {
    if nums.is_empty() {
        return -1;
    }

    let last = *nums.last().unwrap();
    let make_key = |x: i32| (x <= last, x);

    match nums.binary_search_by_key(&make_key(target), |num| make_key(*num)) {
        Ok(i) => i as i32,
        Err(_) => -1,
    }
}

Ok(i) => i as i32,
Err(_) => -1,
}
}
}

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

#[test]
fn step2a_test() {
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 0), 4);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 3), -1);
assert_eq!(Solution::search(vec![1], 0), -1);
assert_eq!(Solution::search(vec![], 0), -1);

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

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

/*
n = nums.len()
時間計算量: O(log n)
空間計算量: O(1)
*/

/*
1回目: 4分36秒
2回目: 3分21秒
3回目: 4分16秒
*/

/*
所感
- コード量に対して時間がかかっている気がするが、暗記せず毎回ロジックを考える部分に時間がかかっているのでむしろ良い傾向だと思った。
*/

pub struct Solution {}
impl Solution {
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
if nums.is_empty() {
return -1;
}

let mut start = 0;
let mut end = nums.len();
let last_num = *nums.last().unwrap();

while start < end {
let middle = start + (end - start) / 2;

if last_num < target && nums[middle] <= last_num {
end = middle;
continue;
}

if target <= last_num && last_num < nums[middle] {
start = middle + 1;
continue;
}

if target <= nums[middle] {
end = middle;
} else {
start = middle + 1;
}
}

if end < nums.len() && nums[end] == target {
return end as i32;
}

-1
}
}

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

#[test]
fn step3_test() {
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 0), 4);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 3), -1);
assert_eq!(Solution::search(vec![1], 0), -1);
assert_eq!(Solution::search(vec![], 0), -1);

assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 2), 6);
assert_eq!(Solution::search(vec![4, 5, 6, 7, 0, 1, 2], 4), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 5), 0);
assert_eq!(Solution::search(vec![5, 1, 3], 1), 1);
assert_eq!(Solution::search(vec![5, 1, 3], 3), 2);
}
}