Skip to content
Open
Show file tree
Hide file tree
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
16 changes: 16 additions & 0 deletions leetcode60/35:Search Insert Position/final.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1;
while (left <= right) { // 最後にrightとleftが交差して終わる形式。終了条件はright<left。
int mid = left + (right - left) / 2;
if (nums[mid] >= target) { // [False,....,False,True,.....,True]の棒を考え、一番左のTrueが答えになる。
right = mid - 1;
}
else {
left = mid + 1;
}
}
return left;
// 以下が終了状態。leftが答えになる。
// [False,....,False,True,.....,True]
// right, left
}
55 changes: 55 additions & 0 deletions leetcode60/35:Search Insert Position/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

# step1で感じたこと

30分かけて、エッジケースがうまくいかなかった(step1-draft)。
初稿では、left < rightの条件でwhile文を回したがうまくいかず。
時間計算量:O(log(n))、空間計算量:O(numsSize)
命名は、tailよりleft、headよりrightの方が明確。
2分探索自体のロジックはわかるが、headとtail、pivot(2分探索のための軸)での条件分岐において、境界条件が悩ましい。
while文で、left < rightにするか、left =< rightにするか。
head,tailをpivot近辺に移せばいいのはわかるが、どうすればエラーが全てのケースを網羅できるか難しい。
最後のループまで答えが見つからなかった場合、どのように答えを与えるか迷った。

# step2で感じたこと

空間計算量は、定数なのでO(1)であった。
c言語久々で忘れている、、
int* numsでアドレスを渡しており、nums[pivot]で参照元のアドレスに格納された値を参照している。
配列のメモリは呼び出し元ですでに確保されている。
*numsは配列の先頭の要素を取得、nums[pivot] == *(nums+pivot)。

他の人の回答を見てみると、閉区間、半開区間というか替えがあるらしい。<=と<の違いか。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

二分探索は様々なバリエーションがあります。どのような形でも読めるならば問題ないかと思います。
何を探しているのか、left, right が一体どういう条件を示す変数だと思っているのか、などが拾えるかということです。
結構よく話す内容なのでコメント集にも結構あります。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.c15qprmvxkc2

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コメント集ありがとうございます。
「left,rightの使い方や条件式」から伝わるニュアンスと、自分の実装方針が一致しているかということが大事だと理解できました。
とりあえず、自分がしっくりきている実装方法を一つ確立できました。

int searchInsert(int* nums, int numsSize, int target) {
    int left = 0, right = numsSize - 1;
    while (left <= right) { // 最後にrightとleftが交差して終わる形式。終了条件はright<left。
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) { // [False,....,False,True,.....,True]の棒を考え、一番左のTrueが答えになる。
            right = mid - 1;
        }
        else {
            left = mid + 1;
        }
    }
    return left;
// 以下が終了状態。leftが答えになる。
// [False,....,False,True,.....,True]
//             right, left
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

はい。

この場合、right とは、そこより右(含まない)がすべて True だと確信している場所、left とは、そこよりも左(含まない)がすべて False だと確信している場所ですね。

left, right の意味付けはコードごとに変わります。どういう順序で読み解くかに留意しながら色々と読んでみましょう。


正直、left <= rightで一回書いて仕舞えば、脳死で欠けてしまうので(頭使ってない)、left < rightで書いてみることにした。

これが試作バージョン

```c:step2.c
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1, pivot = 0;
while (left < right) {
pivot = left + (right - left) / 2;
if (nums[pivot] == target) return pivot;
else if (nums[pivot] > target) right = pivot;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

私は、ここの else 消しますね。上で return しているのでなくても同じです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

確かに消しても問題なさそうですね。
一番上のif文を、下の二つの条件分岐から独立した処理にできるので、思考の処理を減らせる気がします。

else left = pivot + 1;
}
return left;
}
```

この場合の特徴は、else if (nums[pivot] > target) right = pivot;の部分であると思う。
left == rightのループを許していない分、rightの移動をドラスティックにしすぎると、いきなりright==leftになりleftとtargetの大小比較ができなくなる。逆の条件分岐では、else left = pivot + 1;となり、閉区間手法と同じである。終盤で探索範囲のサイズが2になった場合に、left == pivotになるので、else left = pivot;だとループし続けてしまう。

しかしながらこのプログラムだと、右端にtargetが挿入される場合(たとえば、[1,3,5,7]に対して、target==8だった場合)うまく機能しない。
このプログラムは最終的に、left,right,pivotが一致する形で終了する。targetはnums[pivot]のみと比較される。
よって、上記の具体例の場合、最後[5,7]に対してtarget(==8)の挿入位置を検討するが、pivot(==5)と比較を行うものの、right(==7)とは比較しない。よって、index3に入れるのかindex4に入れるのか、検討することができない。

right = numSizeとすることで解決できることがわかった。
半開区間なので、rightを一つ多くとってあげると、右端まで全て検討してあげることができる。

# step3で感じたこと

これくらいの規模のコードだと、深く考えずに脳死で書けてしまうので、閉区間、半開区間それぞれ2回ずつ書いた。
3回連続失敗せずに書くというのが、意外とストレスが溜まる作業だとわかった。
脳死で書いてしまうと、タイポが発生してやり直しになるので、頭を使って書くための制約なのだろうか。
結局、初回ということもあり、ここまでで5時間かかってしまった。。。
22 changes: 22 additions & 0 deletions leetcode60/35:Search Insert Position/step1-draft.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
int searchInsert(int* nums, int numsSize, int target) {
int tail = 0;
int head = numsSize - 1;
int res = 0;

while (tail < head) {
int pivot = (tail + head) / 2 + 1;
if (nums[pivot] == target) {
res = pivot;
return res;
}
else if (nums[pivot] > target) {
head = pivot - 1;
res = pivot;
}
else {
tail = pivot;
res = pivot + 1;
}
}
return res;
}
10 changes: 10 additions & 0 deletions leetcode60/35:Search Insert Position/step1.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize - 1, pivot = 0;
while (left <= right) {
pivot = left + (right - left) / 2;
if (nums[pivot] == target) return pivot;
else if (nums[pivot] > target) right = pivot - 1;
else left = pivot + 1;
}
return left;
}
10 changes: 10 additions & 0 deletions leetcode60/35:Search Insert Position/step2.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize, pivot = 0;
while (left < right) {
pivot = left + (right - left) / 2;
if (nums[pivot] == target) return pivot;
else if (nums[pivot] > target) right = pivot;
else left = pivot + 1;
}
return left;
}
21 changes: 21 additions & 0 deletions leetcode60/35:Search Insert Position/step3.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize, pivot = 0;
while (left < right) {
pivot = left + (right - left) / 2;
if (nums[pivot] == target) return pivot;
else if (nums[pivot] > target) right = pivot;
else left = pivot + 1;
Comment on lines +5 to +7
Copy link
Copy Markdown

@olsen-blue olsen-blue Apr 20, 2025

Choose a reason for hiding this comment

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

条件チェックが3つありますが、1つ目と、2&3つ目は意図が違うので、分離させたい気がします。

if nums[pivot] == target:
    return pivot
    
if nums[pivot] > target:
    right = pivot
else:
    left = pivot + 1

}
return left;
}

int searchInsert(int* nums, int numsSize, int target) {
int left = 0, right = numsSize, pivot = 0;
while (left < right) {
pivot = left + (right - left) / 2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

変数の宣言位置をここにして(pivot->int pivotにして)変数のスコープを狭めた方がいいと思います。
使わない場所でも変数を覚えておかないといけなくなるからです。
また元のコードでも0で初期化する必要はないと思います。
さらに言えば、僕はpivotよりmid(dle)の方が好きです。
pivotであるのは使われ方から分かっているので、それ以外の情報を伝えてほしいからです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます!
変数のスコープを厳密に気にしていませんでした。
そしてmidの方が役割が明確になりますね。

if (target == nums[pivot]) return pivot;
else if (target < nums[pivot]) right = pivot;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

参考までに今はぶら下がりよりブロック{}で囲むほうがスタイルとしては(多分)人気のようです。
フォーマットに関してはc言語だとclang-formatとかを導入してVSCode等で保存時に自動フォーマットを掛けるといいと思います。
philip82148/leetcode-swejp#9 (comment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ぶら下がりだと
if ...
if ...
else ...
の場合に最後のelseがどっちのifにかかっているのかわからないので避けるべきということですよね。今回はelse ifなのでわかりますが

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

なるほど、そのような理由なのですね。ありがとうございます!
今後{}を使用しようと思います。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

僕は「<ループ毎に変わるのもの>は<変わらないもの>に一致するか」と考えたいので、これらの条件式でtargetを右辺にした方が読みやすいと感じます。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

おっしゃることも一理あると考えます。
自分が今回このように書いた理由は、小さいものを左で書いてあげる(>ではなく<を用いる)と、配列のソート順と一致するので、視覚的に分かりやすかったからです。

ただ、一般的には<変わらない閾値>を右に置くというのがセオリーですよね。
ここら辺のベストプラクティスはよくわかっていません。

else left = pivot + 1;
}
return left;
}