-
Notifications
You must be signed in to change notification settings - Fork 0
solve: 153.Find Minimum in Rotated Sorted Array #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
t9a-dev
wants to merge
1
commit into
main
Choose a base branch
from
153.Find-Minimum-in-Rotated-Sorted-Array
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Step1 | ||
| // 目的: 方法を思いつく | ||
|
|
||
| // 方法 | ||
| // 5分考えてわからなかったら答えをみる | ||
| // 答えを見て理解したと思ったら全部消して答えを隠して書く | ||
| // 5分筆が止まったらもう一回みて全部消す | ||
| // 正解したら終わり | ||
|
|
||
| /* | ||
| 問題の理解 | ||
| - 昇順にソートされた一意の整数を含む配列を回転させたものが入力として与えられる。配列中に含まれる最小の値を答えとして返す。 | ||
| - 時間計算量O(log n)のアルゴリズムで実装することが制約。 | ||
| - 時間計算量O(log n)を満たすことが必須なので、入力の長さなどから事前に時間計算量を見積もる必要はない。 | ||
| - 回転させた配列について | ||
| - 4回転のとき [0,1,2,4,5,6,7] -> [4,5,6,7,0,1,2] | ||
| - 7回転のとき [0,1,2,4,5,6,7] -> [0,1,2,4,5,6,7] | ||
|
|
||
| 何を考えて解いていたか | ||
| - 時間計算量O(log n)を満たすために二分探索アルゴリズムで実装する必要がある。 | ||
| - しかし、入力の配列はn回回転したものが渡され、昇順ソートされている状態とは限らない。 | ||
| - 最小値を保持する変数を作っておき、探索を行いながら最小値を見つけるたびに更新する。探索終了後に最小値を返す。 | ||
| - ソート処理を実行するとO(n log n)となり、問題の制約を満たせない。 | ||
| 時間切れなので答えを見る | ||
|
|
||
| 何がわからなかったか | ||
| - 何回回転したかが示されない状態で、入力をソートされた状態に戻す方法が分からなかった。 | ||
|
|
||
| 解答の理解 | ||
| https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/solutions/7178461/153-find-minimum-in-rotated-sorted-array-6m7g/?envType=problem-list-v2&envId=vnnqktms | ||
| - ソートされている配列を回転しても、ある区間(まとまり)はソートされている状態が保持されている。この点に注目している。 | ||
| - ある時点で見ている区間の左端の値 < 右端の値 が成立していれば、この区間の最小値が区間の左端であることが分かる。 | ||
| - 最初に配列全体がこの条件を満たしていれば、配列がソートされていることが分かるので、早期リターンnums[0]できる。 | ||
| - 右端の方が小さければ、最小値と比較して更新できるか試してから右端を飛ばして次の未探索領域を見る。 | ||
|
|
||
| 正解してから気づいたこと | ||
| - 解き方を知っているかどうかという感じがした。 | ||
| - 回転という操作に惑わされず、区間(まとまり)に注目してソートされている状態が維持されているという点に気付くことができるかどうか。 | ||
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { | ||
| pub fn find_min(nums: Vec<i32>) -> i32 { | ||
| if nums.is_empty() { | ||
| panic!("nums must not be empty"); | ||
| } | ||
|
|
||
| if nums.first().unwrap() <= nums.last().unwrap() { | ||
| return *nums.first().unwrap(); | ||
| } | ||
|
|
||
| // [start,end) | ||
| // start <= i < end | ||
| let mut start = 0; | ||
| let mut end = nums.len(); | ||
| let mut min_value = i32::MAX; | ||
|
|
||
| while start < end { | ||
| let middle = start + (end - start) / 2; | ||
| let middle_value = nums[middle]; | ||
| let start_value = nums[start]; | ||
|
|
||
| if start_value <= middle_value { | ||
| min_value = min_value.min(start_value); | ||
| start = middle + 1; | ||
| } else { | ||
| min_value = min_value.min(middle_value); | ||
| end = middle; | ||
| } | ||
| } | ||
|
|
||
| min_value | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn step1_test() { | ||
| assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); | ||
| assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); | ||
|
|
||
| assert_eq!(Solution::find_min(vec![1]), 1); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn step1_empty_nums_test() { | ||
| let empty_nums = Vec::new(); | ||
| Solution::find_min(empty_nums); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // Step2 | ||
| // 目的: 自然な書き方を考えて整理する | ||
|
|
||
| // 方法 | ||
| // Step1のコードを読みやすくしてみる | ||
| // 他の人のコードを2つは読んでみること | ||
| // 正解したら終わり | ||
|
|
||
| // 以下をメモに残すこと | ||
| // 講師陣はどのようなコメントを残すだろうか? | ||
| // 他の人のコードを読んで考えたこと | ||
| // 改善する時に考えたこと | ||
|
|
||
| /* | ||
| コメント集と他の人のコードを読んで考えたこと | ||
| https://github.com/sakupan102/arai60-practice/pull/43/changes/BASE..36167fcd716b107e83476e6b2f73821e91d5554e#diff-4ccb56a4631a2bd95d503978df1e47c6cc3893b0b74adc1393bc155f0ce88bc2R2 | ||
| - nums[i] > nums[i+1] となる境界を探すという考え方は思いつかなかった。別の解法として二分探索の良い練習になりそう。 | ||
|
|
||
| https://github.com/YukiMichishita/LeetCode/pull/7#discussion_r1561132164 | ||
| - 二分探索を実装するときの考え方や意識すると良いこと | ||
|
|
||
| https://github.com/Ryotaro25/leetcode_first60/pull/46/changes/BASE..5cd497a61c1610dfb252de6f0dd2a0823e7b2bec#r1869993674 | ||
| - odaさんによるオプショナルな質問。自分でも考えてみる。 | ||
| Q.> 「2で割る処理がありますがこれは切り捨てでも切り上げでも構わないのでしょうか。」 | ||
| A. 良くない。切り捨てにしておかないとend側が動かなくなり無限ループになるので、切り上げは不可。切り捨てにしておくとmiddleは左に寄っていくのでend = middle としても未確定領域が縮小するという感覚。 | ||
| Q.> 「nums[middle] <= nums[right] とありますが、これは < でもいいですか。」 | ||
| A. 良い。境界を探しているわけではなく、最小値そのものを探しているため。 | ||
| Q.> 「nums[right] は、nums.back() でもいいですか。」 | ||
| A. 良くない。endポインタを更新して最後尾が移動すること(未確定領域の縮小)を期待しているのでnums.last()では不変条件が壊れて無限ループになるため。 | ||
| Q.> 「right の初期値は nums.size() でもいいですか。」 | ||
| A. 良くない。自分のコードで、end = nums.len() に書き換えただけでは動かなくなる。配列の添字を見ることを想定して実装している。配列中の最小値を探したいのに配列外を参照しようとすると不変条件が破綻する。 | ||
|
|
||
| - 他の人のコードだと、最小値そのものを探すというよりは区間のまとまりの境界自体を探して答えを求めているコードが多いように見える。 | ||
| 自分がstep1で解答としてみたコードは最小値自体を探すものであったので、step2では境界を探すコードを書いたほうが二分探索の練習になりそう。 | ||
|
|
||
| https://github.com/olsen-blue/Arai60/pull/42/changes#r1993813625 | ||
| > odaさんのコメントをいただいた上で、再考してみましたが、境界を求める二分探索という整理ができそうであれば、rightの初期値がlen(nums)なのもかなり良いと感じるようになりました。 | ||
| > 一方で、欲しいもの一つ見つける二分探索は、rightの初期値はlen(nums) - 1が良いかもという感覚です。 | ||
| > 使い分けたいかもしれません。 | ||
| - 自分も同じ感想になった。 | ||
|
|
||
| odaさんのコメント | ||
| https://github.com/olsen-blue/Arai60/pull/42/changes#r1993204654 | ||
| > left を「左側、つまり、条件を満たさないことが判明している左側の最大の場所」として書くこともできます。 | ||
| > そうすると、初期値は -1 ですね。 | ||
| > この right も「条件を満たさないことが判明している右側の場所の最小」と思うと、right = middle が素直に見えるはずです。 | ||
|
|
||
| 改善する時に考えたこと | ||
| - 境界を探そうと思って、[start,end)として扱う初期条件を考えてコードを書き始めたがうまく行かなかった。 | ||
| そもそも問題で求められている(探したい)のは、配列中に必ず存在する最小値であって、境界ではないことが原因だと思った。 | ||
| なので[start,end)のようなend側が配列の外側を指すような区間の設定をすると素直に書けないと思った。 | ||
| 配列中の最小値を探す時にendが配列の外側を指していると、そこに値は無いのでend側の端点も閉区間にしてしまった方が良いという感覚。 | ||
| 実装するときの考え方として、境界ではなく配列中の添字と対応する値を見ていると考える方が自然だと思った。 | ||
|
|
||
| 所感 | ||
| - 何か特定の値を探す時は添字で直接配列中の値を見ていく考え方の方が自然だと感じた。 | ||
| lower_boundやupper_boundではtargetを境目として配列を分けるイメージなので境界を探すという感覚。 | ||
| - [start,end)な半開区間を扱うコードではうまく実装できず方針変更したが別で練習しておく。(step2a.rs) | ||
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { | ||
| pub fn find_min(nums: Vec<i32>) -> i32 { | ||
| if nums.is_empty() { | ||
| panic!("nums must not be empty"); | ||
| } | ||
|
|
||
| // [start,end] | ||
| // start <= i <= end | ||
| let mut start = 0; | ||
| let mut end = nums.len() - 1; | ||
|
|
||
| while start < end { | ||
| let middle = start + (end - start) / 2; | ||
|
|
||
| if nums[middle] < nums[end] { | ||
| end = middle; | ||
| } else { | ||
| start = middle + 1; | ||
| } | ||
| } | ||
|
|
||
| nums[end] | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn step2_test() { | ||
| assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); | ||
| assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); | ||
| assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); | ||
|
|
||
| assert_eq!(Solution::find_min(vec![1]), 1); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn step1_empty_nums_test() { | ||
| let empty_nums = Vec::new(); | ||
| Solution::find_min(empty_nums); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // Step2a | ||
| // 目的: 別の書き方で練習する。 | ||
|
|
||
| /* | ||
| odaさんのコメント | ||
| https://github.com/olsen-blue/Arai60/pull/42/changes#r1993204654 | ||
| > left を「左側、つまり、条件を満たさないことが判明している左側の最大の場所」として書くこともできます。 | ||
| > そうすると、初期値は -1 ですね。 | ||
| > この right も「条件を満たさないことが判明している右側の場所の最小」と思うと、right = middle が素直に見えるはずです。 | ||
|
|
||
| - step2.rsで書こうとしてうまく実装できなかった start = 0, end = nums.len() を初期値として実装する練習を行う。 | ||
| step2.rsの実装が自然に感じるが、あえて違和感のある方法で練習することで理解を深める狙い。 | ||
| https://github.com/t9a-dev/LeetCode_arai60/pull/41#discussion_r2607036540 | ||
|
|
||
| 解法の理解 | ||
| - 常に配列の最後の要素より小さいかでendを更新している | ||
| - 配列の最後は動かないのになぜこれで良いのだろうか。 | ||
| - 配列の最後は配列中の値の最大値にも最小値にもなり得る。 | ||
| - 配列外(nums.len())をより大きい値の集合の始まりとして見ている? | ||
| - つまり右側とは何か値の集合があると仮定して、nums.last()より大きい値しか無いという番兵の考え方だと理解した。 | ||
| [ nums[i] <= nums.last() | nums.last() < ] | ||
| - 左側により小さい値を見つけたらそこまで探索範囲を縮小している。nums[middle] <= nums.last() then end = middle | ||
| - <= としているのはnums.last()が最小値の可能性があるので。 | ||
| - 右側(end)により小さい値があれば、左側を捨てる。 nums.last() < nums[middle] then start = middle + 1 // middleを探索範囲外にする。 | ||
|
|
||
| 所感 | ||
| - nums.last()との比較でかなり面食らう感じがある。nums.last()は最大値、最小値、その他の値のどれでもあるのになぜ比較しているのか?と初見で思った。 | ||
| 自分には認知負荷が高い。 | ||
| (start,end]な半開区間も実装しようかと思ったが時間切れなのでスキップ。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { | ||
| pub fn find_min(nums: Vec<i32>) -> i32 { | ||
| if nums.is_empty() { | ||
| panic!("nums must not be empty"); | ||
| } | ||
|
|
||
| // [start,end) | ||
| // start <= i < end | ||
| let mut start = 0; | ||
| let mut end = nums.len(); | ||
|
|
||
| while start < end { | ||
| let middle = start + (end - start) / 2; | ||
|
|
||
| if nums[middle] <= *nums.last().unwrap() { | ||
| end = middle; | ||
| } else { | ||
| start = middle + 1; | ||
| } | ||
| } | ||
|
|
||
| nums[start] | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn step2a_test() { | ||
| assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); | ||
| assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); | ||
| assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![2, 3, 1]), 1); | ||
|
|
||
| assert_eq!(Solution::find_min(vec![1]), 1); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn step1_empty_nums_test() { | ||
| let empty_nums = Vec::new(); | ||
| Solution::find_min(empty_nums); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // Step3 | ||
| // 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する | ||
|
|
||
| // 方法 | ||
| // 時間を測りながらもう一度解く | ||
| // 10分以内に一度もエラーを吐かず正解 | ||
| // これを3回連続でできたら終わり | ||
| // レビューを受ける | ||
| // 作れないデータ構造があった場合は別途自作すること | ||
|
|
||
| /* | ||
| n = nums.len() | ||
| 時間計算量: O(log n) | ||
| 空間計算量: O(1) | ||
| */ | ||
|
|
||
| /* | ||
| 1回目: 1分53秒 | ||
| 2回目: 1分36秒 | ||
| 3回目: 1分20秒 | ||
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { | ||
| pub fn find_min(nums: Vec<i32>) -> i32 { | ||
| if nums.is_empty() { | ||
| panic!("nums must not be empty"); | ||
| } | ||
|
|
||
| let mut start = 0; | ||
| let mut end = nums.len() - 1; | ||
|
|
||
| while start < end { | ||
| let middle = start + (end - start) / 2; | ||
|
|
||
| if nums[middle] < nums[end] { | ||
| end = middle; | ||
| } else { | ||
| start = middle + 1; | ||
| } | ||
| } | ||
|
|
||
| nums[end] | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn step3_test() { | ||
| assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); | ||
| assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); | ||
| assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); | ||
| assert_eq!(Solution::find_min(vec![2, 3, 1]), 1); | ||
|
|
||
| assert_eq!(Solution::find_min(vec![1]), 1); | ||
| } | ||
|
|
||
| #[test] | ||
| #[should_panic] | ||
| fn step3_empty_nums_test() { | ||
| let empty_nums = Vec::new(); | ||
| Solution::find_min(empty_nums); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
最小値のことを、値がnums.last()以下な要素の中で一番左のものと言い換えているから、でしょうか。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
はい。冗長に書くと以下のような理解です。
nums.last()はnums.last()以下な要素の一番大きい値であり、一番右になる。
nums[i] <= nums.last() としているので、nums[i]はnums.last()以下な要素の中で一番左のものになる。
middleが切り捨てになっていて単調減少していくのでiも左に寄っていく。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
以下はfyiの説明です。
回転済みの配列は「左の大きい昇順区間」と「右の小さい昇順区間」に分かれて、そのときnums.last()は必ず右区間の要素になります。
なので nums[i] <= nums.last() は「i が右区間に属するか?」の判定になって、左では False、右では True の単調述語(FF…FFTT…TT)になります。探索中は(FF...F??...??T...TT)のような感じですね。
あとは、この境界部分にまつわる不変条件をstart, endに対して考え、初期化と更新式を考えたらいいですね。step2aの実装では、startは一番左の?(一番右のFの右隣)、endは一番左のTと考えてると言えます。
初期状態は(???...???)なので、start = 0, end = nums.len()の初期化です。
ピボットmiddleから情報を取り、nums[middle] <= nums.last()、つまりmiddle番目がTならend = middleと更新します。
そうでないならmiddle番目はFなので、start = middle + 1と更新します。
やがて?がなくなり、(FF…FFTT…TT)と全て確定します。知りたいのはTの一番左なので、停止時のendを使って答えはnums[end]です。または、一番右のFの右隣と考えてnums[start]でもいいです。このときstart == endなので、ループ継続条件はstart < endで良いですね。