-
Notifications
You must be signed in to change notification settings - Fork 0
33 search in rotated sorted array medium #15
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?
Conversation
| if nums[left] <= nums[right]: | ||
| return 0 |
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行を取り除いて動くように書き換えられますか?
left と right の意味を説明できますか?
| if target < nums[shifted_index(left, shift)]: | ||
| return NOT_FOUND | ||
| elif target > nums[shifted_index(right, shift)]: | ||
| return NOT_FOUND | ||
| elif nums[shifted_index(right, shift)] == target: | ||
| return shifted_index(right, shift) |
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.
上と同じですが、この6行を取り除いて動くように書き換えられますか?
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.
(私も守りきれているかどうかはともかく)個人的にはレビューにおいて能力の確認はあまり好ましくないと思っています。
疑問文を使うときには提案か意図の確認がいいでしょう。
「X という方法もあるが、こちらのほうがよいのではないか。」「A B C の中で B を選んだようだが、どうしてそれがよいと考えたのか。」
これは建設的ですね。X という方法で通じると思っています。コードを書いてしまうかリンクなどをつければより親切です。
「Y という条件を満たす解法もあるが、君は思いつくことができるか。」これ能力の確認ですね。
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.
説明できないかもと思ったならば根拠まで示すのも1つです。たとえば、これは「1つの変数に、最低何枚で作れるかと、そもそも作れるかのふたつの意味を持たせようとしている」のでどう考えているか知りたくなりました。
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.
二分探索を理解されているか不安になり質問してしまいました。
確かにコードを書いてしまう方が親切ですね。
レビューコメントを書き直しました。
find_rotated_shift では (left, right] に最小値を取る index が含まれるように二分探索し、要素が一つになると right を返すようになっていると思います。left は初期値が 0 で、while right - left > 1:であるため right は left と重ならないので right は 0 になりえず、先頭が最小値の場合に見つけられません。left は left 以下に最小値がないことが確定している index を表しているので初期値を left = -1 とすれば、先頭が最小値の場合も含めて二分探索のコードで処理できます。
2番目も同様で(逆ですが)、[left, right) に traget があるとしたらここという index が含まれるように二分探索していると思います。先ほどと同じ理由で right の初期値を len(nums) - 1 とすると、left が len(nums) - 1 にはなりえないので target が末尾にある場合には見つけられません。right は right 以上に target がないことが確定している index を表しているため、right = len(nums) とすると、末尾が target の場合も含めて二分探索のコードで処理できます。
このような初期値とした方が見通しがよくなると思いますがいかがでしょうか?
class Solution:
def search(self, nums: list[int], target: int) -> int:
def find_rotated_shift() -> int:
left = -1
right = len(nums) - 1
while right - left > 1:
mid = (right + left) // 2
if nums[mid] <= nums[right]:
right = mid
else:
left = mid
return right
def shifted_index(index: int, shift: int) -> int:
return (index + shift) % len(nums)
shift = find_rotated_shift()
left = 0
right = len(nums)
while right - left > 1:
mid = (right + left) // 2
if target < nums[shifted_index(mid, shift)]:
right = mid
else:
left = mid
if nums[shifted_index(left, shift)] == target:
return shifted_index(left, shift)
return -1 # not foundThere 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.
こちらこそご指摘いただきありがとうございます。以後気を付けます。
@Kaichi-Irie
失礼いたしました。書き直したコメントをご確認いただけますと幸いです。
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さん
自分自身もレビューの際には能力の確認をしないよう気をつけます。
>tokuhiratさん
丁寧なコメントをありがとうございます。left=-1 やright=len(nums)から始めた方が見通しが良いのはその通りだと思います。しかし、問題を見てすぐの、手探りの状態でいきなりこの方法を取ると、バグを生んでしまいかねないと自分はまだ感じています。特に、Pythonだと負の数もindexに入れられるので、left=-1から始めてプログラムを実行させると、バグっていても動いてしまうことが割とあると感じています。そのため、step1ではこのような読みづらいコードになってしまっています。
一応、このコードの心は次のとおりです。
いずれの二分探索もindexは配列の要素の位置を表すとして、区間[left, right]に対して探索を行っているつもりで書いています。
不変条件は
- 1つ目の二分探索:
nums[left] > shift >= nums[right] - 2つ目の二分探索:
nums[left] <= target < nums[right]
です。これをはじめに満たすためにif文などで端を処理しています。
ちなみに個人的には最も書きやすい書き方(left=0, right=len(nums)-1→端の処理をして不変条件を満たす→不変条件を満たすようにwhile right-left>1: ..., left=mid, ..., right=mid →return left/ rightとテンプレ化している)だと感じています。また、閉区間の方が直感的に分かりやすいとも感じています。(問題にはよりますが)とはいえ、初めの条件分岐がadhocに見えて、読み手にも負荷がかかってしまって良くない書き方で改善したいと思っています。
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.
step1で読みやすいコードにならないことは問題なく、他の書き方を見て整理されたコードに変形していけることが重要と思っています。
コードの心の部分が読み取れなかったので念のため確認させてください。
冒頭の文(区間[left, right]に対して探索を行っている)と、初期値の設定(left=0, right=len(nums)-1)を見ると target があるとすれば left 以上 right 以下にあるように範囲を狭めるという設定と読みました。
一方で、不変条件を見ると right は target より大きい要素を指しているようで、終了条件は while right-left>1 であり left 以上 right 未満の要素が1つになると終了するようになっています。また、if target < nums[shifted_index(mid, shift)]: right = mid をみると、target の index になりえない mid を right としているので、こちらも right 未満に target が含まれるように区間を狭めています。
閉区間 [left, right] で考えている箇所と、半開区間 [left, right) で考えている箇所があるように見え、right が指しているものが区間に含まれるのかどうかわかりませんでした。どのような意味で 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.
コメントありがとうございます。
確かにおっしゃる通り、target/shiftを含む区間はいずれも半開区間ですね。これまで、二分探索で探索する区間についてきちんと言葉を扱えていなかったことがはっきりしました。気づけて良かったです。ありがとうございます。
| - 本問では`target`に一致するindexを探すので、添え字の区間を`[left,mid)`,`[mid,mid]`,`(mid,right]`の3つに分けて考えるとわかりやすい。 | ||
| - 基本的には閉区間でインデックスを指す二分探索がわかりやすいと感じている。 | ||
| - `while left <= right`を使った二分探索は初めて。 | ||
| - `while 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.
while left < right と書いても特定の値を見つけることができると思います。この場合、ループの中では return せず、ループが終わったあとに left (=right) の位置の値を調べ、特定の値と一致しているかどうかを調べることになります。
- left/right が要素自体を指しているか、要素と要素の間の境界を指しているか。
- 区間について考えるあたり、区間には要素が含まれているか、境界が含まれているか。
- left/right が指しているものは区間に含まれているか。
- mid の位置のものを調べたとき、それを狭めたあとの区間に含めたいか/含めたくないか。
- 最後の状態で区間の中には何が残っていて欲しいか。
- 要素が少なくなった時に無限ループしないか。
といった観点で考えて、その結果をもとにコードに落とし込んでみてはいかがでしょうか?
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.
レビューありがとうございます。
脳内にこのチェックリストが叩き込まれるように練習します!
| else: | ||
| right = mid - 1 | ||
|
|
||
| NOT_FOUND = -1 |
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.
定数を関数の最後で定義するのはあまり見ないように思います。クラス変数として定義してはいかがでしょうか?
| right = len(nums) - 1 | ||
| if nums[left] <= nums[right]: | ||
| return 0 | ||
| while (right - left) > 1: |
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.
() は外してよいと思います。
| if nums[mid] == target: | ||
| return mid | ||
|
|
||
| # [left, mid]がソート済み配列の場合 |
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.
読み手にとってやや分かりづらいロジックに対し、十分な量のソースコードコメントが書かれている点、好感が持てました。
問題へのリンク
Search in Rotated Sorted Array - LeetCode
言語
Python
問題の概要
rotateされたソート済み配列から特定の値
targetを検索する問題です。配列は回転されているため、通常の二分探索ではなく、回転された状態を考慮した二分探索を行う必要があります。自分の解法
step1
2回二分探索を用いて、回転されたソート済み配列から特定の値を検索する解法。1回目の二分探索で、配列の最小値を見つけ、
shift(どのくらい配列がrotateされたか) の値を求める。2回目の二分探索で、shiftを考慮して元の配列のインデックスを計算し、通常の二分探索でtargetを検索する。インデックスの計算が毎回必要になるため、少し複雑な実装になる。O(logn)O(1)step2
mid)で分割すると、[left,mid]もしくは[mid,right]のいずれか一方はソートされた配列になっている。」nums[left] <= nums[mid]の場合、左側の部分配列はソートされている。targetが左側にあるかどうかを確認し、そうであれば左側を探索する。もしそうでなければ左側はもう探索する必要がないので、この範囲を捨て、右側を探索するようにする。nums[left] > nums[mid]の場合、逆にnums[mid]<=nums[right]が成り立ち、右側の部分配列はソートされている。targetが右側にあるかどうかを確認し、もしそうであれば右側を探索する。そうでなければ右側はもう探索する必要がないので、この範囲を捨て、左側を探索するようにする。targetに一致するindexを探すので、添え字の区間を[left,mid),[mid,mid],(mid,right]の3つに分けて考えるとわかりやすい。while left <= rightを使った二分探索は初めて。while left <= right: 特定の値を「見つける」 ための探索。ループの 中で 答えが見つかる。答えが見つかり次第、値を返すので、ループを走査し終えたときは答えが見つからなかったことを意味する。while left < right: 条件を満たす「境界」(例: 挿入位置、最初のtrueなど)を探すための探索。ループが 終わった後 に答えが確定する。while (right-left)>1という条件で探索を行っていたが、これでは前処理が少し複雑になる。while left < rightに変更することで、よりシンプルに実装できるようになるが、leftとrightの更新がmid,mid+1,mid-1のいずれかになることに気をつける必要がある。(while (right-left)>1の場合は、midだけで更新できる)→めぐる式を採用すれば解決しそうだが、今度は読み手に伝わるかどうかが問題となる。step3
O(logn)O(1)別解 区間を分ける
こちらも二分探索を2回用いる解法である。最初に最小値のインデックス
min_indexを求めた後、min_indexを基準にして2つの区間に分けて探索を行う。具体的には、
nums[:min_index]とnums[min_index:]の2つの部分配列に分けて、それぞれに対して通常の二分探索を行う。時間計算量:
O(logn)空間計算量:
O(1)次に解く問題の予告