Skip to content

153. Find Minimum in Rotated Sorted Array#39

Open
seal-azarashi wants to merge 12 commits intomainfrom
find-minimum-in-rotated-sorted-array
Open

153. Find Minimum in Rotated Sorted Array#39
seal-azarashi wants to merge 12 commits intomainfrom
find-minimum-in-rotated-sorted-array

Conversation

@seal-azarashi
Copy link
Owner

public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
if (nums[left] < nums[right]) {
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

@seal-azarashi seal-azarashi Nov 18, 2024

Choose a reason for hiding this comment

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

@nodchip
そうですね、「左端に最小の値がある範囲が探索対象になったかどうかを確認する」という文言が思いつきました。ただ、left と right が同じ値だったときも左端に最小の値がある範囲を示しますので、これを書くなら if 文条件式の比較演算子は <= としたい気持ちになります。
合わせて以下のように修正してみましたが、いかがでしょうか。

...
        while (left < right) {
            // 左端に最小の値がある範囲が探索対象になったかどうかを確認する
            if (nums[left] <= nums[right]) {
                return nums[left];
            }

            ...
        }
...

While 文の条件式から、left と right は等しくならないので < としていましたが、もしかしたらこれのせいで直感的でなくなってしまっていたかもしれません。

Copy link

Choose a reason for hiding this comment

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

左端に最小の値がある範囲が探索対象になったかどうかを確認する

ええっと…。日本語として係り結びがどうなっているのか理解できませんでした…。
気持ちとしては、なぜこれで正しい処理になっているかを書いてほしかったです。
自分が書くとするならば、「区間の左端が最小値になった場合に早期 return する。」あたりかなと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ええっと…。日本語として係り結びがどうなっているのか理解できませんでした…。

申し訳ありません...。
書いて頂いた文言をそのまま使ってしまい恐縮ですが、「区間の左端が最小値になった場合」と言いたかったです🙇‍♂️

気持ちとしては、なぜこれで正しい処理になっているかを書いてほしかったです。
自分が書くとするならば、「区間の左端が最小値になった場合に早期 return する。」あたりかなと思いました。

補足ありがとうございます。


## Step 1

答えを知っていたので書けはしたのですが、手癖ではなく充分な理解を持った上で解けているか自信がないので、この解法に対する理解を以下に書き出します。フィードバックいただけますと嬉しいです。
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.

ありがとうございます!

答えを知っていたので書けはしたのですが、手癖ではなく充分な理解を持った上で解けているか自信がないので、この解法に対する理解を以下に書き出します。フィードバックいただけますと嬉しいです。

- 閉区間で二分探索を行い、最小の値から始まる範囲とそうでない範囲の境目を探す
- 例えば `[3,4,5,1,2]` の場合、最小の値から始まる範囲に属する値を T 、そうでないものを F とすると `[F, F, F, T, T]` となるが、この T の中で一番左側にあるものを探していくイメージ
Copy link

Choose a reason for hiding this comment

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

これの意図として、仮に先頭が最小だった場合は、全部 T ですかね?

下の方の条件との関係では、そうなのだとしたら、一番うしろとの比較になりそうです。
(結果的に、left <= middle < right が成立しているので、nums[right] との比較でもいいのですが。)

あと、nums が空だったときはどうなるかも一応考えておきますか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

@oda

これの意図として、仮に先頭が最小だった場合は、全部 T ですかね?

はい、そうです。

下の方の条件との関係では、そうなのだとしたら、一番うしろとの比較になりそうです。
(結果的に、left <= middle < right が成立しているので、nums[right] との比較でもいいのですが。)

確かにそうですね。改めて読み返すと Step 2 の右端の値を用いて比較を行うパターンの説明として書かれていた方がしっくり来るように思いました。

より適切な説明にするため、12行目と13行目をまとめて次の文にしようかと考えたのですが、違和感ありませんでしょうか。

- 閉区間で二分探索を行い、最小の値から始まる範囲の左端の値を探す

Copy link
Owner Author

Choose a reason for hiding this comment

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

@oda

あと、nums が空だったときはどうなるかも一応考えておきますか。

次のように配列のチェックを実装し、Javadoc も記載してみました。
Leetcode では使えないですが、The Apache Commons Lang 3 library の ArrayUtils.isEmpty() を使っています。

import org.apache.commons.lang3.ArrayUtils;

class Solution {
    /**
     * Find minimum value in rotated sorted array.
     * If the array is not sorted, the results are undefined.
     */
    public int findMin(int[] nums) {
        if (ArrayUtils.isEmpty(nums)) {
            throw new IllegalArgumentException("Argument nums must not be empty");
        }

        int left = 0, right = nums.length - 1;
        while (left < right) {
            int middle = left + (right - left) / 2;
            if (nums[middle] <= nums[right]) {
                right = middle;
            } else {
                left = middle + 1;
            }
        }
        return nums[left];
    }
}

Step 4 に残しています。

Copy link

Choose a reason for hiding this comment

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

最小の値から始まる範囲の左端の値を探す

「最小の値を探す」ならともかく、日本語としてあまり自然だとは思いませんが、書くならば入力がどのようなものであるかの制限から丁寧に書かないと伝わらないでしょう。

nums が空だったときはどうなるか

空かどうかの判定は、素直に .length 使ったほうがいいのではないでしょうか。

あ、いや、どっちかというとこの話をしている理由は、二分探索が回るための条件などを考えていると、自然と気になりそうだと思ったんですね。

Copy link
Owner Author

@seal-azarashi seal-azarashi Nov 19, 2024

Choose a reason for hiding this comment

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

@oda

「最小の値を探す」ならともかく、日本語としてあまり自然だとは思いませんが、書くならば入力がどのようなものであるかの制限から丁寧に書かないと伝わらないでしょう。

ありがとうございます。
こちらの指摘の内容と合わせて全体的に修正してみようと思います。
(追記: 修正しました: #39 (comment))

空かどうかの判定は、素直に .length 使ったほうがいいのではないでしょうか。

次のようなイメージでしょうか。

        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException("Argument nums must not be empty");
        }

元のコードは ArrayUtils.isEmpty() の仕様を知らないと処理内容、特に null チェックも行っているという点を推測するのが難しそうなので、確かにこちらの方が素直でわかりやすい実装に思いました。
(何か的外れなことを言っていたらご指摘ください)

あ、いや、どっちかというとこの話をしている理由は、二分探索が回るための条件などを考えていると、自然と気になりそうだと思ったんですね。

すいません、気にならなかったわけではないのですがプルリクエストには書き漏らしてしまっていました。今後ちゃんと書くように気をつけます。いつもご指摘ありがとうございます。

Copy link

Choose a reason for hiding this comment

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

ArrayUtils.isEmpty は周りで使われているか次第だと思います。

public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;
while (left < right) {
if (nums[left] < nums[right]) {
Copy link

Choose a reason for hiding this comment

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

並列して議論始めますが、
まず、nums[left] < nums[right]の条件が満たされる可能性があるのは、はじめのループか left が更新された直後ですね。
それだったら、left 更新の直後に書いたほうがいいかもしれません。

次、これによって、どれくらい速くなるかですが、たしかに再帰の深さは浅くなることがあることが期待されますね。
純粋に答えがどこにあるかが均一な分布だったとしましょう。
インデックスが2で何回割れるかによって浅くなるので、1/2は変わらず、1/4は1浅くなり、1/8は2浅くなり……となっていますね。これ、高校数学で無限和が計算できますが平均で1ですかね? 一方でループを回すたびに分岐がつくので極限を取るとこの仕組みがあったほうが遅いように見えます。

Copy link
Owner Author

@seal-azarashi seal-azarashi Nov 21, 2024

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。
left 更新の直後に書いたほうがいいのはその通りですね。right の更新後はチェックする意味がありません。

そして、そもそも極限を取るとこの仕組みがあった方が遅くなるというのも仰るとおりですね... ループを一回減らせても、ある程度の回数このチェックをするだけでその削減効果を帳消しにしてしまいそうなので、外した方が良いと思いました。

Comment on lines +14 to +19
- 各イテレーションで探索範囲の中央 (以下 middle) を確認し、次のどちらかの処理を行う:
- middle の位置にある値が right の位置にある値よりも小さい場合、最小の値はそこかそれよりも左の位置にあるため、right を middle の位置に移動させる
- 上の条件に当てはまらない場合、最小の値は middle よりも右の位置にあるため、middle のひとつ右の位置に left を移動させる
- 各ポインタは以下示しているため、同じ位置を指したら即ちその位置に存在する値が最小の値であると判断する:
- left: これが指す位置よりも左には最小の値が存在しないことを示す
- right: これが指す位置にある値が、最小の値かそれよりも右に存在するものであることを示す
Copy link

Choose a reason for hiding this comment

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

これ説明の順序として制約から出てきて欲しい気がしますね。

  1. 入力はこういう制約のものである
  2. そこに、left, right という変数を置く、これはこういう不変条件を満たす変数である
  3. ループを回すときに、middle という位置について計算をする、それは left, right との大小関係等こういう性質を持っている
  4. middle での値によって、left, right をこのように更新すると、不変条件を満たしたままにできる
  5. 更新をしていくと while をこういう条件で抜ける、この際にある式で表される値は left, right の不変条件から求めるものになっている
  6. これが必ず停止することは、ある値が狭義単調減少であることから言える

Copy link
Owner Author

Choose a reason for hiding this comment

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

@oda
ご指摘ありがとうございます。
上記に従って12行目から19行目までの文面を書き直してみました。指摘内容が汲めているかご確認頂けますと嬉しいです。


  • 与えられた配列から最小の値を二分探索する
    • 昇順に並んだ一意の整数値を何度か回転させた配列が入力として与えられる
      • 補足: 配列 [a[0], a[1], a[2], ..., a[n-1] を1度回転させると [a[n-1], a[0], a[1], a[2], ..., a[n-2]] となる
  • 処理の内容は次の通り:
    • 閉区間を指定する変数 left, right を置く:
      • それぞれ次の不変条件を満たす:
        • left: これが指す位置よりも左には最小の値が存在しないことを示す
        • right: これが指す位置にある値が、最小の値かそれよりも右に存在するものであることを示す
      • 初期値として、left には配列の左端、right には右端を指す添え字が格納される
    • 最小の値を探索するループ処理を実行する
      • 継続を指定する条件文は left < right
      • ループの中で閉区間の中央の値 middle を用いて left, right を更新する:
        • middle 次の式で算出される: left + (right - left) / 2
        • right と比較して、middle が指す値がそれと等しいかより小さい場合、right が middle の位置を指すように更新
          • right は初期値よりも大きい値を指すことはないので、その不変条件は満たされたままとなる
        • 上記を満たさない場合、right が指す値よりも middle が指す値の方が大きい場合は left が middle のひとつ右の位置を指すように更新
          • この場合 middle は最小の値ではない
          • middle よりも右のどこかに最小の値があるので、left の不変条件 (これより左には最小の値が存在しないこと) は満たされたままとなる
      • ループを抜ける際は left == right になる
        • left, right の不変条件から、left == right であるときこれらが指す値は最小の値となる
          • これより左には最小の値が存在しないため、right が指す値が最小の値であることが確定する
      • 区間の長さ (right - left) はループごとに必ず短くなるため、ループは必ず停止すると言える
    • 見つかった最小の値を返す

Copy link

Choose a reason for hiding this comment

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

ありがとうございます。

いや、実は違和感が払拭されていないのですが、しかし、このまま文章を生成され続けていたとしても解消されない気がしています。

どのあたりに違和感を感じているのか、なんですが、まず

「2で割る処理がありますがこれは切り捨てでも切り上げでも構わないのでしょうか。」
「nums[middle] <= nums[right] とありますが、これは < でもいいですか。」
「nums[right] は、nums[nums.length - 1] でもいいですか。」
「right の初期値は nums.length でもいいですか。」

これ、組み合わせて16通りのソースコードが生成できますが、どれが動いてどれが動かないか答えられますか。

Copy link
Owner Author

@seal-azarashi seal-azarashi Nov 21, 2024

Choose a reason for hiding this comment

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

@oda
ご指摘ありがとうございます。
動くのは次の5通りのパターンで、他は動きません。

  1. 元のソースコード
  2. nums[middle] を nums[nums.length - 1] と比較する
  3. nums[middle] を nums[nums.length - 1] と比較し、かつ right の初期値を nums.length にする
  4. if 文の比較演算子を < にする
  5. if 文の比較演算子を < にし、かつ nums[middle] を nums[nums.length - 1] と比較する

上記に対する返答としてはこれだけですが、実は全パターン試し、それぞれの結果について考えていました。その中で大事だと思ったものを書き出しておきます。

  • While 文内では middle がそれ自身よりも右にある値が小さいかどうかを判定したいので、比較対象として常に右端の値を用いて良い
    • Step 2 の右端の値を用いて比較を行うパターンが問題なく動くのはこのため
  • (while 文、if 文それぞれの条件文の内容が元のソースコードのままであったとして) middle の比較対象が常にそれより右のどれかである限り:
    • 実装は停止性を持つことになる
    • left == right の判定は考慮する必要がない

Copy link

Choose a reason for hiding this comment

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

ありがとうございます。これが答えられるならば、少なくとも正しいモデルにはなってそうですね。

違和感の原因をさぐったところ「意味と操作がごちゃごちゃになっていて、自分が書いていないソースコードを日本語に変えたように見える」ということかなと思いました。

ただ、全体から違和感が出ていて、

「切り捨てているので、left <= midle < right」は今回のような協会の表現の仕方をするならば大事な情報なんですがないとか。

閉区間で二分探索を行い、最小の値から始まる範囲とそうでない範囲の境目を探す

これ、境界の表現の仕方が色々あるでしょうから、そこはっきりさせないと閉区間といわれてもと思います。
たとえば、境界がインデックス4と5の間にあるという風に両側で挟む。left は F 側の領域を指し、right は T の領域を指す、left = -1, right = nums.length - 1 からはじめて、2つが隣り合ったときの右側という求め方もできるでしょう。

Copy link

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.

そういうわけで閉区間というような専門用語によって、分かっていないのか伝わらないのかを迷うんですかねえ。
専門用語は圧縮しようとしている内容があってそれを圧縮したまま伝えられると思っているからそれを使うのであって、そういう状況かも確認せずに使うものではないという感覚ですかねえ。
しかし、それとは別に理解が整理されて入っていないようには見えています。

他に、自主性みたいなものがないようには思っていて、変数の意味は書いた人が決めるんですよ。
その決め方がいくつかあるんですが、それのキメラのような話をしているように見えますねえ。

Copy link
Owner Author

Choose a reason for hiding this comment

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

すいません、返信が大変遅くなりました。

@nodchip

補足すると、目的を設定し、目的に必要な手段を決め、手段を粒度の大きい部分から小さい部分に向かって検討していく、という思考ができていないように感じました。また、粒度の大きい部分を、木構造のように再帰的に小さい部分に分割して思考するということもできていないように感じました。この辺りを意識しながら思考の仕方を改善されることをおすすめいたします。

ご指摘ありがとうございます。引き続きこれら意識して取り組んでいきます。

@oda

そういうわけで閉区間というような専門用語によって、分かっていないのか伝わらないのかを迷うんですかねえ。
専門用語は圧縮しようとしている内容があってそれを圧縮したまま伝えられると思っているからそれを使うのであって、そういう状況かも確認せずに使うものではないという感覚ですかねえ。

ご指摘ありがとうございます。仰るとおりだと思います。

しかし、それとは別に理解が整理されて入っていないようには見えています。

他に、自主性みたいなものがないようには思っていて、変数の意味は書いた人が決めるんですよ。
その決め方がいくつかあるんですが、それのキメラのような話をしているように見えますねえ。

こちらも仰るとおりだと思います。全体通してこの傾向があるので引き続き改善できるように意識して取り組んで行こうと思います。
とりわけ、この前後のプルリクエストでは上記の傾向が顕著に出ていたと思います。実は当時プライベートで色々あって忙しく、睡眠時間を削って無理やりこの課題に取り組む時間を捻出していました。疲労と進捗が思うように出せない焦りから、よく考えずに横着して無理やり進めていたような状況になっており、それが用語の使い方だったり変数名選択の自主性の無さに表れていたように思います。
当たり前ですが、体に鞭打ってプルリクエストを出したところでこの取り組みの目的は達成出来ませんので、その時の状況に合わせて今後は無理のない範囲で取り組んでいこうと思います。お手数ですが引き続きよろしくお願いいたします。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants