-
Notifications
You must be signed in to change notification settings - Fork 0
153 find minimum in rotated sorted array #2
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize - 1; | ||
| while (left <= right) { // leftとrightが交差して終了するケース | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] < nums[0]) { // [False,...,False,True,...True]で、Trueの左端が答えになる | ||
| right = mid - 1; // 基本True側にいるが、終了時にFalseに来る | ||
|
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. 一番左端の True の位置を求めようとしているのだとすると、区間の中に一番左端の True が常に含まれる状態になければならないように思います。ところが、 mid の位置に一番左端の True が来た場合、 -1 をしてしまうと、区間から一番左端の True が外れてしまいます。これは元の問題設定と矛盾した実装となっているように思います。 |
||
| } | ||
| else { | ||
| left = mid + 1; // 基本False側にいるが、終了時にTrueに来る | ||
| } | ||
| } | ||
| // 終了状態は以下のよう。leftの位置が答えに | ||
| // [False,...,False,True,...True] | ||
| // right "left" | ||
| return nums[left % numsSize]; // [False,...,False]タイプに対応([0,1,2,3]のケースに対応) | ||
| } | ||
|
|
||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize - 1; // leftとrightが交差して終了するケース | ||
| while (left <= right) { // leftとrightが交差して終了するケース | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] <= nums[numsSize - 1]) { // [False,...,False,True,...True]で、Trueの左端が答えになる | ||
| right = mid - 1; // 基本True側にいるが、終了時にFalseに来る | ||
| } | ||
| else { | ||
| left = mid + 1; // 基本False側にいるが、終了時にTureに来る | ||
| } | ||
| } | ||
| return nums[left]; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
|
|
||
| # step1 | ||
|
|
||
| 40分かかった。 | ||
| 時間計算量:O(log(n)) | ||
| 空間計算量:O(1) | ||
| 閉区間で行くか半開区間で行くか、前もって決めれば、midの動かし方で躊躇しなくなった。 | ||
| midとの比較対象として、ずっと配列の左端の値(target)を使っているが、ベストかわからない。 | ||
| もっとスマートな方法があるのだろうか? | ||
|
Comment on lines
+7
to
+9
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. 分からなかったらすぐに他の人の回答を見るでいいと思いますよ
Owner
Author
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. アルゴリズムの知識やその実装経験が少ないので、そのようにしようかと思います。 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. うーん、結論から言うと2分で考えて書いているのは本当なのですが、何故できているかに関しては(直前に書いてて覚えてるのもあるんですが)経験も含まれるんでしょうかね。
この絵がどんなものか僕は分からないのですが、僕がイメージするのは左半分がFalse(またはTrue)になっていて、右半分がTrue(またはFalse)になっている棒ですね。 まず僕にとって理解しやすかったのはこちらのパターンです。 // 始める前にleftがFalse、rightがTrueであることを確認しておく
int left = 0, right = numSize - 1;
while(right - left > 1) { // leftとrightが隣り合うまで持ち替え続ける
int mid = (left + right) / 2;
if(条件式(mid)) {
right = mid; // rightは常に条件式をTrueにする
} else {
left = mid; // leftは常に条件式をFalseにする
}
}
// この時点でleftとrightはFalseとTrueの境目
return nums[right]; // 条件式がTrueとなるものの一番左
// もしくは、
return nums[left]; // 条件式がFalseとなるものの一番右次に、leftがTrueになることを許可することで、このパターンが理解できるようになります。 // 始める前にrightがTrueであることを確認しておく
int left = 0, right = numSize - 1;
while(left < right) { // leftとrightが同じになるまで
int mid = (left + right) / 2;
if(条件式(mid)) {
right = mid; // rightは常に条件式をTrueにする
} else {
left = mid + 1; // midはFalseだが、leftはTrueになりうる
}
}
// この時点でleft=rightは一番左のTrue
return nums[right]; // 条件式がTrueとなるものの一番左もしくはrightがFalseになることを許可することで、このパターンが理解できるようになります。 // 始める前にleftがFalseであることを確認しておく
int left = 0, right = numSize - 1;
while(left < right) { // leftとrightが同じになるまで
int mid = (left + right) / 2;
if(条件式(mid)) {
right = mid - 1; // midはTrueだが、rightはFalseになりうる
} else {
left = mid; // leftは常に条件式をFalseにする
}
}
// この時点でleft=rightは一番右のFalse
return nums[left]; // 条件式がFalseとなるものの一番右以上は
に対する回答ですが、
に関しては条件式に当たる部分で、問題によって異なります。 ちなみに、「二分探索は別に特別なアルゴリズムではなく、実は人間が日常生活でも自然に行っている身近な方法だ」という話をどこかで聞いたことがある気がします。 ちなみに、二分探索は読むよりも書く方が楽なので、正直上の説明でも分かりずらい気がしています。
Owner
Author
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. アドバイスありがとうございます! 今回の問題ですと、 もし、 int left = 0, right = numSize;
while(left < right) { // leftとrightが同じになるまで
int mid = (left + right) / 2;
if(nums[mid]<=nums[0]) {
right = mid; // midはTrueで、rightもTrueにしかならない
} else {
left = mid + 1; // midはFalseだが、leftはTrueになりうる
}
}
return nums[right % numSize]; いや、この方法だと[2,1]のケースに対応できませんでした。。。 ともかくちょっとだけ条件式やleft,rightのずらし方の剪定方法がわかった気がします。 レビューしてくださった方の知識を咀嚼していたら、半日ほど経ってしまいました。。。 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. そうですね…。もしかしたらstd::partition_point使ってみるといいかもしれません。
(この協会ではパズルが悪い意味で使われていることもあったかもしれませんが、 @takuya576 さんのおっしゃりたいことの意味的には)その感覚はいいと思っています。パズルというか、アクロバティックな考え方というか、本来そういう考え方の自由さは(常に?)姿勢としてあった方がいいと思っています。
(多分これ最初に早期リターン追加すれば済むんじゃないですかね。同じ設定ではないですが参考) 問題はそのパズルをどう解くかですね。
っていう3つで考えていて、2はいくつかのパターンしかなく、3はおまけというか、あんまり重要ではないです。 あとちなみに、コメントに関しては話半分で聞くでいいと思いますよ。 Miyamoto-tryk/leetcode-arai60#1 (comment)
書く側っていうのはどうしても読む側の気持ちが分からないので、人のコードを読んだ時にこの書き方読みづらいなとか、この書き方の方が読みやすいじゃん!となればいいと思います。 あと、レビューコメントも「あくまで私はこう思う」というスタンスです(多分皆面倒くさくてコメントでそのニュアンスを表現できていないのですが笑)。 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. // 始める前にleftがFalse、rightがTrueであることを確認しておく
int left = 0, right = numSize - 1;
while(right - left > 1) { // leftとrightが隣り合うまで持ち替え続ける
int mid = (left + right) / 2;
if(条件式(mid)) {
right = mid; // rightは常に条件式をTrueにする
} else {
left = mid; // leftは常に条件式をFalseにする
}
}
// この時点でleftとrightはFalseとTrueの境目
return nums[right]; // 条件式がTrueとなるものの一番左
// もしくは、
return nums[left]; // 条件式がFalseとなるものの一番右こちらのコードに違和感を感じました。 とあることから、閉区間を想定しているように見えました。また、 とあることから、ループを終了したタイミングで、区間の中に 2 つの要素が含まれることが分かります。この実装ですと、
の位置を求めることができないのではないでしょうか? 仮にこのパターンで書くのであれば、 と開区間を想定して False と True の境界を求めたあと、条件式がTrueとなるものの一番左、もしくは条件式がFalseとなるものの一番右の位置を求めてあげるのが良いと思いました。これは、番兵として、一番左の左側に False を、一番右の右側に True を置いて閉区間を想定するのと等価になると思います。 残りの二つは問題ないように思いますが、 right が False になることを許可するパターンはあまり見ないように思いました。 left が True になることを許可するパターンのほうをよく見かけます。
すでに他の方の解答等を見てご存じかと思いますが、 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.
こちらその通りですね。コードが間違っていました。失礼しました。
そうなのですね。というか、こちらのコードよくよく考えたら動かないことに気づきました。(笑)
Owner
Author
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.
C++の標準ライブラリを使って慣れてみようと思います!
自分で何度か書き直したり、皆さんの議論を見ているうちに自分もこの考え方が何となく染み付いてきました。ありがとうございます! |
||
|
|
||
| # step2 | ||
|
|
||
| 最初にtargetとして、左端の値をとり、midがtargetより小さくなるたびにtargetをmidで更新すれば、targetが最終的な答えにできる。 | ||
| leetcodeの答えを見てみたが、全くrotateされていないケースについては、エッジケースとして先に処理し、 | ||
| 残りのケースの最小値を以下の判定式で求めている。 | ||
|
|
||
| ```c:example.c | ||
| // If the mid element is greater than its next element then mid+1 | ||
| // element is the smallest This point would be the point of change. From | ||
| // higher to lower value. | ||
| if (nums[mid] > nums[mid + 1]) { | ||
| return nums[mid + 1]; | ||
| } | ||
|
|
||
| // If the mid element is lesser than its previous element then mid | ||
| // element is the smallest | ||
| if (mid > 0 && nums[mid - 1] > nums[mid]) { | ||
| return nums[mid]; | ||
| } | ||
| ``` | ||
|
|
||
| rotateしていないケースは特殊なので、こっちの方が直感的で分かりやすい可能性もある。 | ||
| また、上記の条件式で最初にinflection pointを検出していることで、[2,1]などのケースにおいてもエラーが発生しない。 | ||
| 上記の条件式での検出を行わない場合、以下の操作をすると、nums[mid] <= nums[0]の条件でright = mid - 1を実行するので、1がnums[0]と比較されずスキップされてしまうのである。 | ||
|
|
||
| ```c:example.c | ||
| if (nums[mid] > nums[0]) { | ||
| left = mid + 1; | ||
| } else { | ||
| // If nums[0] is greater than the mid value then this means the | ||
| // smallest value is somewhere to the left | ||
| right = mid - 1; | ||
| } | ||
| ``` | ||
|
|
||
| また、他の人の回答も見てみた。 | ||
| while (left < right - 1)という条件で、leftとrightが並ぶ形で終了するケースもあるのだ学んだ。 | ||
|
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. 以前小田さんから追加質問を受けたものです(恥ずかしながらめちゃくちゃ躓いております) 「2で割る処理がありますがこれは切り捨てでも切り上げでも構わないのでしょうか。」 |
||
| 色々やると疲れて続かないので、基本的な半開区間、閉区間で練習しようと思う。 | ||
|
|
||
| # step3 | ||
|
|
||
| 半開区間、閉区間それぞれ2回ずつ行った。 | ||
| エッジケースをそれぞれ手元で書きながら確認しないと厳しい。 | ||
| 何もせずに書けはするが、脳死で書くだけなので内容を把握したことにはならなそう。 | ||
| トータル3時間かかった。 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize - 1, target = nums[0], min = nums[0]; | ||
|
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. 一行で定義している変数の数が多く、読みにくく感じました。一行に 1 つずつ定義したほうが読みやすいように思います。 |
||
| while (left <= right) { | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] < min) { | ||
| min = nums[mid]; | ||
| } | ||
| if (nums[mid] < target) { | ||
| right = mid - 1; | ||
|
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. 理解の確認のため質問させてください。 |
||
| } | ||
| else { | ||
| left = mid + 1; | ||
| } | ||
| } | ||
| return min; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize - 1, target = nums[0]; | ||
| while (left <= right) { | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] < target) { | ||
| right = mid - 1; | ||
| } | ||
| else { | ||
| left = mid + 1; | ||
| } | ||
| if (nums[mid] < target) { | ||
| target = nums[mid]; | ||
|
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. ターゲットとなる何らかの値を更新しているという点に違和感を感じました。 min_num といった変数名にしたほうが分かりやすいと思います。 |
||
| } | ||
| } | ||
| return target; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize - 1, target = nums[0]; | ||
| while (left <= right) { | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] < target) { | ||
| right = mid - 1; | ||
| target = nums[mid]; | ||
|
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. target が最小値、つまり二分探索で見つけたい値ということですよね?二分探索は「この範囲に目当てのものがあるはず」という区間を狭めていくアルゴリズムなので target が right より右に来ることは自然ではないように思います。
Owner
Author
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. 確かに、一見自然でない見え方がすると思いました。 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. hroc135さんやolsen-blueさんからも指摘がある通り、targetは一定であって欲しいと思いました。 自分も何度か受けた指摘になるのですが、ループ内での不変条件を意識したらいいと思いました。 |
||
| } | ||
| else { | ||
| left = mid + 1; | ||
| } | ||
| } | ||
| return target; | ||
| } | ||
|
|
||
| int findMin(int* nums, int numsSize) { | ||
| int left = 0, right = numsSize, target = nums[0]; | ||
| while (left < right) { | ||
| int mid = left + (right - left) / 2; | ||
| if (nums[mid] < target) { | ||
| right = mid; | ||
| target = nums[mid]; | ||
|
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. targetをいじりたくない気がします。 |
||
| } | ||
| else { | ||
| left = mid + 1; | ||
| } | ||
| } | ||
| return target; | ||
| } | ||
|
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. int findMin(int* nums, int numsSize) {
int left = 0, right = numsSize - 1;
int target = nums[0];
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return nums[left % numsSize];
}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. @takuya576 私がちょっと手を加えるならば、このあたりです。どういう風に考えて書いたかをもう少し教えて欲しいのと、こっちのちょっと変えた二つのコードを見て、何が違うかを考えて欲しいです。 int findMin(int* nums, int numsSize) {
int left = 0, right = numsSize - 1;
int target = nums[right];
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return nums[left];
}
Owner
Author
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. 自分の思考過程を記述しようと思います。 小田さんが提示してくださったコードについて、考えを記述します。
これらの指摘は、小田さんが考えていらっしゃることと合致していますか? 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. えー、まず、改善の余地はあるものの、この構造が悪いとは思いません。 そのうえで、聞きたかったのは、「どう考えていたのか」「どう表現されているか」の距離感です。 これは「ターゲット」とはいい難いですね。標準的な二分探索では「ある値より小さいか等しいものの中で一番左にあるものを探したい、その目標値」という意味なので、ターゲットが満点かはともかく名付けとしては筋が通っています。 そうすると、「最小値を更新しながら、より小さいものがある可能性のある場所を絞っていき、その可能性がなくなったら終了」というプログラムですね。次に気になるのは、「一番小さい場所を見る前に終了することはないのか」ということです。これは大丈夫そうです。 そういうわけで、名前が実態を表していないのが、一番妙なところかと思います。 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. 言い方を変えると、left とはそこよりも左に最小値がないと確定している領域、right とはそこよりも右に最小値がないと確定している領域ですね。 ループはある種の仕事の引き継ぎのようなもので、どういうものだと思って引き継いでいるのか、それが変数名などで初めて読んだ人にもある程度通じるのか、を意識して書くといいでしょう。 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. 意味を考えるとここでの意味での閉区間での表現ならば終了時には入れ替わりますよね。left 以上 right 以下の範囲は条件を満たしているか調べないといけない領域なので。 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.
この部分の解釈に自信ありませんでした。終了時には、調べないといない領域を調べつくしたので、 left 以上 right 以下の範囲には要素が存在しない状態になる、という解釈で合っていますか? 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. まず、nums[mid] <= nums[right] のバージョンとの兼ね合いでいえば、 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. target = nums[mid]; と target を更新し続ける回答との兼ね合いでいうと、target は、常にここまで出てきた最小値を指しています。また、これは、先頭との比較をしています。このコードを < か <= で理解するかは、どちらでもよく少し意味付けが変わります。target の位置を t、target よりも大きいと判明しているところを True とすると、 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. 区間の中に要素が一つもない状態を left > right で表していると理解しました。ありがとうございます。 |
||
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.
区間を考えるにあたり、プログラムの実行中、一瞬でも left > right となる瞬間がある点に違和感を感じました。
とあるため、区間の種類を閉区間としているのだと思いました。その場合、常に left <= right が成り立つと思いました。
とあるのですが、これは False と True の境界の位置を求めようとしているのでしょうか?それとも、一番左端の True の位置を求めようとしているのでしょうか?
仮に後者だとすると、最後に一番左端の True の位置に left と right が来て、閉区間の中に一番左端の True のみが含まれる状態になるのだと思いました。
一方、コメントには
とあり、区間についての根本的な考え方に違和感を感じました。
https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562
このコメントや他の二分探索に関するコメントをご覧になることをお勧めします。