-
Notifications
You must be signed in to change notification settings - Fork 0
Solved: 35. Search Insert Position #41
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,159 @@ | ||
| ## 取り組み方 | ||
| - step1: 5分以内に空で書いてAcceptedされるまで解く + テストケースと関連する知識を連想してみる | ||
| - step2: 他の方の記録を読んで連想すべき知識や実装を把握した上で、前提を置いた状態で最適な手法を選択し実装する | ||
| - step3: 10分以内に1回もエラーを出さずに3回連続で解く | ||
|
|
||
| ## step1 | ||
| target以上になるindexを二分探索で返せば良い。 | ||
|
|
||
| leftが最終的にtargetを超える最初のインデックスになる、もしくはmidがtargetと一致して打ち切りするようにする。 | ||
| 探索範囲を`[left, right]`とするパターンと、`[left, right)`とするパターンで書く。 | ||
|
|
||
| また、midの生成方法はleft + right // 2で切り捨て固定。 | ||
| ループの終了条件は探索範囲が空の時、更新時はmidを2度と使わない(必ず範囲を狭める)ようにする。 | ||
|
|
||
| テストケースとして考えたいのは | ||
| - sortされていないnums: [5,4], 1 <- 一旦、このケースはなしとする。 | ||
| - 空配列: [], 1 | ||
| - 単一要素: [5], 1 | ||
| - 重複要素を含むnums: [5,5], 1 | ||
| - 正常系: 奇数個のnums: [1,2,3], 5 | ||
| - 正常系: 偶数個のnums: [1,2,3,4], 5 | ||
|
|
||
| ### 探索範囲を`[left, right]`とするとき | ||
| - 終了条件は探索範囲が空の時なので、left > right | ||
| - 更新方法は | ||
| - nums[mid]がtarget : return left | ||
| - nums[mid]がtargetより小さい : left = mid + 1 | ||
| - nums[mid]がtargetより大きい : right = mid - 1 | ||
|
|
||
| ```python | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| if not nums: | ||
| return 0 | ||
|
|
||
| # search [left, right] | ||
| left = 0 | ||
| right = len(nums) - 1 | ||
|
|
||
| while left <= right: | ||
| mid = (left + right) // 2 | ||
| if nums[mid] == target: | ||
| return mid | ||
| if nums[mid] < target: | ||
| left = mid + 1 | ||
| if nums[mid] > target: | ||
| right = mid - 1 | ||
|
|
||
| return left | ||
| ``` | ||
|
|
||
| ### 探索範囲を`[left, right)`とするとき | ||
| - 終了条件は探索範囲が空の時なので、left >= right | ||
| - 更新方法は | ||
| - nums[mid]がtarget : return left | ||
| - nums[mid]がtargetより小さい : left = mid + 1 | ||
| - nums[mid]がtargetより大きい : right = mid | ||
|
|
||
| ```python | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| if not nums: | ||
| return 0 | ||
|
|
||
| # search [left, right) | ||
| left = 0 | ||
| right = len(nums) | ||
|
|
||
| while left < right: | ||
| mid = (left + right) // 2 | ||
| if nums[mid] == target: | ||
| return mid | ||
| if nums[mid] < target: | ||
| left = mid + 1 | ||
| if nums[mid] > target: | ||
| right = mid | ||
|
|
||
| return left | ||
| ``` | ||
|
|
||
| ## step2 | ||
| ### 読んだコード | ||
| - https://github.com/seal-azarashi/leetcode/pull/38/files | ||
| - https://github.com/Yoshiki-Iwasa/Arai60/pull/34/files | ||
| - https://github.com/nittoco/leetcode/pull/28/files | ||
| - https://github.com/hayashi-ay/leetcode/pull/40/files | ||
|
|
||
| ### 感想 | ||
| - leftをtarget以上になる初めての値になるように更新しているので、`if nums[mid] > target:`の条件にイコールを含めて、`if nums[mid]==target`の条件をなくしても成立する | ||
| - 探索範囲を決めたら、あとは無限ループをせず探索範囲を正しく狭められるように、終了条件、更新方法、切り捨てor切り上げを選択するくらいの理解度で実装しているが問題ないのだろうか | ||
| - 探索範囲を`(l, r]`、`(l, r)`であったり、`mid`に切り上げを使うのは直感に反する感じがする | ||
| - `(l, r]`はスライスの定義の仕方と逆なので不自然な感じがする | ||
| - `(l, r)`は`mid`が`-1`になる可能性を秘めていそうで抵抗感がある | ||
| - 切り上げを使うのは、leftを返す話なのに、あえて更新が右寄りになるようにするところが変な感じ | ||
|
|
||
| - `bisect_left`の実装は、`[lo, hi)`で探索をし、`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. この方法は、同じ値があった場合に一番左が出てきていることに注意です。なので、考え方がちょっと違いますね。
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
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://github.com/python/cpython/blob/cfbdce72083fca791947cbb18114115c90738d99/Lib/bisect.py#L94 | ||
|
|
||
| ```python | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| if not nums: | ||
| return 0 | ||
|
|
||
| lo = 0 | ||
| hi = len(nums) | ||
|
|
||
| while lo < hi: | ||
| mid = (lo + hi) // 2 | ||
| if nums[mid] < target: | ||
| lo = mid + 1 | ||
| else: | ||
| hi = mid | ||
|
|
||
| return lo | ||
| ``` | ||
|
|
||
| ## step3 | ||
| ```python | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| if not nums: | ||
| return 0 | ||
|
|
||
| lo = 0 | ||
| hi = len(nums) - 1 | ||
|
|
||
| while not lo > hi: | ||
| mid = (lo + hi) // 2 | ||
| if nums[mid] < target: | ||
| lo = mid + 1 | ||
| else: | ||
| hi = mid - 1 | ||
|
|
||
| return lo | ||
| ``` | ||
|
|
||
| ### step4 | ||
| 暗記で書いてないか確かめるために`(lo, hi)`で探索するパターンも書いてみる | ||
|
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. いろいろなパターンで練習されていて素晴らしいと思います!もし範囲に関して毎回頭を使いたくなかったら「めぐる式」のようにokの範囲とngの範囲で分けるのも手かもしれません。(変数名は 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. @oda 読める幅を増やすべきという意見は同意です。また共有したアイデアもその幅のひとつとして、意味がわかって説明できるならば運用してもいいかなとも思ってます(それしかできない、テンプレ通り使うだけでロジックがわかっていないはアウト)。個人的にはこんな感じなら更新している範囲の解釈がしやすくて、むしろleft, rightの以上に意味をもたせられるという立場なのですがどうですかね? class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
if nums[0] >= target:
return 0
def is_valid(index: int) -> bool:
# valid range := {i | target <= nums[i]}
return target <= nums[index]
valid_index = len(nums) # Intentional. This index is not directly accessed.
invalid_index = 0
# Find the left most valid index by binary search
while valid_index - invalid_index > 1:
mid_index = (valid_index + invalid_index) // 2
if is_valid(mid_index):
valid_index = mid_index
else:
invalid_index = mid_index
return valid_indexThere 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. 私は読めますが、私の読む力を試しても仕方がないので、色々なものを読んで理解ができるかを確認していきましょう。はじめの分岐は invalid_index = -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. @Fuminiton さんごめんなさい、勝手に別の書き方の流れを作ってしまい💦
結果としては、そうしても大丈夫です。それを許容すると最初の class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
"""
Approach:
* Consider all indices in [0, 1, ..., len(nums)-1, len(nums)] as candidates.
* Split these indices into two consecutive sections: `invalid` and `valid`.
* Define `valid` so that the answer is the smallest valid index.
* Use binary search to find this minimum valid index, which is the answer.
* The search range is defined as (invalid_index, valid_index], starting from (-1, len(nums)].
"""
def is_valid(index: int) -> bool:
# Valid indices are those from 0 to len(nums),
# where either nums[index] >= target,
# or index == len(nums) (just after the last element)
return 0 <= index and (index == len(nums) or target <= nums[index])
valid_index = len(nums)
invalid_index = -1
# Find the left most valid index by binary search
while valid_index - invalid_index > 1:
mid_index = (valid_index + invalid_index) // 2
if is_valid(mid_index):
valid_index = mid_index
else:
invalid_index = mid_index
return valid_indexこの話題を上げた意図は次のようなものでした。
ただ私が書いた方法も、相手になんでその 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. is_valid の中で範囲チェックしていますが、(valid_index + invalid_index) // 2 の関係からはみでることはないのではないですか。 この場合は、valid_index は valid であると確認された最小のインデックスで、invalid_index は invalid であると確認された最大のインデックスですね。これはこれで一つの案でしょう。 もしも興味があるならば、色々な言語の標準ライブラリーを調べてみたらいかがですか。きちんとした人が作って色々なテストをしているでしょうから。 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. あ、はい。is_valid(-1) という呼び出しは起きうるか。ない。なぜならば、invalid_index == -1 のときに、valid_index >= 1 だからだ、という話をしています。 ライブラリーのコードはどういう環境で動くか分からないので、比較的速度が優先される傾向があるでしょう。 |
||
|
|
||
| ```python | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| if not nums: | ||
| return 0 | ||
|
|
||
| lo = -1 | ||
| hi = len(nums) | ||
|
|
||
| while not hi - lo < 2: | ||
| mid = (lo + hi) // 2 | ||
| if nums[mid] < target: | ||
| lo = mid | ||
| else: | ||
| hi = mid | ||
|
|
||
| return lo + 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. (メモ)
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. レビュー、質問ありがとうございます。大変助かります。。
はい。解釈合っています。
になるように意識していて、
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.
この場合、
と変わり、targetと同じ値がnumsに複数含まれる場合、初めて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.
あくまで個人の好みの問題なのですが、 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<=mid<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 > right となると思います。そして left が指し示す境界のすぐ右側の値の位置が、求める位置になります。
で考え、かつ区間を閉区間で考えた場合、最後の状態では left == right となると思います。このとき、値が丁度一つだけ区間に含まれる状態になり、その値の位置が求める答えとなります。ただし、左端が求める答えとなる場合、この考え方だとそれを出力することができません。
以下も参考にすることをお勧めいたします。
https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562
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.
@nodchip
丁寧なご説明、リンクの共有もありがとうございます。おかげさまで理解できた気がします。
理解の確認のため、以下の指摘の解釈と、二分探索の考え方の流れに違和感がないかをご確認していただけないでしょうか?
FalseとTrueの境界の位置を求めようとした時、nums[left:right + 1]の要素(rightを含む)の中に境界があるとして考えると、ループで評価する要素が空になるのは
left >= right + 1でleft > rightとなり、区間の定義と矛盾するように感じられたということであっていますでしょうか?
続いて、二分探索の考え方の流れですが、例えば、
FalseとTrueの境界の位置を求めたいFalseの時はlen(nums)を返したいnums[left:right]の要素(rightを含まない)の中に境界があり、nums[:left]の要素は全てFalse、nums[right:]の要素は全てTrueが満たされるように評価する範囲を狭めていくことにするとなれば、二分探索を考える流れとして違和感なさそうでしょうか?
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.
あくまで個人的なイメージなのですが、閉区間で考えた場合、頭の中で以下のように考えています。

上記は、区間の中に 2~5 まで含まれている様子を表します。
left > right となったとき、以下のようになります。

このとき、左カッコが右カッコより右に来てしまっているのが、なんとなく違和感を感じます。
繰り返しになりますが、このように感じるのは多数派ではないと思います。