-
Notifications
You must be signed in to change notification settings - Fork 0
35. Search Insert Position #39
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,5 @@ | ||
| start_new_problem.sh | ||
| main.go | ||
| go.mod | ||
| go.sum | ||
| *.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| 問題: https://leetcode.com/problems/search-insert-position/description/ | ||
|
|
||
| ### Step 1 | ||
| - Goの標準パッケージにBinarySearchがあるのでそれを使うことに | ||
| - BinarySearchは返り値がintとbool値の二つ。 | ||
| int型の返り値はtargetがあればそのtargetと同じ値の要素のうち最も若いインデックスが返り、 | ||
| targetがなければinsert positionが返る。 | ||
| bool値の返り値はtargetがあったかなかったかどうか | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| index, _ := slices.BinarySearch(nums, target) | ||
| return index | ||
| } | ||
| ``` | ||
|
|
||
| - ライブラリを使わずに実装する | ||
| - テストケース | ||
| - nums=[], target=1 -> 0 | ||
| - nums=[0], target=0 -> 0 | ||
| - nums=[0], target=-1 -> 0 | ||
| - nums=[0], target=1 -> 1 | ||
| - nums=[0,2,4,6], target=4 -> 2 | ||
| - nums=[0,2,4,6], target=6 -> 3 | ||
| - nums=[0,2,4,6], target=7 -> 4 | ||
| - nums=[0,2,4,6], target=-1 -> 0 | ||
| - nums=[0,2,4,6], target=3 -> 2 | ||
| - nums=[0,2,4,6,8], target=3 -> 2 | ||
| - このコードでは同じ要素が複数ある場合にどのインデックスを返すかが統一されないことに留意 | ||
| - nums=[0,0,0,0], target=0 -> 2 | ||
| - なんとなく半開区間によるインデックス管理を採用したが、 | ||
| 閉区間だとどうなるかstep2でやってみる | ||
| - あとは再帰も選択肢 | ||
| - データ数が少なければ前から順に見ていけば良い | ||
| - メモリへのアクセスが二分探索のようにランダムにならず、 | ||
| キャッシュがいい感じに使われるのでむしろこちらの方が速い場合もあると聞いたことがある | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| // left and right are half open index such that [left, right) | ||
| left := 0 | ||
| right := len(nums) | ||
| for { | ||
| if left == right { | ||
| return left // not found | ||
| } | ||
| middle := (left + right) / 2 | ||
| if nums[middle] == target { | ||
| return middle | ||
| } | ||
| if nums[middle] < 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. この部分は左と右で対称の処理となるため、 continue せず if else で書いたほうが分かりやすくなるかもしれません。 |
||
| left = middle + 1 | ||
| continue | ||
| } | ||
| right = middle | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| - 二分探索について、以前discordで議論されていたのを覚えていたので、まずそこから確認してみる | ||
| - https://discord.com/channels/1084280443945353267/1196498607977799853/1268762035173326959 | ||
| - middleを計算する際にinteger overflowを起こさないための工夫 | ||
| - https://github.com/Ryotaro25/leetcode_first60/pull/45#discussion_r1878268512 | ||
| - 何を探しているのかを明確にする | ||
| - https://discord.com/channels/1084280443945353267/1084283898617417748/1281994919417745528 | ||
| - 今回は同じ値の要素がないので、targetが存在すれば最初にtargetと一致する要素のインデックス、targetが存在しなければinsert positionを返す | ||
| - つまり、`nums[i] == target`or`nums[i] < target < nums[i+1]`となるiを返す | ||
| - ただし、`target < nums[0]`なら0を、`nums[len(nums)-1] < target`ならlen(nums)を返す | ||
| - discordで二分探索の考え方のステップについて書いてあったので、それに従って言語化してみる | ||
| - https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562 | ||
|
|
||
| #### 2a | ||
| - step1を言語化 | ||
| 1. 二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか? | ||
| - false: target未満、true: target以上として一番左のtrueの位置を求める問題として捉える | ||
| 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか? | ||
| - step1の解法だと範囲を狭めていきつつ、たまたまmiddleがヒットしたらmiddleを返すという仕様だった。 | ||
| これがいいのか悪いのかよくわからないが、ここは範囲をひたすら狭めていく方法でやってみる | ||
| 3. 範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? | ||
| - [left, right)という半開区間を狭めていく方法を使う | ||
| 4. 用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? | ||
| - 初期状態として、配列の全ての要素が[left, right)にちょうど含まれている必要があるので、 | ||
| left=0, right=len(nums) | ||
| 5. 用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? | ||
| - ループ不変条件は`nums[left] <= target < nums[right]`と`left < right` | ||
| - https://discord.com/channels/1084280443945353267/1196498607977799853/1269560324818731028 を見ると、終了条件から考えている | ||
| - 終了条件は、leftもrightも一番左のtrueを指している時、つまり、left==rightの時 | ||
| - こう考えると不変条件を`left != right`としても良いことになるが、 | ||
| 上記リンク先では`left < right`になっている。 | ||
| こちらの方がわかりやすい。 | ||
| 一応、left == rightにならずにright < leftになることはないので | ||
| `left != right`としても動くはずではある。 | ||
| (実際にleetcode上でもACした) | ||
| 6. 用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? | ||
| - まず、middleを取る | ||
| - `nums[middle] < target`、`target <= nums[middle]`の2パターンがあり得る | ||
| - パターン1: 範囲を[middle+1, right)に更新 | ||
| - パターン2: 範囲を[left, middle)に更新 | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| left := 0 | ||
| right := len(nums) | ||
| for left < right { | ||
| middle := left + (right-left)/2 | ||
| if nums[middle] < target { | ||
| left = middle + 1 | ||
| } else { | ||
| right = middle | ||
| } | ||
| } | ||
| return left | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b | ||
| - 閉区間 | ||
| - 1,2は2aと同じ | ||
| 3. 範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? | ||
| - 閉区間を使う | ||
| 4. 用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? | ||
| - 初期状態として、配列の全ての要素が[left, right]にちょうど含まれている必要があるので、 | ||
| left=0, right=len(nums)-1 | ||
| 5. 用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? | ||
| - ループ終了時に、false,false],[true,trueとなって欲しい([はleft, ]はright)ので終了条件は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. 閉区間を使うのであれば、ループ終了時に false,false,[true],true と、一番左の true だけが区間の中に含まれていて欲しいと思いました。
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. ご指摘ありがとうございます。配列が false,false,...,false の場合に困ると思って left と right が最終的に交差するように問題を設定したのですが、考え直してみて末尾にダミーの true を置いたら解決すると思い書き直してみました
func searchInsert(nums []int, target int) int {
left := 0
right := len(nums)
for left < right {
middle := left + (right-left)/2
if nums[middle] < target {
left = middle + 1
} else {
right = middle
}
}
return left
}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. 「境界を求める」と「一番左のtrueを求める」は同じ意味だと思っていたのですが違っていたのですね。 結局一番左のtrueを求める問題を解こうと思ったら閉区間を使っているつもりでも開区間を使った時のコードと同じになりました。 |
||
| - こうしないとinsert positionが右端の場合に困る | ||
| - 不変条件は`left <= right` | ||
| 6. 用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? | ||
| - まず、middleを取る | ||
| - `nums[middle] < target`、`target <= nums[middle]`の2パターンがあり得る | ||
| - パターン1: 範囲を[middle+1, right]に更新 | ||
| - パターン2: 範囲を[left, middle-1]に更新 | ||
| - ここで[left, middle]ではなくmiddle-1にしたのは、 | ||
| 5番の終了条件のようにleftとrightが逆転する瞬間がないといけないから | ||
| - と理解して一度納得したが`nums[left] <= target <= nums[right]`が不変条件にならないのは気持ち悪い | ||
| - 参考: https://github.com/Ryotaro25/leetcode_first60/pull/45/files#r1888663072 | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| left := 0 | ||
| right := len(nums) - 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. 答えが存在しうる範囲は[0, len(nums)]なので、最初にright=len(nums)-1としてバグが発生しないことを理解するのに時間がかかった |
||
| for left <= right { | ||
| middle := left + (right-left)/2 | ||
| if nums[middle] < target { | ||
| left = middle + 1 | ||
| } else { | ||
| right = middle - 1 | ||
| } | ||
| } | ||
| return left | ||
| } | ||
| ``` | ||
|
|
||
| #### 2c | ||
| - 再帰 | ||
| - スタックフレームが積まれていくので空間計算量はO(log n)になる | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| var searchInsertHelper func(left int, right int) int | ||
| searchInsertHelper = func(left, right int) int { | ||
| if left == right { | ||
| return left | ||
| } | ||
| middle := left + (right-left)/2 | ||
| if nums[middle] < target { | ||
| return searchInsertHelper(middle+1, right) | ||
| } else { | ||
| return searchInsertHelper(left, middle) | ||
| } | ||
| } | ||
|
|
||
| return searchInsertHelper(0, len(nums)) | ||
| } | ||
| ``` | ||
|
|
||
| #### 2d | ||
| - 前から舐めていく | ||
| - 入力が小さければこれでも良い | ||
| - TLEするかと思ったら通った | ||
| - 入力サイズが高々10^4なので、1e4 / 1e8 = 1e-4 -> 0.1msでできてしまう | ||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| for i, n := range nums { | ||
| if n < target { | ||
| continue | ||
| } | ||
| return i | ||
| } | ||
| return len(nums) | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| - 半開区間のループ | ||
| - step2では「答えが含まれる範囲を狭めていく問題」と捉えることを重視して | ||
| nums[middle] == targetの時にすぐ答えを返すということをしなかったが、 | ||
| 今回のようにstrictly ascendingならそうした方が直感的だと思った | ||
| - 停止性についての確認の仕方 | ||
| - https://github.com/seal-azarashi/leetcode/pull/38#discussion_r1845409634 | ||
|
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. そうですね。 |
||
|
|
||
| ```Go | ||
| func searchInsert(nums []int, target int) int { | ||
| left := 0 | ||
| right := len(nums) | ||
| for left < right { | ||
| middle := left + (right-left)/2 | ||
| if nums[middle] == target { | ||
| return middle | ||
| } | ||
| if nums[middle] < target { | ||
| left = middle + 1 | ||
| } else { | ||
| right = middle | ||
| } | ||
| } | ||
| return left | ||
| } | ||
| ``` | ||
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.
このforはGO特有の書き方なのですね。
個人的には他の言語のwhileに合わせて条件を for left < right {...} として最後にreturn leftをしたくなります。
これはGO言語では一般的に使われるものでしょうか?(GOのことあまり知らずすみません)🙇♂️
気になった理由としては、44行目にのif文の条件を必ず満たすのか直感的にわかりづらかったためです。
調べたところ大丈夫そうですがコンパイラがエラーを吐かないのかなと思いました。
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.
おっしゃる通り自分も後でコードを見返した時に同じことを思ったのでstep3で「条件を for left < right {...} として最後にreturn left」するコードに変えました。Go言語の一般的な書き方かどうかというよりも、単に自分がわかりにくい書き方をしていました
無限ループをコンパイラがどう扱うのかについて自分も気になって色々試してみたところ、不思議な現象が起きました。まず、step1のコードのように無限ループを回してある条件を満たすとreturnするという場合にコンパイルエラーは生じません。さらに、下記コードでもコンパイルエラーは生じませんでした。int型を返す関数なのにreturn文がないよ、というエラーが出ることを期待したのですが、正常にコンパイルできてしまいました。"golang how compiler treats infinite loop with no return"などと調べてみても情報は見つからなかったです、、
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.
@hroc135
説明いただきありがとうございました。step2以降は直感的にもわかりやすいと感じました。
自分の調べた限りでもGOだとコンパイラ可能ということしか見つけられませんでした。。。