Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions easy/35/answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Step1

かかった時間:2min

計算量:nums.length=Nとして

時間計算量:O(logN)

空間計算量:O(1)


```python
import bisect


class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
return bisect.bisect_left(nums, target)
```
思考ログ:
- bisect周りの仕様を復習しよう

シンプルに探索
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
for i, num in enumerate(nums):
if target <= num:
return i

return len(nums)
```
思考ログ:

再帰
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
if len(nums) == 1:
if nums[0] < target:
return 1
else:
return 0

split_index = len(nums) // 2
if target >= nums[split_index]:
return split_index + self.searchInsert(nums[split_index:], target)
else:
return self.searchInsert(nums[:split_index], target)
```
- クイックセレクトを思い出した

# Step2

講師役目線でのセルフツッコミポイント:

参考にした過去ログなど:
- https://github.com/Ryotaro25/leetcode_first60/pull/45
- https://github.com/seal-azarashi/leetcode/pull/38
- left/rightの位置について、何が言えるか
- 停止性問題
- https://github.com/Mike0121/LeetCode/pull/43
- https://github.com/Yoshiki-Iwasa/Arai60/pull/34
- https://github.com/nittoco/leetcode/pull/28
- bisectのコード
- https://github.com/python/cpython/blob/3.12/Lib/bisect.py
- https://github.com/fhiyo/leetcode/pull/42
- https://github.com/sakupan102/arai60-practice/pull/42
- https://github.com/rm3as/code_interview/pull/5
- https://github.com/shining-ai/leetcode/pull/41
- https://github.com/hayashi-ay/leetcode/pull/40
- Arrays.binarySearch(java)を使った解法
- bit反転と2の補数表現

bisect使わず2分探索
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
def is_larger_than_target(i: int) -> bool:
Copy link

@Ryotaro25 Ryotaro25 Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今まで気づかなかったのですがPythonにはInner Functionがあるのですね。

Avoid nested functions or classes except when closing over a local value other than self or cls.

https://google.github.io/styleguide/pyguide.html#26-nestedlocalinner-classes-and-functions
Google Guideにも記載ございましたが今回のように使うのですね!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これは、正直に言うと実は私の意識が足りていないです()
もう少し気をつけて使います。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(間が空いてしまいすみません)
is_larger_thanだと、target < num[i]のように思えてしまう気がします。
chatGPTに聞いたら、is_at_least_targetを提案してくれました。

return target <= nums[i]

left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if is_larger_than_target(middle):
right = middle
else:
left = middle + 1

return left
```
思考ログ:
- 二分探索についてはdiscord上でかなり議論されているので、別途検索をかけてみる
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

二分探索は何をループの不変条件とするかによっていろいろな書き方があるんですが、不変条件を理解せずに書いている人が多いということです。

理解せずに書くためには書き方を固定すれば、ある程度は書けるようになります。

書く時は自分の書き方を固定してもいいんですが、読むときには、不変条件をどのような形で書いてくるかを指定できないので、どんな書き方がされていても動くのか動かないのかも含めて読めないといけない、というように読むほうがだいぶ難しいという話かと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今回、色々見て回ってとても勉強になりました。

白状すると、二分探索は自分も書き方を固定して使っていました。
具体例をなぞってみたり、なぜそれで上手くいくのかを自分なりに理解した上で覚えていたつもりでしたが、不十分だったと思います。

そのせいで、型から外れたコードを読むのには、少なからず抵抗感があったと自覚してます。
所謂エンジニアリングをしようとしていなかったのだなと反省しました。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

まずは「何を探しているのか」「ループごとに今どこまで分かっているのか」「それをどう変数に表現しているのか」というのが意味の部分の話で、あとはループの中で「終了条件」「更新」「必ず終了すること」という形式操作の話くらいです。

だいたい、意味をすっ飛ばして形式だけでやろうとするので、「左」「右」「閉区間」などと唱えたら倒せると思っているんですが、もっと単純に、二分探索の仕事を途中で引き継いだら知りたいことは何なのか「昇順で並んでいる数字の中で一番左の400以上の数を探していて、いくつか確認した。重要な知見として、25番目が288で、51番目が789、その間は開いていない。」くらいですよね。で、この情報を圧縮して変数とコードにするだけです。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACすることが目的になれば、等号不等号の組み合わせを全探索したり、特定の型を暗記した方がコスパが良い、ということですよね。
エンジニアリングをするという目的で正しく報酬設計ができていれば、こういった意味の部分を蔑ろにすることに自然と抵抗感が生まれる。それがエンジニアリングの専門家集団界隈に入門した、ということなのかなと思っています。

- 二分探索について、もう少し一般化して思考の痕を残しておく
- とある条件を満たす集合Xと満たさない集合X^cを考える
- 今回のお題ではtarget以上の数が入っている配列のインデックス集合をXとおく
- 探索する配列(arrとする)の要素について、ある整数kが存在して、arr[i]∈X^c (i<=k)、arr[i]∈X (i>k)、とできるとする(前提条件)
- 今回のお題では配列がソートされているのでこの前提条件を満たす
- 探索インデックスの範囲を[left, right)と設定する
- mid = (left + right) // 2を計算し、midが条件を満たすかどうか確認して、探索範囲を更新する
- left未満のインデックスはX^cの要素、right以上のインデックスはXの要素となるように更新する
- mid∈X^cの時、left = mid + 1
- mid∈Xの時、right = mid
- 探索範囲は単調に縮まり最終的にleft==rightとなりループが停止する
- この時left==rightのインデックスは、Xの下限となっている(X^cの上限でもある)
- パラパラ漫画も作ってみた
- https://docs.google.com/presentation/d/1QABhB8fFeR5nt8x58dpSa3R3cNGzZ4Rcnmj-1cX74qA/edit#slide=id.p

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分ごとですが、二分探索はとても苦手(現在進行形で苦戦してます)なのでビジュアルの説明と思考の文書化はとても勉強になります🙇

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
一緒に克服しましょう💪

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

パラパラ漫画わかりやすかったです。
毎回ではないですが、自分はコード書いた後、エディタ上で下記のように手計算して検算しています。
l 0 ..
r 3 ..
m 1 ..


pivotをランダムにしてみる
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
def is_larger_than_target(i: int) -> bool:
return target <= nums[i]

left = 0
right = len(nums)
while left < right:
pivot = random.randint(left, right - 1)
if is_larger_than_target(pivot):
right = pivot
else:
left = pivot + 1

return left
```
思考ログ:

# Step3

かかった時間:2min

```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
def is_larger_than_target(i: int) -> bool:
return target <= nums[i]

left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if is_larger_than_target(middle):
right = middle
else:
left = middle + 1

return left
```
思考ログ:
- pythonでは心配はないが、オーバーフローを気にする場合はleft + (right - left) // 2の選択肢も頭においておく
- left <= middle < rightになっているのも大事なポイント

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このポイントですが常にこうなっているということでしょうか?
スライドの最後にL = Rになったのでループを抜けるとあるので、どのタイミングでなっているのか補足があればよりいいのかなと思いました。

自分は現在ここに苦戦していますが。。。🙇

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントありがとうございます。
middleを計算しているwhileループの中での話ですね。
つまりleft < rightの時、もっというとleft, rightは整数なので、left + 1 <= rightが成り立っている時です。

この時、left = ⌊(left + left + 1) / 2⌋ <= ⌊(left + right) / 2⌋ <= (left + right) / 2 < right
となっているので、left <= middle (=⌊(left + right) / 2⌋) < rightがいえると思います。


# Step4

```python
```
思考ログ: