From 65630d2b4d03091f76fbc3cc44f6c74947976050 Mon Sep 17 00:00:00 2001 From: t9a Date: Wed, 10 Dec 2025 17:21:59 +0900 Subject: [PATCH 1/2] solve: 35.Search Insert Position --- src/bin/step1.rs | 92 ++++++++++++++++-- src/bin/step1a_recursive.rs | 93 +++++++++++++++++++ .../step1b_recursive_range_close_to_open.rs | 73 +++++++++++++++ src/bin/step2.rs | 81 ++++++++++++++-- src/bin/step2a.rs | 24 +++++ src/bin/step3.rs | 44 +++++++-- 6 files changed, 384 insertions(+), 23 deletions(-) create mode 100644 src/bin/step1a_recursive.rs create mode 100644 src/bin/step1b_recursive_range_close_to_open.rs create mode 100644 src/bin/step2a.rs diff --git a/src/bin/step1.rs b/src/bin/step1.rs index d640da2..6dde443 100644 --- a/src/bin/step1.rs +++ b/src/bin/step1.rs @@ -8,26 +8,104 @@ // 正解したら終わり /* + 問題の理解 + - ソート済の重複のない整数からなる配列numsと整数targetが与えられる。 + numsにtargetが含まれる場合、numsのindexを返す。 + numsにtargetが含まれない場合、numsがソートされた状態でtargetをnumsのどの位置に挿入するべきかのindexを返す。 + - nums[1,3] target=2 output=1 nums[1]にtarget=2を挿入すればソートされた状態が維持できる + 時間計算量がO(log n)になるようなアルゴリズムで実装する必要がある。つまり、線形探索は使えない。 + 何がわからなかったか - - + - binary searchをしながら、元の配列に対応するインデックスをうまく取り回す方法 何を考えて解いていたか - - + - binary searchの時間計算量はO(log n)になるので、binary searchを実装する。 + - ちなみにRustでは似たような処理を行うiter::insert_positionといった感じで存在すると思うので後で実装を見てみる。 + - binary searchは実装したことが無いので何をしているのか整理する。 + - numsの真ん中のインデックス(middle)を取得する。 + - target <= nums[middle] のように比較してtargetがどちらの集合にありそうか判断して、該当しない方は捨てる。 + - binary searchするために2つに分ける。nums[0..middle],nums[middle..nums.len()] + - nums.len() == 2 になるまで行う。 + + - 再帰で解けそうな感じがする。 + - 基本ケース + - nums.len() == 1 であれば target == nums[0] target <= nums[0] + でインデックスまたは挿入位置を特定する。 + - 再帰ケース + - target <= nums[middle] で targetが含まれていると思われるnumsのスライスを引数に再帰に入る。 + ここで手が止まったので答えを見る - 想定ユースケース - - + 解法の写経と理解 + https://leetcode.com/problems/search-insert-position/solutions/7169834/search-insert-position-binary-search-for-0hjb/ + - start,endで見ている区間の開始位置、終了位置を管理している。 + - start,endの区間で見るものがなくなるまで繰り返しを行っている。 + - 区間の真ん中の値(middle_value)とtargetの値を比較して等しければindexを返している。 + - target と middle_valueを比較して、targetがmiddleで分けたときの区間の左側、右側どちらかにあるかを確認している。 + - 区間の開始、終了を表す変数start,endをmiddleを基準にずらすことで、log n の範囲に区間を絞り込んでいる。 + - 最後にstartをそのまま返しているのは直感的でないと感じた。 + - target と middle_value の比較 + - targetの方が小さい場合はmiddle - 1 をendに更新している + - targetの方が大きい場合はmiddle + 1 をstartに更新している + - targetがnums[i]のいずれよりも小さい時、startは動かないので0となり、targetはnums[0]の位置に挿入される。 + - targetがnums[i]のいずれかより大きい時、startは常にnums[i] < target の位置を指し示す。 + - nums[middle] == targetであれば早期リターンする。見つからない時はstartが指し示すインデックスを返せば良いというロジックになっている。 正解してから気づいたこと - - + - 解法のコードは自然に理解できた。解法自体は近いところまで考えれていた気がするが、コードで表現する所までは距離があった。 + - このコードは計算量や記述のシンプルさから無駄がないように見える。最適解だと思った。 + - 練習として再帰に書き換えてみる。step1a_recursive.rs + + n = nums.le() + 時間計算量: O(log n) + 空間計算量: O(1) + */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + let mut start = 0; + let mut end = nums.len() as i32 - 1; + + while start <= end { + let middle = (start + end) / 2; + let middle_value = nums[middle as usize]; + + if middle_value == target { + return middle; + } + if target < middle_value { + end = middle - 1; + continue; + } + + start = middle + 1; + } + + start + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step1_test() {} + fn playground() { + let nums = vec![1, 2]; + let empty: Vec = vec![]; + + assert_eq!(empty, &nums[0..0]); + assert_eq!(vec![1], &nums[0..1]); + assert_eq!(vec![2], &nums[1..nums.len()]); + } + + #[test] + fn step1_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } } diff --git a/src/bin/step1a_recursive.rs b/src/bin/step1a_recursive.rs new file mode 100644 index 0000000..7bdc8b3 --- /dev/null +++ b/src/bin/step1a_recursive.rs @@ -0,0 +1,93 @@ +// Step1a_recursive +// 目的: 別実装(再帰)への書き換えによる解法理解度の確認と実装練習 + +/* + 問題の理解 + - ソート済の重複のない整数からなる配列numsと整数targetが与えられる。 + numsにtargetが含まれる場合、numsのindexを返す。 + numsにtargetが含まれない場合、numsがソートされた状態でtargetをnumsのどの位置に挿入するべきかのindexを返す。 + - nums[1,3] target=2 output=1 nums[1]にtarget=2を挿入すればソートされた状態が維持できる + 時間計算量がO(log n)になるようなアルゴリズムで実装する必要がある。つまり、線形探索は使えない。 + + 何がわからなかったか + - 再帰の基本ケースで end < start とするところを end <= start としてしまった。 + - なぜ間違えたのか分からず end < start に直感的に修正していしまったので整理する。GPT-5.1の学習サポートモードに質問して理解を進める。 + - まずこの実装が扱っているのは閉区間[start,end]。つまり、start,end両方とも区間に含める。 + - 再帰に入るときの呼び出し方、再帰の基本ケースで分かる。 + - [start,middle - 1]と[middle+1,end]という呼び出しになっている。 + - ここで middle - 1,middle + 1 に注目するとmiddleが重複しないような呼び出しとなっている。 + 閉区間だと値それ自体を含むので、[start,middle],[middle,end]とすると値が重複する。 + - 再帰の基本ケースで enc < start としている。 + - 閉区間[start,end]を扱っているので、start == end [start..=end] は要素を1つ含む。 + つまり、end <= start とすると、要素がまだ残っているのにも関わらず、基本ケースでstartを返してしまい誤りとなる。 + end < start のとき、startがendを超えていることから、この区間で表せる範囲にもう見るべき値が残っていないので、startを返す。 + - 再帰関数呼び出しの初期状態がnums.len() - 1 であることからも配列全体を示すときの区間の表し方として閉区間であることが分かる。[0..=nums.len() - 1]となる。 + + 区間について + https://w3e.kanazawa-it.ac.jp/math/category/other/syuugou/henkan-tex.cgi?target=/math/category/other/syuugou/kukann.html&list=1 + + 何を考えて解いていたか + - 再帰処理で実装するにはどうするか考える。 + - 基本ケース + - target == nums[middle] return middle + - end <= start return start <- これは間違いで end < start が正しい + - 再帰ケース + - middleの計算 start + end / 2 + - target < nums[middle] then end = middle - 1 else start = middle + 1 + - 更新したstart,endを利用して再帰処理に入る + + 正解してから気づいたこと + - 再帰処理の基本ケース条件を間違えて気付いたが、区間が値自体を含む閉区間なのか、含まない開区間なのかしっかり分かっていないと危うい。 + - 再帰処理にするのが難しいのではなくて、区間の開始、終了だけをずらす解法に思い至るまでに距離を感じる。 + - step1.rs,本ステップの実装ともに閉区間による処理を行っている。RustのRange記法では&nums[a..b]としたときに区間[a..b)となり、left-close right-openとなる。 + 閉区間ではなく、左閉区間右開区間とするのが普通だということだろうか?ということが気になったのでGPT-5.1に聞いてみる。 + - Off-by-oneエラーを避けるため + - &nums[a..b]がleft-close,right-openとなった根拠ではなさそうだが、&nums[a..b]のようなRange記法が[a,b)となるのは自然な感じがする。 + 閉区間[a..b]とした場合、&nums[nums.len()]が範囲外アクセスとなりOff-by-oneエラーになるため。 + 配列境界アクセス関連のエラーにOff-by-oneエラーという名前がついていること初めて知った。 + https://ja.wikipedia.org/wiki/Off-by-one%E3%82%A8%E3%83%A9%E3%83%BC + こうやって見るとbinary searchを実装するときの区間の扱い方としては、left-close,right-openな半開区間が自然な気がするので、このバージョンの実装を行う。 + + n = nums.le() + 時間計算量: O(log n) + 空間計算量: O(1) +*/ + +pub struct Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + Self::search_insert_position(&nums, 0, nums.len() as i32 - 1, target) + } + + fn search_insert_position(nums: &[i32], start: i32, end: i32, target: i32) -> i32 { + if end < start { + return start; + } + + let middle = (start + end) / 2; + let middle_value = nums[middle as usize]; + if middle_value == target { + return middle; + } + + if target < middle_value { + return Self::search_insert_position(nums, start, middle - 1, target); + } + return Self::search_insert_position(nums, middle + 1, end, target); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1a_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + assert_eq!(Solution::search_insert(vec![1, 3], 2), 1); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } +} diff --git a/src/bin/step1b_recursive_range_close_to_open.rs b/src/bin/step1b_recursive_range_close_to_open.rs new file mode 100644 index 0000000..5db5f3f --- /dev/null +++ b/src/bin/step1b_recursive_range_close_to_open.rs @@ -0,0 +1,73 @@ +// step1b_recursive_range_close_to_open +// 目的: 左閉区間右開区間(left-close,right-open)として区間を扱う実装を練習する。 + +/* + 問題の理解 + - ソート済の重複のない整数からなる配列numsと整数targetが与えられる。 + numsにtargetが含まれる場合、numsのindexを返す。 + numsにtargetが含まれない場合、numsがソートされた状態でtargetをnumsのどの位置に挿入するべきかのindexを返す。 + - nums[1,3] target=2 output=1 nums[1]にtarget=2を挿入すればソートされた状態が維持できる + 時間計算量がO(log n)になるようなアルゴリズムで実装する必要がある。つまり、線形探索は使えない。 + + 何がわからなかったか + + 何を考えて解いていたか + - 区間を半開区間(left-close,right-open)として扱う時の再帰処理の設計 + 呼び出し時は[0..nums.len())となる。right-openなので区間に自身(nums.len())を含まないため。 + - 基本ケース + - start >= end return start 比較記号を数直線上の並びにするよりも、変数の並びを数直線上にしたほうが個人的に分かりやすい + - start ~ end間に値があるかどうかで考える。left-close,right-openなので、start == end になると区間に値がなくなる。 + [0..0)の間に値はない。 + - target = nums[middle] return middle + - 再帰ケース + target < nums[middle] then [start..middle) else [middle+1..end) <-ここで target == nums[middle]ではないことが確定しているのでmiddle+1としてmiddle自体を除いている。 + + 正解してから気づいたこと + - start,endをleft,rightに書き換えて見たがしっくりこなかったのでstart,endに戻した。start,endの方が単語の違いが視覚的に分かりやすい感じ。 + - 区間を扱う時特に理由が無ければ半開区間(right-close,left-open)が良さそう。 + - RustのRange記法[a..b]も半開区間(a..b] + - https://doc.rust-lang.org/std/ops/struct.Range.html + - Pythonのrange型も半開区間(a..b] + - https://docs.python.org/ja/3/library/stdtypes.html#typesseq-range + +*/ + +pub struct Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + Self::search_insert_position(&nums, 0, nums.len() as i32, target) + } + + fn search_insert_position(nums: &[i32], start: i32, end: i32, target: i32) -> i32 { + if start >= end { + return start; + } + + let middle = (start + end) / 2; + let middle_value = nums[middle as usize]; + if middle_value == target { + return middle; + } + + if target < middle_value { + return Self::search_insert_position(nums, start, middle, target); + } + + return Self::search_insert_position(nums, middle + 1, end, target); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1b_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + assert_eq!(Solution::search_insert(vec![1, 3], 2), 1); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs index e92520d..b745d71 100644 --- a/src/bin/step2.rs +++ b/src/bin/step2.rs @@ -12,26 +12,89 @@ // 改善する時に考えたこと /* - 講師陣はどのようなコメントを残すだろうか? - - - 他の人のコードを読んで考えたこと - - + - 二分探索のコメント集 + https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.c15qprmvxkc2 + - 35. Search Insert Positionのコメント集 + https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.e13uiztrq2u9 + - 二分探索関連のコメントが豊富 + https://github.com/Ryotaro25/leetcode_first60/pull/45 + https://github.com/seal-azarashi/leetcode/pull/38 + https://github.com/Fuminiton/LeetCode/pull/41#discussion_r2080995529 + + - 値が大きい場合のオーバーフローについて。 + https://github.com/Ryotaro25/leetcode_first60/pull/45#discussion_r1878268512 + 問題を解いている時に意識の中になかった。C++でint型の値を扱っている。 + (start + end) / 2 について start + (end - start) / 2 としてオーバーフローを避けるべきという内容。 + endをnums.len()から持ってきている。numsのサイズが非常に大きい場合にendはintの上限まで膨らむ可能性がある。 + つまり、endがINT::MAXみたいな感じになった時に、startを加算するとオーバーフローするという内容だと理解した。 + 自分のコードにも当てはまる指摘なので、修正する。 + それにしても、(start + end) / 2 を見た時にオーバーフローするかも知れないと気付けても、start + (end - start) / 2 にしようと思いつけないと思った。 + 数学的なテクニックを感じた。(start + end) / 2 を代数変形すると start + (end - start) / 2 で等価になる。今回この方向の考え方に触れられたので、いつか思い出して使えそうだと思った。 + start + (end - start) / 2 でやっていることとして + - オーバーフローしないよう先に減少する方向の演算を行う。 + - (end - start) / 2 + - 前の項の計算結果は 1 / 2 以下になっており start を加算してもオーバーフローしない。 + start = 1 end = 3 middle = 2 + - (start + end) / 2 -> (1 + 3) / 2 = 4 / 2 = 2 + - start + (end - start) / 2 -> 1 + (3 - 1) / 2 = 1 + 2 / 2 = 1 + 1 = 2 + 単純に式として見ると代数変形したところで結果は変わらないが、コンピュータに計算させることを前提とすると結果が変わる(オーバーフローの有無)式になることが面白いと思った。 - 他の想定ユースケース - - + - どこか(見失った)のコメントからたどり着いた、ソートされていない大きなデータ10GBの中央値を2GB以内の空間計算量で求めるには?といったstack overflowの質問。 + 時間を書けて調べれば解法が何を言っているのかわかりそうな気もするが時間切れなのでメモのみ。 + https://stackoverflow.com/questions/3572640/interview-question-find-median-from-mega-number-of-integers/3576479 改善する時に考えたこと - - + - step1.rsでは閉区間で区間を扱っていたが、半開区間(a..b]で区間を扱うように変更。特に理由が無ければOff-by-oneエラーを避けるために半開区間(left-close,right-open)を使った方が良さそう。 + - middle = (start + end) / 2 についてオーバーフローしないよう middle = start + (end - start) / 2 とする。 + + 所感 + - Rustではpartition_pointメソッドで同じことができそう。 + https://doc.rust-lang.org/std/primitive.slice.html#method.partition_point + 実装はbinary_search_byメソッドのWrapperという感じ。 */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + let mut start = 0; + let mut end = nums.len(); + + // left-close,right-open + // [0..1) の時、区間内に値が残っている + // [0..0) の時、区間内に値が残っていない + while start < end { + let middle = start + (end - start) / 2; + let middle_value = nums[middle]; + if middle_value == target { + return middle as i32; + } + + if target < middle_value { + // 右開区間(right-open)であり、その値自体を含まないのでそのままmiddleを代入 + end = middle; + continue; + } + + // 左閉区間(left-close)であり、その値自身を含むのでmiddle自体をスキップするためにmiddle + 1を代入 + start = middle + 1; + } + + start as i32 + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step2_test() {} + fn step2_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + assert_eq!(Solution::search_insert(vec![1, 3], 2), 1); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } } diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..40f2f6b --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,24 @@ +// Step2a +// 目的: 言語が提供する標準ライブラリを利用した解法 + +pub struct Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + nums.partition_point(|num| *num < target) as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + assert_eq!(Solution::search_insert(vec![1, 3], 2), 1); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs index 0af0a4a..5cf0e82 100644 --- a/src/bin/step3.rs +++ b/src/bin/step3.rs @@ -9,23 +9,53 @@ // 作れないデータ構造があった場合は別途自作すること /* - 時間計算量: - 空間計算量: + n = nums.len() + 時間計算量: O(log n) + 空間計算量: O(1) */ /* - 1回目: 分秒 - 2回目: 分秒 - 3回目: 分秒 + 1回目: 2分26秒 + 2回目: 1分51秒 + 3回目: 2分18秒 */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + let mut start = 0; + let mut end = nums.len(); + + while start < end { + let middle = start + (end - start) / 2; + let middle_value = nums[middle]; + if middle_value == target { + return middle as i32; + } + + if target < middle_value { + end = middle; + continue; + } + + start = middle + 1; + } + + start as i32 + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step3_test() {} + fn step3_test() { + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 5), 2); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 2), 1); + assert_eq!(Solution::search_insert(vec![1, 3, 5, 6], 7), 4); + assert_eq!(Solution::search_insert(vec![1, 3], 2), 1); + + assert_eq!(Solution::search_insert(vec![], 8), 0); + } } From a909060b6863946f9fe035a88eafc85b325cfa6c Mon Sep 17 00:00:00 2001 From: t9a Date: Fri, 26 Dec 2025 09:35:55 +0900 Subject: [PATCH 2/2] =?UTF-8?q?add:=20=E4=BA=8C=E5=88=86=E6=8E=A2=E7=B4=A2?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=82=B4=E3=83=AA=E3=82=BA=E3=83=A0=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E7=B7=B4=E7=BF=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/step4_lower_bound.rs | 272 ++++++++++++++++++++++++++++++++++ src/bin/step5_upper_bound.rs | 275 +++++++++++++++++++++++++++++++++++ 2 files changed, 547 insertions(+) create mode 100644 src/bin/step4_lower_bound.rs create mode 100644 src/bin/step5_upper_bound.rs diff --git a/src/bin/step4_lower_bound.rs b/src/bin/step4_lower_bound.rs new file mode 100644 index 0000000..62d599d --- /dev/null +++ b/src/bin/step4_lower_bound.rs @@ -0,0 +1,272 @@ +// Step4 +// 目的: lower_bound(下限)を探す二分探索アルゴリズムの実装 + +/* + 方法 + - lower_boundについて考えられる全ての区間を扱って実装を行う + - target == nums[i] による早期リターンは行わない方針とする + - 実装を行う際に考えるポイントなどをまとめる + + 実装するときの考え方まとめ + - lower_bound: target <= nums[i] となる最小のiを返す + 配列を nums[i] < target, target <= nums[i] に分ける境界の位置を探す + - start側を nums[i] < target + - end側を target <= nums[i] + - ループ条件は「未確定領域が空でない」という考え方をする。(見るべき未確定領域が残っているか) + - ループ条件は区間から決められる。 + - 閉区間 [start,end] start <= i <= end + while start <= end + - 開区間 (start,end) start < i < end + while 1 < end - start + - 半開区間 + - [start,end) start <= i < end + while start < end + - (start,end] start < i <= end + while start + 1 < end : start側が開区間でstart自体を含まないので+1 + - 2ポインタ(startとend)の更新 + - nums[i]を未確定領域に含めるかどうかを考える。 + - lower_boundのとき nums[i] < target である nums[i] は答えの候補(未確定領域)に含めない。target未満であるので答えになりえない。 + - lower_boundのとき target <= nums[i] である nums[i] は答えの候補(未確定領域)に含める。target以上であるので答えになりえる。 + - ポインタ更新時に+-1するかどうか + - 探索候補に含めないとき i が区間に含まれないようにする。 + - 端点が開いているか閉じているかに気をつける。 + - 端点から+-1が決まるわけではなく、端点を未確定領域に含めたいかどうかが先にあって、そのあとに端点の状態を見て+-1するか決める。 + - 無限ループが発生しないかを確認する + - 未確定領域に含まれる要素が1つの時の最小ケースを用意して、無限ループにならないか(ループ毎に未確定領域が縮小するか)をチェックする + + 所感 + - 最初にソートされた適当な配列[1,3,5,7]を考えて、欲しい答えを考えた時に境界をどこに置くのか考えると分かりやすく感じる。 + lower_boundにおいて、target <= num[i] を満たす最小のiを探す時の境界を | で表している。 + target = 7のとき [1,3,5|7] + target = 4のとき [1,3|5,7] + target = 5のとき [1,3|5,7] + [ nums[i] < target | target <= nums[i] ]といった感じで境界で分けている。 + + - left-open,right-closeな半開区間が難しく感じる。 + - 探索範囲が空になるまでループを回すと無限ループになる + - 探査範囲に要素が1つ残った状態で探索を終了して、最後に値をチェックしてから返す必要がある。 + - upper_boundも練習して、境界が変わった時に実装できるかで理解度チェックする。 +*/ + +pub struct Solution {} +impl Solution { + pub fn search_insert_close_to_close_interval(nums: Vec, target: i32) -> i32 { + // [start,end] + // start <= i <= end + let mut start = 0; + let mut end = nums.len() as isize - 1; + + while start <= end { + let middle = start + (end - start) / 2; + + if nums[middle as usize] < target { + // middleの位置にある値は答えになりえないので探索候補から外す。 + // startは middle < target となる境界の1つ右側を常に指し示すので、lower_boundになっている。 + start = middle + 1; + } else { + /* + middleの位置にある値middle_valueは答えになりうるので探索候補に含めたい(end = middle)が、 + 探索範囲が縮小しなくなることを避けるためにend = middle - 1としている。 + */ + end = middle - 1; + } + } + + start as i32 + } + + pub fn search_insert_close_to_open_interval(nums: Vec, target: i32) -> i32 { + // [start,end) + // start <= i < end + let mut start = 0; + let mut end = nums.len() as isize; + + while start < end { + let middle = start + (end - start) / 2; + + if nums[middle as usize] < target { + start = middle + 1; + } else { + end = middle; + } + } + + end as i32 + } + + pub fn search_insert_open_to_close_interval(nums: Vec, target: i32) -> i32 { + // (start,end] + // start < i <= end + if nums.is_empty() { + return 0; + } + + let mut start = -1; + // endを閉じた端点として扱う練習をしているので、end = nums.len() as isize と番兵を使わずに実装している。 + let mut end = nums.len() as isize - 1; + + // 未確定領域に要素が1つ残った状態で探索が終了しないと無限ループする + while start + 1 < end { + let middle = start + (end - start) / 2; + + if nums[middle as usize] < target { + start = middle; + } else { + end = middle; + } + } + + if nums[end as usize] < target { + return (end + 1) as i32; + } + + end as i32 + } + + pub fn search_insert_open_to_open_interval(nums: Vec, target: i32) -> i32 { + // (start,end) + // start < i < end + let mut start = -1; + let mut end = nums.len() as isize; + + while 1 < end - start { + let middle = start + (end - start) / 2; + + if nums[middle as usize] < target { + start = middle; + } else { + end = middle; + } + } + + end as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step4d_close_to_close_interval_test() { + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![1, 3, 5, 6], 5), + 2 + ); + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![3, 5], 3), + 0 + ); + + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!( + Solution::search_insert_close_to_close_interval(vec![], 8), + 0 + ); + } + + #[test] + fn step4d_close_to_open_interval_test() { + assert_eq!( + Solution::search_insert_close_to_open_interval(vec![1, 3, 5, 6], 5), + 2 + ); + assert_eq!( + Solution::search_insert_close_to_open_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::search_insert_close_to_open_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::search_insert_close_to_open_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![3, 5], 3), + 0 + ); + + assert_eq!( + Solution::search_insert_close_to_open_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::search_insert_close_to_open_interval(vec![], 8), 0); + } + + #[test] + fn step4d_open_to_close_interval_test() { + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![1, 3, 5, 6], 5), + 2 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![3, 5], 3), + 0 + ); + + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::search_insert_open_to_close_interval(vec![], 8), 0); + } + + #[test] + fn step4d_open_to_open_interval_test() { + assert_eq!( + Solution::search_insert_open_to_open_interval(vec![1, 3, 5, 6], 5), + 2 + ); + assert_eq!( + Solution::search_insert_open_to_open_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_open_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::search_insert_open_to_open_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::search_insert_open_to_close_interval(vec![3, 5], 3), + 0 + ); + + assert_eq!( + Solution::search_insert_open_to_open_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::search_insert_open_to_open_interval(vec![], 8), 0); + } +} diff --git a/src/bin/step5_upper_bound.rs b/src/bin/step5_upper_bound.rs new file mode 100644 index 0000000..b4e7125 --- /dev/null +++ b/src/bin/step5_upper_bound.rs @@ -0,0 +1,275 @@ +// Step5 +// 目的: 二分探索アルゴリズムの理解を深めるためにupper_bound(上限)を探す実装を行う + +/* + 方法 + - upper_boundについて考えられる全ての区間を扱って実装を行う + - target == nums[i] による早期リターンは行わない方針とする + - 実装を行う際に考えるポイントなどをまとめる + + 実装するときの考え方まとめ + - upper_bound: target < nums[i] となる最小のiを返す + 配列を nums[i] <= target,target < nums[i] に分ける境界の位置を探す + - start側を nums[i] <= target + - end側を target < nums[i] + - ループ条件は「未確定領域が空でない」という考え方をする。(見るべき未確定領域が残っているか) + - ループ条件は区間から決められる。 + - 閉区間 [start,end] start <= i <= end + while start <= end + - 開区間 (start,end) start < i < end + while 1 < end - start + - 半開区間 + - [start,end) start <= i < end + while start < end + - (start,end] start < i <= end + while start + 1 < end : start側が開区間でstart自体を含まないので+1 + - 2ポインタ(startとend)の更新 + - nums[i]を未確定領域に含めるかどうかを考える。 + - upper_boundのとき target < nums[i] である nums[i] は答えの候補(未確定領域)に含める。targetを超えるので答えになりえる。 + - upper_boundのとき nums[i] <= target である nums[i] は答えの候補(未確定領域)に含めない。target以下であるので答えになりえない。 + - ポインタ更新時に+-1するかどうか + - 探索候補に含めないとき i が区間に含まれないようにする。 + - 端点が開いているか閉じているかに気をつける。 + - 端点から+-1が決まるわけではなく、端点を未確定領域に含めたいかどうかが先にあって、そのあとに端点の状態を見て+-1するか決める。 + - 無限ループが発生しないかを確認する + - 未確定領域に含まれる要素が1つの時の最小ケースを用意して、無限ループにならないか(ループ毎に未確定領域が縮小するか)をチェックする + + 所感 + - 最初にソートされた適当な配列[1,3,5,7]を考えて、欲しい答えを考えた時に境界をどこに置くのか考えると分かりやすく感じる。 + upper_boundにおいて、target < num[i] を満たす最小のiを探す時の境界を | で表している。 + target = 7のとき [1,3,5,7|] + target = 4のとき [1,3|5,7] + target = 5のとき [1,3,5|7] + [ nums[i] <= target | target < nums[i] ]といった感じで境界で分けている。 + + - left-open,right-closeな半開区間は考えることが多く、バグを埋め込みやすいと思った。 + この区間を扱わざるをえない状況がすぐに思いつかないが、可能であればleft-close,right-openな区間として扱うように番兵を利用するなどして区間を調整するのが良さそう。 + - ロジックを実装した後にコメントアウトで小さいテストケース // [1,3,5] target=5) を書いて、無限ループしないかどうか頭の中で状態遷移を追いながらデバッグして動作確認をしていた。 + ホワイトボードを用いたコーディングテストを考えると、この方法ができれば大丈夫そうだと思った。 + - left-close,right-openな区間として扱うと、探しているupper_boudの条件とポインタの更新が対になっていて分かりやすいと感じる。 + 具体的には target < nums[i] を満たす最小のiを探していて、これを満たすiを見つけるたびにendポインタに入れているので、探索が終わったときにendが答え(upper_bound)になるのが分かりやすいという感覚。 + Rustで範囲を表すときにnums[0..i]とするとleft-close,right-openな範囲[0,i)になるが、扱いやすいからこうしているのかなどと思った。 +*/ + +pub struct Solution {} +impl Solution { + /* + 以下のコードは二分探索アルゴリズム学習のために実装しており、LeetCodeの問題(35.Search Insert Position)とは関係ありません。 + */ + pub fn upper_bound_close_to_close_interval(nums: Vec, target: i32) -> i32 { + // [start,end] + // start <= i <= end + let mut start = 0; + let mut end = nums.len() as isize - 1; + + while start <= end { + let middle = start + (end - start) / 2; + + if target < nums[middle as usize] { + // targetを超えるnums[middle]は答え候補に残したいので、end = middle としたいが、探索範囲を減少させるために end = middle - 1としている。 + // startが常にtarget以下となる境界の次を指すので、target < nums[middle] を満たす最小のmiddleとなり、uppper_boundとなる。 + end = middle - 1; + } else { + // nums[middle] <= target + start = middle + 1; + } + } + + start as i32 + } + + pub fn upper_bound_close_to_open_interval(nums: Vec, target: i32) -> i32 { + // [start,end) + // start <= i < end + let mut start = 0; + let mut end = nums.len() as isize; + + while start < end { + let middle = start + (end - start) / 2; + + if target < nums[middle as usize] { + end = middle; + } else { + // nums[middle] <= target + start = middle + 1; + } + } + + end as i32 + } + + pub fn upper_bound_open_to_close_interval(nums: Vec, target: i32) -> i32 { + // (start,end] + // start < i <= end + if nums.is_empty() { + return 0; + } + + let mut start = -1; + let mut end = nums.len() as isize - 1; + + // 未確定領域に要素が1つ残った状態で探索を終了しないと無限ループする + while start + 1 < end { + let middle = start + (end - start) / 2; + + if target < nums[middle as usize] { + end = middle; + } else { + // nums[middle] <= target + start = middle + 1; + } + } + + // target < nums[end] を満たす最小のendであればそのまま帰す。 + if target < nums[end as usize] { + return end as i32; + } + + // nums[end] <= target の範囲にあるので、境界を超えたところがupper_boundになる + (end + 1) as i32 + } + + pub fn upper_bound_open_to_open_interval(nums: Vec, target: i32) -> i32 { + // (start,end) + // start < i < end + let mut start = -1; + let mut end = nums.len() as isize; + + while 1 < end - start { + let middle = start + (end - start) / 2; + + if target < nums[middle as usize] { + end = middle; + } else { + start = middle; + } + } + + end as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step5_close_to_close_interval_test() { + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![1, 3, 5, 6], 5), + 3 + ); + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![3, 5], 3), + 1 + ); + + assert_eq!( + Solution::upper_bound_close_to_close_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::upper_bound_close_to_close_interval(vec![], 8), 0); + } + + #[test] + fn step5_close_to_open_interval_test() { + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![1, 3, 5, 6], 5), + 3 + ); + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![3, 5], 3), + 1 + ); + + assert_eq!( + Solution::upper_bound_close_to_open_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::upper_bound_close_to_open_interval(vec![], 8), 0); + } + + #[test] + fn step5_open_to_close_interval_test() { + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![1, 3, 5, 6], 5), + 3 + ); + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![3, 5], 3), + 1 + ); + + assert_eq!( + Solution::upper_bound_open_to_close_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::upper_bound_open_to_close_interval(vec![], 8), 0); + } + + #[test] + fn step5_open_to_open_interval_test() { + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![1, 3, 5, 6], 5), + 3 + ); + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![1, 3, 5, 6], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![1, 3, 5, 6], 7), + 4 + ); + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![1, 3], 2), + 1 + ); + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![3, 5], 3), + 1 + ); + + assert_eq!( + Solution::upper_bound_open_to_open_interval(vec![1, 3, 5, 6], 0), + 0 + ); + assert_eq!(Solution::upper_bound_open_to_open_interval(vec![], 8), 0); + } +}