Conversation
hayashi-ay
left a comment
There was a problem hiding this comment.
良いと思います。好みですが、left, rightではなくlow, hightという命名もありだと思います。
挿入する地点の前の要素は、targetより小さく、挿入する地点を含む後ろの要素は、targetより大きくなる地点を選べばよいですね。
Pythonの記法ちっくですが、挿入するインデックスをipとすると
nums[:ip]の要素についてはtarget未満、nums[ip:]の要素についてはtarget以上になるように選べばよいですね。
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { |
There was a problem hiding this comment.
Arrays.binarySearchも見ておくと良いのかなと思います。
https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#binarySearch-int:A-int-
There was a problem hiding this comment.
ありがとうございます。標準クラスのことがすっかり頭から抜けていました。Step 4 にこれを用いた実装を追加しました: 4ab9895
There was a problem hiding this comment.
Step4も良いと思います。0 < foundIndexのところは自分はfoundIndex > 0と書きますが、好みかもしれないです。
自分はJavaは詳しくないですが、Binary Searchは結構基本的なものなので言語として提供されていてもおかしくないようなと思って調べたら、あったみたいな感じです。
| return foundIndex; | ||
| } | ||
|
|
||
| return -(foundIndex + 1); |
There was a problem hiding this comment.
あー、ドキュメント上は記述はなかったですが、ビット反転で元の挿入位置が求まりますね。算術演算をするよりビット演算の方が良いですね。
There was a problem hiding this comment.
2の補数 -> ビット反転をして1を足す
ipをマイナスして、1を引く
-> ipの2の補数を求めて、1を引く
-> ipのビット反転をして、1を足して、1を引く
-> ipのビット反転をする
なので元の挿入位置を求めるには再度ビット反転をするだけで良い。
There was a problem hiding this comment.
@oda
ありがとうございます!Step 4 に追記しました。
いやービット反転や2の補数については頭に入れたつもりになってましたが、こういうところで出てこないのが基礎力不足を表してるなと痛感しますね😇
|
|
||
| ### 必ず while 文を抜ける実装 | ||
|
|
||
| 途中で処理を中断せず while 文を抜けた場合、その時点で left ポインタは探索対象、もしくはその次に大きい値を指している。これを踏まえて step 1 の処理を、必ず while 文を抜けるように修正してみる。 |
There was a problem hiding this comment.
二分探索についてどのように理解しているか、この文章からは拾えませんでした。
色々なところに色々なことが書かれていますので、下のリンクなどから一通り見て自明なことしかなければ大丈夫でしょう。
rossy0213/leetcode#26 (comment)
There was a problem hiding this comment.
ご指摘ありがとうございます。上記拝見しましたが、正直自明と言えるほどは理解が固まっていなかったと思います。
自分の中で整理して改めて以下のように書き下してみたのですが、違和感ございませんでしょうか?
この解法の二分探索は、探索対象の区間を一度の操作で半分に狭めていくことで target を見つけることを試みる。区間は半開区間 (左閉右開) としているため、各イテレーションでは、区間の凡そ真ん中にある値を確認してそれが target よりも大きければ右の境界をその真ん中の値の位置に、target よりも小さければ左の境界をその真ん中の値よりひとつ右の位置に設定し直す操作が行われる。
仮に target が探索対象に存在しない場合、最後に確認される値が target よりも小さければ、左の境界が指すインデックスはその一つ右に移動して処理が終了する。また最後に確認される値が target よりも大きい場合、左の境界が指すインデックスはそのままに、右の境界が指すインデックスがその一つ左に移動して処理が終了するため、左の境界が指すインデックスは、target の次に小さい値のひとつ右を指したままとなる。
つまり、左の境界が指すインデックスは、target が探索対象に存在しない場合、処理の終了時点で target よりもひとつ小さい値の右側を常に指し、これは target の正しい挿入位置を示している。この解法はこの特徴を利用したものとなっている。
There was a problem hiding this comment.
お題は「すべての値が異なる昇順の配列と target という値が与えられる、target があるならばその場所を、ないならば、それよりも左がすべて target よりも小さくなる場所を返せ」ですかね。
いや、私の抵抗感がどこから来ているかというと、left, right はそれぞれ何を満たしている値だと思ってループを回していて、それがループから出たときに何が満たされているのかの認識がふわふわしているように思っています。
mid の選び方は left <= mid < right でないといけない(mid == right では駄目)ですが、最後、left == right となってループを抜けて left == right を返しますね。そこをどう理解しているかを聞きたいです。
There was a problem hiding this comment.
たとえば、
left は ∀ i < left, nums[i] < target を満たします。
right は ∀ i >= right, target <= nums[i] を満たします。
これならば分かります。
There was a problem hiding this comment.
@oda
ご返信ありがとうございます。
いや、私の抵抗感がどこから来ているかというと、left, right はそれぞれ何を満たしている値だと思ってループを回していて、それがループから出たときに何が満たされているのかの認識がふわふわしているように思っています。
mid の選び方は left <= mid < right でないといけない(mid == right では駄目)ですが、最後、left == right となってループを抜けて left == right を返しますね。そこをどう理解しているかを聞きたいです。
正直ふわふわしているというのはその通りで、どう返信すればいいか30分以上考えてしまいました。
以下のように整理したのですが、理解が充分かご確認頂けますと幸いです。
- ループ中の left, right は、それぞれ target が存在する可能性がある範囲の左端と右端を表しており、[left, right) の範囲内に target が存在する可能性があり、それ以外には存在しないことを保証する。
- ループを抜けた際は探索範囲が一点に収束し
left == rightとなり、その位置が target が存在するもしくは target より大きい最小の要素がある場所を示す
たとえば、
left は ∀ i < left, nums[i] < target を満たします。
right は ∀ i >= right, target <= nums[i] を満たします。
これならば分かります。
こちらもご指摘ありがとうございます。恥ずかしながら不変条件を考えるというのも出来ておりませんでしたので、大変参考になりました。
このように整理されていればループ開始時から終了時までずっと満たされている条件が明確で、なぜループを抜けた際に left == right を返して問題ないと考えているのかが分かりますね。left より左の範囲は target より小さい値しかなく、target が存在するならば right かそれより右になるので、left == right になっているということは、その位置が target が存在するもしくは target より大きい最小の要素がある場所を示すと。
全称記号すら記憶が曖昧でググりながら返信をしているような状況なので先は長いですが、このような返答が出来る状態になれるよう、引き続きやっていこうと思います。
There was a problem hiding this comment.
あ、はい。これでクリスタルクリアーですかね。
たぶん、もう一回ここまでを読み直して、論評をすると理解が深まると思います。
There was a problem hiding this comment.
閉区間になっている1つ目の実装について、left は right の一つ右隣の位置を指します。
半開区間になっている2つ目の実装については、left と right は同じ位置を指します。
あ、一箇所、この部分、正確に位置を同定せずとも、while を抜けているので、その条件が成立していない、すなわち
- left は right よりも右にある
- left は right と同じまたは右にある
だけでも同じ議論が成立するはずです。
There was a problem hiding this comment.
あ、はい。これでクリスタルクリアーですかね。
ありがとうございます!たくさんお時間頂いてありがとうございました🙇♂️
たぶん、もう一回ここまでを読み直して、論評をすると理解が深まると思います。
はい、後ほど実施しておきます。
There was a problem hiding this comment.
閉区間になっている1つ目の実装について、left は right の一つ右隣の位置を指します。
半開区間になっている2つ目の実装については、left と right は同じ位置を指します。あ、一箇所、この部分、正確に位置を同定せずとも、while を抜けているので、その条件が成立していない、すなわち
- left は right よりも右にある
- left は right と同じまたは右にある
だけでも同じ議論が成立するはずです。
ご指摘ありがとうございます。確かにそうですね。
上記踏まえて以下の通り書き直してみました。
> それぞれ、while の条件が成立せずにループを抜けてきたとしましょう。
> left と right の関係について、それぞれ何が言えますか。
閉区間になっている1つ目の実装について、left は right よりも右にある。
半開区間になっている2つ目の実装については、left は right と同じまたは右にある。
> 抜けた後ですべて return left をしていますが、それぞれ right についての条件から、return した left の指す位置の値について何かいえますか。
閉区間になっている1つ目の実装について:
- right よりも右の値は target よりも大きい。
- left が指す位置について、これより左は target よりも小さく、right との位置関係からこの位置にある要素は (もし存在するなら) target よりも大きい。
- よって (left かそれよりも右に要素が存在するならそれらを一つ右にずらして) この位置に target を挿入すると、配列はソートを保ったままになると言える。
半開区間になっている2つ目の実装について:
- right が指す位置かそれよりも右の位置の値たちは target よりも大きい。
- left が指す位置について、これより左は target よりも小さく、right との位置関係からこの位置にある要素は (もし存在するなら) target よりも大きい。
- よって (left かそれよりも右に要素が存在するならそれらを一つ右にずらして) この位置に target を挿入すると、配列はソートを保ったままになると言える。
There was a problem hiding this comment.
そんなところです。
あとは、停止性の議論がありますね。
閉区間の場合、left <= middle <= right ですね。
left = middle + 1;またはright = middle - 1;が起き、right - left は必ず1以上減っていくので、0未満になりますね。
半開区間の場合は、left <= middle < right ですね。
left = middle + 1;またはright = middle;が起きると、やはり right - left は必ず1以上減っていき、0以下になりますね。
|
見ました!やり取り等参考になりました🙏 |
https://leetcode.com/problems/search-insert-position/description/