二分探索の実装
半閉区画
最初に見つけたtargetのインデックスを返す場合
半閉区画の場合、right
は常に探索済みの範囲に含める必要がある。そのため範囲外のインデックス(len(nums)
)で初期化する必要があるし、すでに探索済みの値(mid
)で更新する必要がある。またleft == right
になるということはleft
も探索済みとなり、全ての探索が終了したことがわかるので、探索を終了する必要がある。
target
未満の値の場合はfalse
、target
以上の値の場合はtrue
だとすると、探索対象の数列は[false, false, ..., true, true, ...]
のようなbool値の集合と捉えることができる。false
とtrue
の境界にいる場合はleft
はmid + 1
でfalse
からtrue
に更新される。一方で、right
はmid
がtrue
の場合にmid
で更新されるので、常にtrue
の範囲内に居続ける(インデックスの範囲外もtrue
とみなすなら)し、いきなりright
がleft
を飛び越える(right < left
)ことはない。そのため、下記のように書くこともできる。
境界の右側を返す場合
ところで同じ二分探索でも、同じtarget
ならどのtarget
でも良いのか、それとも最初のtarget
(false
とtrue
の境界)を返したいのかで実装が変わってくる。これまでに提示した2つの例はどちらも同じtarget
ならどのtarget
でも返して良いという考えのもとつくられた実装である。この場合、例えばnums := []int{0, 0, 1, 1, 1, 1, 1, 1, 1, 1}; target := 1;
のとき、インデックス5が返される。一方で最初の1(インデックス2)を返して欲しい場合はこれまでの実装では適さない。
最初の1(インデックス2)を返す実装を作りたい場合は、最初のtrue
(false
とtrue
の境界)を求めてから、そのtrue
がtarget
かどうかを確かめるという方法を取れば良い。
right
がleft
を飛び越えることはないので、必ずleft == right
になった瞬間がfalse
とtrue
の境界だとわかる。left
がfalse
からtrue
の境界に入ったときに、right
がleft < right
の位置にいる可能性はあるが、その場合は必ずtarget < nums[mid]
になるので、left
は動かず、right
のみが更新されて徐々にleft
に近づいていき、最終的にleft
とright
は重なる。
left == right
になったとき、left
とright
はともに一番最初にあるtrue
にいるが、全てfalse
の場合はright
は範囲外のインデックスのまま動かないし、true
が存在するとしてもtarget
自体は存在しない可能性が考えられるので、このときのleft
またはright
の値がtarget
と一致するかを確認する必要がある。
閉区画
最初に見つけたtargetのインデックスを返す場合
対して閉区画の場合は、right
は常に未探索の範囲に含める必要があるため、len(nums)-1
で初期化し、mid + 1
で更新する必要がある。またright
もfalse
とtrue
の境界にいる場合はmid - 1
でtrue
からfalse
に更新されるため、left
とright
はtrue
の範囲内(最初のtrue
)で重なる場合もあれば、false
の範囲内(最後のfalse
)で重なる可能性もある。true
の範囲内で重なる場合は、必ずtarget < nums[mid]
となるので、left
は動かず、right
だけが更新されてfalse
の範囲内に入ることになる。反対にfalse
の範囲内で重なる場合は、right
は動かず、left
がtrue
の範囲内に入ることになる。そのため、left == right
の時はまだ探索を続ける必要があり、right < left
となったときに初めて探索を終了することができる。
これは考えてみれば当たり前で、閉区画の場合right
は未探索の範囲に含まれているので、left == right
の時はまだ未探索の領域が残っているため、探索を続ける必要があると捉えることができ、right < left
となったときに初めて未探索の領域がなくなるので探索を終了することができるわけである。
ちなみに下記の実装ではnums := []int{0, 0, 1, 1, 1, 1, 1, 1, 1, 1}; target := 1;
のとき、インデックス4が返される。半閉区画と違ってright
がlen(nums)-1
で初期化されているためである。この実装はtarget
ならなんでも良いので返すという実装なので、どのインデックスを返そうがあまり大きな違いはない。
境界の右側を返す場合
right < left
となった瞬間、right
はfalse
の範囲内に入ってしまっているが、left
は必ず一番最初にあるtrue
にいる。そのため、下記のようにも書くことができる。一方で、閉区画のようにright
を返すことはできない。
Last updated