-
Notifications
You must be signed in to change notification settings - Fork 0
35. Search Insert Position #38
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,153 @@ | ||
| # 35. Search Insert Position | ||
|
|
||
| LeetCode URL: https://leetcode.com/problems/search-insert-position/description/ | ||
|
|
||
| この問題は Java で解いています。 | ||
| 各解法において、メソッドが属するクラスとして `Solution` を定義していますが、これは Java の言語仕様に従い、コードを実行可能にするために必要なものです。このクラス自体には特定の意味はなく、単にメソッドを組織化し、実行可能にするためのものです。 | ||
|
|
||
| ## Step 1 | ||
|
|
||
| Binary search を実施して値が見つからなかった場合、while 文を抜けた時点で left ポインタが探索対象の値の次に大きい値を指すことを理解し、次のように書いた。好みで閉区間にしている。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { | ||
| public int searchInsert(int[] nums, int target) { | ||
| int left = 0, right = nums.length - 1; | ||
| while (left <= right) { | ||
| int middle = left + (right - left) / 2; | ||
| if (nums[middle] == target) { | ||
| return middle; | ||
| } | ||
| if (nums[middle] < target) { | ||
| left = middle + 1; | ||
| } | ||
| if (target < nums[middle]) { | ||
| right = middle - 1; | ||
| } | ||
| } | ||
| return left; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 2 | ||
|
|
||
| ### 半開区間 (左閉右開) の実装 | ||
|
|
||
| 半開区間でも書いてみる。こちらの方が一般的な印象があるので以後はこちらをベースに書いていく。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { | ||
| public int searchInsert(int[] nums, int target) { | ||
| int left = 0, right = nums.length; | ||
| while (left < right) { | ||
| int middle = left + (right - left) / 2; | ||
| if (nums[middle] == target) { | ||
| return middle; | ||
| } | ||
| if (nums[middle] < target) { | ||
| left = middle + 1; | ||
| } else { | ||
| right = middle; | ||
| } | ||
| } | ||
| return left; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 必ず while 文を抜ける実装 | ||
|
|
||
| 途中で処理を中断せず while 文を抜けた場合、その時点で left ポインタは探索対象、もしくはその次に大きい値を指している。これを踏まえて step 1 の処理を、必ず while 文を抜けるように修正してみる。 | ||
|
|
||
| "return the index if the target is found" とある問題文の指示を反映している感が薄まったような印象。 | ||
| 一方、どの実装についても内容の理解には上記の left ポインタの特徴を把握することが避けられないのを考えると、これを把握している読み手ならまあ迷うこともないなずなのでこれでもいいのかなと思った。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { | ||
| public int searchInsert(int[] nums, int target) { | ||
| int left = 0, right = nums.length; | ||
| while (left < right) { | ||
| int middle = left + (right - left) / 2; | ||
| if (nums[middle] < target) { | ||
| left = middle + 1; | ||
| } else { | ||
| right = middle; | ||
| } | ||
| } | ||
| return left; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 3 | ||
|
|
||
| 必ず while 文を抜ける実装を採用。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 解いた時間: 2分未満 | ||
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { | ||
|
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.
https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#binarySearch-int:A-int-
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. ありがとうございます。標準クラスのことがすっかり頭から抜けていました。Step 4 にこれを用いた実装を追加しました: 4ab9895 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. Step4も良いと思います。 自分はJavaは詳しくないですが、Binary Searchは結構基本的なものなので言語として提供されていてもおかしくないようなと思って調べたら、あったみたいな感じです。 |
||
| public int searchInsert(int[] nums, int target) { | ||
| int left = 0, right = nums.length; | ||
| while (left < right) { | ||
| int middle = left + (right - left) / 2; | ||
| if (nums[middle] < target) { | ||
| left = middle + 1; | ||
| } else { | ||
| right = middle; | ||
| } | ||
| } | ||
| return left; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 4 | ||
|
|
||
| ### Arrays クラスを用いる解法 | ||
|
|
||
| ahayashi さんの指摘で Arrays.binarySearch() があったことを思い出したので加筆。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(log n) | ||
| * 空間計算量: O(1) | ||
| */ | ||
| class Solution { | ||
| public int searchInsert(int[] nums, int target) { | ||
| int foundIndex = Arrays.binarySearch(nums, target); | ||
| if (0 <= foundIndex) { | ||
| return foundIndex; | ||
| } | ||
|
|
||
| return -(foundIndex + 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. これ、ビット反転 ~ を使うことが想定ではありませんでしたっけ。 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の補数 -> ビット反転をして1を足す ipをマイナスして、1を引く なので元の挿入位置を求めるには再度ビット反転をするだけで良い。
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. @oda |
||
| } | ||
| } | ||
| ``` | ||
|
|
||
| その後 [oda さんの指摘](https://github.com/seal-azarashi/leetcode/pull/38#discussion_r1834834703)を受けビット反転するよう修正。 | ||
|
|
||
| ```java | ||
| class Solution { | ||
| public int searchInsert(int[] nums, int target) { | ||
| int foundIndex = Arrays.binarySearch(nums, target); | ||
| return 0 <= foundIndex ? foundIndex : ~foundIndex; | ||
| } | ||
| } | ||
| ``` | ||
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.
二分探索についてどのように理解しているか、この文章からは拾えませんでした。
色々なところに色々なことが書かれていますので、下のリンクなどから一通り見て自明なことしかなければ大丈夫でしょう。
rossy0213/leetcode#26 (comment)
Uh oh!
There was an error while loading. Please reload this page.
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.
ご指摘ありがとうございます。上記拝見しましたが、正直自明と言えるほどは理解が固まっていなかったと思います。
自分の中で整理して改めて以下のように書き下してみたのですが、違和感ございませんでしょうか?
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.
お題は「すべての値が異なる昇順の配列と target という値が与えられる、target があるならばその場所を、ないならば、それよりも左がすべて target よりも小さくなる場所を返せ」ですかね。
いや、私の抵抗感がどこから来ているかというと、left, right はそれぞれ何を満たしている値だと思ってループを回していて、それがループから出たときに何が満たされているのかの認識がふわふわしているように思っています。
mid の選び方は left <= mid < right でないといけない(mid == right では駄目)ですが、最後、left == right となってループを抜けて 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 は ∀ i < left, nums[i] < target を満たします。
right は ∀ i >= right, target <= nums[i] を満たします。
これならば分かります。
Uh oh!
There was an error while loading. Please reload this page.
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.
@oda
ご返信ありがとうございます。
正直ふわふわしているというのはその通りで、どう返信すればいいか30分以上考えてしまいました。
以下のように整理したのですが、理解が充分かご確認頂けますと幸いです。
left == rightとなり、その位置が target が存在するもしくは target より大きい最小の要素がある場所を示すこちらもご指摘ありがとうございます。恥ずかしながら不変条件を考えるというのも出来ておりませんでしたので、大変参考になりました。
このように整理されていればループ開始時から終了時までずっと満たされている条件が明確で、なぜループを抜けた際に
left == rightを返して問題ないと考えているのかが分かりますね。left より左の範囲は target より小さい値しかなく、target が存在するならば right かそれより右になるので、left == rightになっているということは、その位置が target が存在するもしくは target より大きい最小の要素がある場所を示すと。全称記号すら記憶が曖昧でググりながら返信をしているような状況なので先は長いですが、このような返答が出来る状態になれるよう、引き続きやっていこうと思います。
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.
あ、はい。これでクリスタルクリアーですかね。
たぶん、もう一回ここまでを読み直して、論評をすると理解が深まると思います。
Uh oh!
There was an error while loading. Please reload this page.
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.
あ、一箇所、この部分、正確に位置を同定せずとも、while を抜けているので、その条件が成立していない、すなわち
だけでも同じ議論が成立するはずです。
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.
ありがとうございます!たくさんお時間頂いてありがとうございました🙇♂️
はい、後ほど実施しておきます。
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.
ご指摘ありがとうございます。確かにそうですね。
上記踏まえて以下の通り書き直してみました。
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 <= middle <= right ですね。
left = middle + 1;またはright = middle - 1;が起き、right - left は必ず1以上減っていくので、0未満になりますね。半開区間の場合は、left <= middle < right ですね。
left = middle + 1;またはright = middle;が起きると、やはり right - left は必ず1以上減っていき、0以下になりますね。