Conversation
| - targetの方が大きい場合はmiddle + 1 をstartに更新している | ||
| - targetがnums[i]のいずれよりも小さい時、startは動かないので0となり、targetはnums[0]の位置に挿入される。 | ||
| - targetがnums[i]のいずれかより大きい時、startは常にnums[i] < target の位置を指し示す。 | ||
| - nums[middle] == targetであれば早期リターンする。見つからない時はstartが指し示すインデックスを返せば良いというロジックになっている。 |
There was a problem hiding this comment.
いいえ。5分程度考えましたがよく分かりませんでした。早期リターンしない場合は区間を全部見終わった時(while loopの終了)のstart又はendの中身から戻り値を決定できそうだという感覚のみがあります。
他のレビューコメントでも自身の書いたコードやコメントから二分探索の理解、練習不足を指摘するものがあるので、次の問題に進む前に一度立ち止まって追加の練習を行います。
https://github.com/t9a-dev/LeetCode_arai60/pull/41/files#r2606203505
There was a problem hiding this comment.
改めてstep1.rsのコードを見るとstartがupper_boundを返す条件になっていることに気付きました。このコードのまま早期リターン無しで書くと探索終了後にstartの直前が有効な添字かつ、target以上であればstart - 1をlower_boundとして扱えます。
pub fn search_insert(nums: Vec<i32>, 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はupper_boundを指している。
// 問題の制約としてnumsは重複しないのでupper_boundの1つ左がtarget以上であればlower_boundとして返せる。
let previous_upper_bound = start - 1;
if 0 <= previous_upper_bound && target <= nums[previous_upper_bound as usize] {
return previous_upper_bound as i32;
}
start as i32
}最初からlower_boundを探すのであれば以下のように書けると理解しました。
lower_bound: target <= nums[i] を満たす最小のi
pub fn search_insert(nums: Vec<i32>, 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
}ありがとうございました。
| if target < middle_value { | ||
| end = middle - 1; | ||
| continue; | ||
| } | ||
|
|
||
| start = middle + 1; |
There was a problem hiding this comment.
自分はここif-elseで書きたいですね。場合分けの気持ちで書いており、また対比させた方が読みやすいと思うからです。
| - 区間を扱う時特に理由が無ければ半開区間(right-close,left-open)が良さそう。 | ||
| - RustのRange記法[a..b]も半開区間(a..b] | ||
| - https://doc.rust-lang.org/std/ops/struct.Range.html | ||
| - Pythonのrange型も半開区間(a..b] |
There was a problem hiding this comment.
半開区間 a <= x < b を[a, b)と書くので丸括弧と角括弧の使い方が逆になってる気がします。
| 何を考えて解いていたか | ||
| - 区間を半開区間(left-close,right-open)として扱う時の再帰処理の設計 | ||
| 呼び出し時は[0..nums.len())となる。right-openなので区間に自身(nums.len())を含まないため。 | ||
| - 基本ケース |
There was a problem hiding this comment.
基本ケースという言葉は見たことがないです。
アルゴリズムイントロダクションの分割統治の章では、base case(基底段階)とrecursive case(再帰段階)という言葉が使われていました。
またWikipediaのRecursionを引くと、base caseとrecursive stepが使われていたので、許容される表記揺れはこのくらいだと思います。
| if target < middle_value { | ||
| // 右開区間(right-open)であり、その値自体を含まないのでそのままmiddleを代入 | ||
| end = middle; | ||
| continue; | ||
| } | ||
|
|
||
| // 左閉区間(left-close)であり、その値自身を含むのでmiddle自体をスキップするためにmiddle + 1を代入 | ||
| start = middle + 1; |
There was a problem hiding this comment.
これらのコメントも間違っているわけではないですが、言ってしまえば説得力がやや弱いです。さらにループ不変条件や、この区間設定からループ継続条件がstart < endになることの導出、二分探索の正当性を説明できるところまでがSWEの常識に含まれると思います。
こちら、odaさんが最近書かれた記事の一節もよければ参考にされてください。
There was a problem hiding this comment.
あと、いつものコメント集ですね。
自分語りで恐縮なんですが、二分探索について自分はここのコメント集を全て漁った上で3時間ほどLLMに壁打ちし、異なる9パターン実装してだいたい把握できました。
誰もやらない(left, right]などの不確定域の区間設定や、あえてupper bound型の二分探索にしたり、過剰にkey空間を抽象化したりとさまざまなわかりにくいコードを意図的に書く練習をしたのが自分は効きました。
There was a problem hiding this comment.
さまざまなわかりにくいコードを意図的に書く練習をした
これは練習において何をするべきかよく分かっていますね。ボール球を意図的に投げてみないとストライクは投げられないです。
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { |
There was a problem hiding this comment.
二分探索もやっている処理は再帰的である、と捉えればこのような書き方もできるのですね。勉強になりました。
| let mut start = 0; | ||
| let mut end = nums.len(); | ||
|
|
||
| // left-close,right-open |
There was a problem hiding this comment.
imo: 区間の話を書くよりも、"探しているものをどんなものだと捉えているか"と、"それを探すにあたってleftやrightをどんな意味で使っているか"」を書くと、どんなことを考えているのか読み手に伝わりやすいかも知れません。
問題: 35. Search Insert Position
次に解く問題: 153. Find Minimum in Rotated Sorted Array
ファイルの構成:
./src/bin/<各ステップ>.rs