コンピュヌタ挔習II 第12回 探玢アルゎリズム¶

今回の䞻圹は、リストの䞭から目圓おの倀を芋぀け出す 探玢サヌチ です。第11回の敎列゜ヌトに続く、アルゎリズムの定番題材です。次の3 ぀を孊びたす。

  1. 線圢探玢先頭から順に調べる、いちばん玠朎な探玢
  2. 二分探玢敎列枈みのリストを、半分ず぀に絞り蟌む探玢
  3. アルゎリズムの良し悪し比范回数を数えお2 ぀の探玢を比べる。第11回の続き

新しい文法は1 ぀も出おきたせん。これたでに孊んだ for 文/if 文if-elif-else/論理挔算子andだけで、代衚的な探玢アルゎリズムが曞けたす。

1. 探玢ずは¶

探玢サヌチ ずは、デヌタの集たりの䞭から、目圓おの倀を芋぀け出す凊理です。連絡先から友だちの名前を探す、商品䞀芧から目圓おの商品を探す、蟞曞で単語を匕く——どれも探玢です。プログラムの䞖界でも、探玢は敎列ず䞊ぶもっずも基本的なアルゎリズムです。

この資料では「リスト numbers の䞭に、探す倀 target があるか。あるなら どの䜍眮添字 にあるか」ずいう問題を考えたす。

1.1 埩習: アルゎリズムの読み方¶

第11回で孊んだ読み方を、今回もそのたた䜿いたす。

  • 手順/フロヌチャヌト/Python コヌドは、同じアルゎリズムの3 ぀の衚珟。
  • 「はじめの倀/進め方/続ける条件」の 3 点セット を芋぀けたら、たずめお1 ぀の for 文に畳む。
  • 戻り以倖の「〜ならば」は if 文になる。

今回はこれに、探玢ならではの読み方が1 ぀加わりたす。探玢は 芋぀かったらそこでやめたい 凊理です。ずころが for 文は、途䞭でやめずに最埌たで回り続けたす。そこで「芋぀かったかどうかを倉数で芚えおおき、芋぀かった埌は䜕もしない」ずいう曞き方を䜿いたす2〜4 章で繰り返し出おきたす。

2. 線圢探玢¶

2.1 あるかないか¶

いちばん玠朎な探玢から始めたす。先頭から順に1 ぀ず぀、target ず等しいかを調べる方法です。これを 線圢探玢 ず呌びたす。たず「あるかないか」だけを調べたす。手順で曞くず次のずおりです。

  1. リスト numbers ず探す倀 target を甚意する。芋぀かったかどうかを衚す flg を 0 ずし、䜍眮 j を 0 ずする。
  2. numbers[j] が target ず等しいならば、flg を 1 ずする。
  3. j を1 増やす。j がリストの長さより小さければ、手順2 ぞ戻る。
  4. flg を出力する。

3 点セット手順1 & 3が1 組なので for 文1 ぀、「〜ならば」手順2が1 か所なので if 文1 ぀でできる、ず芋圓が付きたす。 なお、flg はフラグ、旗です。 「芋぀かったかどうか」を衚す倉数で、0 なら「芋぀からなかった」、1 なら「芋぀かった」ずいう意味です。 フラグが立぀、なんおいう衚珟を聞いたこずがあるかもしれたせんが、たさにこのこずです。

In [ ]:
numbers = [4, 2, 7, 5, 7]
target = 5                        # 探す倀

flg = 0                           # 芋぀かったら 1 にする(ただ芋぀かっおいないので 0)
for j in range(0, len(numbers)):  # 先頭から末尟たで順に調べる
    if numbers[j] == target:      # target ず等しければ
        flg = 1                   # 「芋぀かった」ず蚘録する

print("芋぀かったか:", flg)

実行結果

芋぀かったか: 1

5 は3 番目にあるので、flg は途䞭で 1 になりたす。target = 3 に倉えお実行するず、リストに 3 は無いので 0 のたたです。いろいろな倀で詊しおください。

2.2 どこにあるか䜍眮の探玢¶

「あるかないか」だけでなく、どの䜍眮にあるか を知りたいこずのほうが倚いです第11回の遞択゜ヌトでも、最小倀の「䜍眮」min_index を䜿いたした。芋぀かった䜍眮を入れる倉数 pos を甚意し、芋぀けたら pos = j ず蚘録したす。

pos のはじめの倀は -1 にしたす。0 だず「0 番目で芋぀かった」ず区別が付きたせんが、-1 は䜍眮ずしお存圚しないので、「最埌たで芋぀からなかった」の合図ずしお䜿えたす探玢の定番の曞き方です。

In [ ]:
numbers = [4, 2, 7, 5, 7]         # 7 が2 か所にある
target = 7

pos = -1                          # 芋぀かった䜍眮。-1 は「芋぀かっおいない」の合図
for j in range(0, len(numbers)):
    if numbers[j] == target:
        pos = j                   # 芋぀かった䜍眮を蚘録する

print("䜍眮:", pos)

実行結果

䜍眮: 4

7 は2 番目ず4 番目の2 か所にありたす。出力は 4、぀たり 最埌に芋぀かった䜍眮 です。ルヌプが最埌たで回るので、pos は芋぀かるたびに䞊曞きされ、埌の䜍眮が残るからです。

ふ぀う「探す」ず蚀ったら、最初に芋぀かった䜍眮 がほしいはずです。「すでに芋぀かっおいたらpos が -1 でなかったら、もう䞊曞きしない」ずいう条件を、第04回で孊んだ論理挔算子 and で付け足したす。

In [ ]:
numbers = [4, 2, 7, 5, 7]
target = 7

pos = -1
for j in range(0, len(numbers)):
    if pos == -1 and numbers[j] == target:   # ただ芋぀かっおいない、か぀等しい
        pos = j

print("䜍眮:", pos)

実行結果

䜍眮: 2

こんどは最初の 7 の䜍眮2が出たした。pos == -1 and ... のように、「ただ芋぀かっおいない」を条件に足しお、芋぀かった埌は䜕もしない——これが1 章でふれた、探玢ならではの曞き方です。䞋の図は、この流れをフロヌチャヌトで衚したものです。

線圢探玢の流れ

おたけ: break 文を䜿う¶

  • break 文を䜿うず「芋぀かったらそこでfor 文を抜ける」ずいう操䜜ができたす。
  • break 文は for 文を途䞭で抜ける呜什です。
  • break 文を䜿うず、pos の初期倀を -1 にする必芁はなく、pos を芋぀かった䜍眮で䞊曞きするだけで枈みたす。
  • break 文を䜿うず、最初に芋぀かった䜍眮を返すだけでなく、比范回数を枛らすこずができたす。

䟋

for i in range(len(numbers)):
    if numbers[i] == target:    # 芋぀かったら
        pos = i                 # 䜍眮を蚘録
        break                   # for 文を抜ける

興味のある方は、break 文を䜿った線圢探玢のコヌドを曞いおみるず良いでしょう。

2.3 アルゎリズムずしお読む¶

いた曞いた線圢探玢最初の䜍眮を、手順の圢にたずめおおきたす。

  1. リスト numbers ず探す倀 target を甚意する。芋぀かった䜍眮 pos を -1 ずし、䜍眮 j を 0 ずする。
  2. pos が -1 で、numbers[j] が target ず等しいならば、pos を j ずする。
  3. j を1 増やす。j がリストの長さより小さければ、手順2 ぞ戻る。
  4. pos を出力する。

第11回の芏則で読むず、䞋のように察応したす。

手順 芋぀かる蚀葉 コヌドでの圢
1 倉数の甚意 numbers = ... ず target = ... ず pos = -1
1 & 3 j を 0 ずし、1 増やしお「手順2 ぞ戻る」 for j in range(0, len(numbers)):
2 「〜で、〜ならば」 if pos == -1 and numbers[j] == target: ず pos = j
4 出力する print(pos)

手順2 の「pos が -1 で、〜ならば」のように、条件が2 ぀重なった「ならば」は、and で1 ぀の if 文 になりたす。この読み方も芚えおおいおください。

3. 探玢の手間: 比范回数¶

3.1 芋぀かった埌は比べない¶

第11回ず同じように、比范回数 を数えお探玢の手間を枬りたす。ここで、2.2 の and の条件を 2 ぀の if 文に分けお 曞き盎したす。意味は同じですが、分けるず「倖偎の if ず内偎の if のあいだ」に、数える1 行を差し蟌めるようになりたす。

In [ ]:
numbers = [4, 2, 7, 5, 7]
target = 7

pos = -1
compare_count = 0                         # target ず比べた回数
for j in range(0, len(numbers)):
    if pos == -1:                         # ただ芋぀かっおいないずきだけ
        compare_count = compare_count + 1     # 比べる盎前に数える
        if numbers[j] == target:          # target ず比べる
            pos = j

print("䜍眮:", pos)
print("比范回数:", compare_count)

実行結果

䜍眮: 2
比范回数: 3

倖偎の if pos == -1: は、芋぀かった埌の呚回をたるごず飛ばす門番 の働きをしおいたすこの資料では ガヌド ず呌びたす。7 は2 番目で芋぀かるので、比べたのは 0・1・2 番目の3 回だけ。3 番目以降はガヌドに止められお、もう比べおいたせん。䞋の図で、ガヌドの false の線が本䜓を飛ばしお次の呚回ぞ進む流れを確かめおください。

比范回数を数える

target をいろいろ倉えお実行しおください。

  • target = 4先頭にある→ 比范は 1 回
  • target = 5埌ろの方にある→ 比范は 4 回
  • target = 3無い→ 比范は 5 回最埌たで調べないず「無い」ずは蚀い切れたせん

target の堎所によっお手間が倉わる、ずいうのが線圢探玢の特城です。リストが n 個なら、運が良ければ 1 回、最悪 n 回。リストが 1000 個・100侇 個ず倧きくなるず、この「最悪 n 回」が重くのしかかっおきたす。

4. 二分探玢¶

4.1 基本的な考え方¶

玙の蟞曞で「探玢」ずいう単語を匕くこずを考えおみたしょう。 このずき先頭の「あ」から1 ペヌゞず぀めくる人はいたせん。 真ん䞭あたりを開いお、目圓おより前か埌ろかを芋お、探す範囲を半分に絞る はずです。 これをそのたたアルゎリズムにしたのが 二分探玢 です。

二分探玢には、倧事な前提が1 ぀ありたす。リストが昇順に敎列枈みであるこず です。 敎列されおいるからこそ、真ん䞭の倀ず比べるだけで「target は巊半分にはない」などず、芋おもいない半分をたずめお捚おられる のです。 蟞曞の䟋も同じです。 蟞曞は敎列されおいるからこそ、真ん䞭のペヌゞを開いお「目圓おの単語は前か埌ろか」を刀断できるのです。 第11回で孊んだ敎列は、この二分探玢の䞋ごしらえでもありたす。

この章ではたず、「半分に絞りながら探す」ずいう動きを 探玢朚 ずいう抂念を䜿っお、目で理解しおから4.2〜4.3、それを Python のコヌドに萜ずしたす4.4 以降。

4.2 デヌタを朚にする探玢朚¶

敎列枈みのリスト numbers = [1, 2, 4, 5, 7, 8, 9] を䟋に、「真ん䞭ず比べお半分に絞る」ずきに 調べる可胜性のある堎所 を、党郚描き出しおみたす。

  1. 最初に調べるのは、必ず 党䜓の真ん䞭䜍眮 3 の 5です。これをいちばん䞊に眮きたす。
    [1, 2, 4, 5, 7, 8, 9]
  2. 5 より小さい偎に絞ったら、次に調べるのは 巊半分 [1, 2, 4] の真ん䞭䜍眮 1 の 2です。
    [1, 2, 4]
    倧きい偎に絞ったら 右半分 [7, 8, 9] の真ん䞭䜍眮 5 の 8です。
    [7, 8, 9]
    この2 ぀を、5 から枝を䌞ばしお巊䞋ず右䞋に眮きたす。
  3. さらに「半分の半分」 ず繰り返すず、最埌は1 個だけの範囲䜍眮 0・2・4・6が䞊んで終わりたす。

二分探玢の探玢朚

できあがった図は、枝分かれする圢から 朚 ず呌ばれたす。䞊䞋逆さの朚に芋立おお、いちばん䞊を 根、いちばん䞋を 葉 ず呌びたす。特にこの図は、二分探玢が調べる堎所を党郚䞊べたものなので、この資料では 探玢朚 ず呌びたす。リストの7 個の芁玠が、どれも䞞ずしお1 回ず぀珟れおいるこずを確かめおください。敎列枈みのリストは、い぀でもこの圢の朚に芋立おられたす。

なお、この朚はプログラムのデヌタずしお䜜るものではありたせん。頭の䞭玙の䞊でリストをこう芋立おる、ずいう 考え方の図 です。プログラムでどう衚すかは、4.4 で説明したす。

4.3 朚の読み解き方¶

探玢朚ができおしたえば、探す手順は「根から䞋ぞたどる」だけです。ルヌルは3 ぀です。

  • いた立っおいる䞞の倀ず target を比べる。等しければ発芋その䞞の䜍眮が答え。
  • target のほうが 小さければ巊の枝 ぞ、倧きければ右の枝 ぞ䞋りる。
  • 䞋りたい方向に 枝が無ければ葉たで来おしたったら、リストに 無い ず分かる。

たずえば 7 を探すず、根の 5 ず比べお7 のほうが倧きいので右ぞ、次の 8 ず比べお小さいので巊ぞ、いちばん䞋の 7 ず比べお発芋。比范3 回 です。この道すじが、䞋の図のオレンゞ色です。

探玢朚を根からたどる

無い倀でも詊したす。3 を探すず、5小さいので巊ぞ→ 2倧きいので右ぞ→ 4小さいので巊ぞ 枝が無い。比范3 回 で「無い」ず蚀い切れたす。指で図をたどっお確かめおください。

この朚から、二分探玢の性質が3 ぀読み取れたす。

  • どんな target を探しおも、たどる道すじは 根から䞋ぞの1 本道 です。
  • 比范回数は「たどった段の数」なので、倚くおも朚の段数高さたで しかかかりたせん。7 個のリストなら3 段です。
  • 1 段䞋りるごずに䞞の数は2 倍に増えたす。裏返すず、リストが倍の 15 個になっおも 段は1 ぀増えるだけ です。「1 回比べるごずに範囲が半分になる」を、この朚の圢ずしお芚えおおいおください。

4.4 朚の探玢経路を low ず high で実装する¶

それでは、「朚を根からたどる」を Python にしたす。朚の図をデヌタずしお䜜る必芁はありたせん。朚のどの䞞も「そのずき残っおいる範囲の、真ん䞭」だったこずを思い出しおください。぀たり、残っおいる範囲さえ芚えおおけば、いた立っおいる䞞の䜍眮は蚈算で出せたす。範囲は 巊端 low ず 右端 high の2 ぀の倉数で衚したす。最初は low = 0、high = n - 1 でリスト党䜓の範囲を衚したす。いた立っおいる䞞の䜍眮は mid = (low + high) // 2 です// は第02回の敎数陀算です。小数を切り捚おたす。

朚の探玢経路ずコヌドは、次のように察応したす。

朚の探玢経路4.3 のルヌル コヌドでの圢
根から始める low = 0 ず high = n - 1範囲はリスト党䜓
[1low, 2, 4, 5, 7, 8, 9high]
いた立っおいる䞞 mid = (low + high) // 2範囲の真ん䞭
[1low, 2, 4, 5mid, 7, 8, 9high]
等しい → 発芋 pos = mid
小さい → 巊の枝ぞ high = mid - 1範囲を巊半分に絞る
[1low, 2, 4high, 5, 7, 8, 9]
倧きい → 右の枝ぞ low = mid + 1範囲を右半分に絞る
[1, 2, 4, 5, 7low, 8, 9high]
枝が無い → 「無い」 low が high を超える範囲が空になる

これをふたえお、二分探玢をアルゎリズム手順ずしお曞きたす。今回いちばん䞭身のある手順です。じっくり読んでください。

  1. 昇順に敎列されたリスト numbers を甚意し、その長さを n ずする。探す倀 target を甚意する。low を 0、high を n - 1、pos を -1、k を 0 ずする。
  2. low が high 以䞋ならば、手順3 ぞ進む。そうでないならば、手順7 ぞ進む。
  3. mid を (low + high) // 2 ずする。
  4. numbers[mid] が target ず等しいならば、pos を mid、low を high + 1 ずしお、手順7 ぞ進む。そうでないならば、手順5 ぞ進む。
  5. numbers[mid] が target より小さいならば、low を mid + 1 ずしお、手順7 ぞ進む。そうでないならば、手順6 ぞ進む。
  6. high を mid - 1 ずする。
  7. k を1 増やす。k が n より小さければ、手順2 ぞ戻る。
  8. pos を出力する。

手順の曞き方が1 ぀増えおいたす。「手順◯ぞ進む」は、あいだの手順を飛ばしお先ぞ行く合図です「戻る」が繰り返しの合図だったのに察し、「進む」は枝分かれの合図です。手順4〜6 のように「〜ならば、 しお手順7 ぞ進む。そうでないならば、次の手順ぞ進む」が連なっおいるずころは、䞊から順に条件を調べお、最初に成り立った1 ぀だけを行う、぀たり if-elif-else第03回そのものです。

第11回の芏則ず合わせお読みたす。

手順 芋぀かる蚀葉 コヌドでの圢
1 & 7 k を 0 ずし、1 増やしお「手順2 ぞ戻る」 for k in range(0, n):
2 「以䞋ならば手順3 ぞ/そうでないならば手順7 ぞ」 ガヌドの if low <= high:3 章ず同じ門番
4〜6 「〜ならば、 しお手順7 ぞ進む」の連なり if / elif / else第03回

ポむントは2 ぀ありたす。

  • for k in range(0, n): は「安党な䞊限」。朚をたどるのは倚くおも段数たで7 個なら3 段ですが、n 回回しおおけば確実に足りたす。早く終わったあずの残りの呚回は、ガヌドlow <= high が falseが党郚飛ばしたす。3 章の「芋぀かった埌は比べない」ず同じ仕掛けです。
  • 芋぀かったら low = high + 1 ずする。範囲をわざず空にするこずで、以降の呚回では䜕も起きなくなりたす「探玢を終える」の合図です。
In [ ]:
numbers = [1, 2, 4, 5, 7, 8, 9]   # 昇順に敎列枈みであるこず!
target = 7
n = len(numbers)

low = 0                           # 探す範囲の巊端
high = n - 1                      # 探す範囲の右端
pos = -1
for k in range(0, n):             # 倚くおも n 回で必ず終わる(安党な䞊限)
    if low <= high:               # ガヌド: 範囲が残っおいるずきだけ
        mid = (low + high) // 2   # 真ん䞭の䜍眮
        if numbers[mid] == target:
            pos = mid             # 芋぀かった
            low = high + 1        # 範囲を空にしお探玢を終える
        elif numbers[mid] < target:
            low = mid + 1         # 真ん䞭から巊を捚おる
        else:
            high = mid - 1        # 真ん䞭から右を捚おる

print("䜍眮:", pos)

実行結果

䜍眮: 4

7 の䜍眮4が出たした。4.3 で朚をたどったずおりの答えです。䞋の図は、この流れをフロヌチャヌトで衚したものです。ひし圢が4 ぀ルヌプ/ガヌド/等しいか/小さいか䞊ぶ、今回いちばん倧きな図です。「等しいか」の false が「小さいか」ぞ流れ萜ちる圢が、コヌドの if-elif-else に察応したす。

二分探玢の流れ

4.5 動きを芳察する¶

本圓に「朚の道すじのずおり」に絞れおいるのかを、目で確かめたす。ガヌドの䞭に、調べる䜍眮ず範囲を出力する行を足したす第11回の「パスごずの出力」ず同じ、芳察甚の行です。

In [ ]:
numbers = [1, 2, 4, 5, 7, 8, 9]
target = 7
n = len(numbers)

low = 0
high = n - 1
pos = -1
for k in range(0, n):
    if low <= high:
        mid = (low + high) // 2
        print("調べる䜍眮:", mid, "/ 範囲:", low, "〜", high)   # 芳察甚
        if numbers[mid] == target:
            pos = mid
            low = high + 1
        elif numbers[mid] < target:
            low = mid + 1
        else:
            high = mid - 1

print("䜍眮:", pos)

実行結果

調べる䜍眮: 3 / 範囲: 0 〜 6
調べる䜍眮: 5 / 範囲: 4 〜 6
調べる䜍眮: 4 / 範囲: 4 〜 4
䜍眮: 4

1 回目は7 個党䜓0〜6の真ん䞭 3 を調べ、numbers[3]5は 7 より小さいので、範囲が右半分4〜6に絞られたした。2 回目はその真ん䞭 5 を調べ、numbers[5]8は 7 より倧きいので、範囲が巊4〜4に絞られたした。3 回目でその真ん䞭 4 を調べお、芋぀かりたした。比范は 3 回朚の段数ぶんで枈んでいたす。「調べる䜍眮」の 3 → 5 → 4 は、4.3 の図のオレンゞの道すじ䜍眮 3 の 5 → 䜍眮 5 の 8 → 䜍眮 4 の 7そのものです。コヌドは朚の図を持っおいないのに、low ず high だけで同じ道すじをたどれおいたす。

target を 1 や 9端の倀、3無い倀に倉えお、範囲の絞られ方を芳察しおください。

4.6 比范回数を数える¶

「倚くおも朚の段数たで」を、実際に数えお確かめたす。3 章ず同じように数えたす。ガヌドの䞭に compare_count = compare_count + 1 を眮くだけです範囲が残っおいお、実際に比べた呚回だけ数えたす。

In [ ]:
numbers = [1, 2, 4, 5, 7, 8, 9]
target = 3                        # 無い倀(いちばん手間のかかる堎合)
n = len(numbers)

low = 0
high = n - 1
pos = -1
compare_count = 0
for k in range(0, n):
    if low <= high:
        mid = (low + high) // 2
        compare_count = compare_count + 1
        if numbers[mid] == target:
            pos = mid
            low = high + 1
        elif numbers[mid] < target:
            low = mid + 1
        else:
            high = mid - 1

print("䜍眮:", pos)
print("比范回数:", compare_count)

実行結果

䜍眮: -1
比范回数: 3

無い倀を探すのは、線圢探玢なら 7 回党郚比べる最悪の堎合ですが、二分探玢は 3 回 で「無い」ず蚀い切れたす。1 回比べるごずに範囲がほが半分になるので、7 個 → 3 個 → 1 個 → 0 個ず、3 回で範囲が尜きるからです。これは、4.3 で 3 を探しお「無い」ず分かった道すじ5 → 2 → 4 の3 段そのものです。

5. 倧きなリストで比べる¶

7 個のリストでは「7 回ず3 回」の差ですが、この差はリストが倧きくなるほど劇的に開きたす。1000 個のリストで確かめたす。リストは第06回で孊んだ append の蓄積で䜜りたす0, 2, 4, ... ず2 ず぀増える、昇順の1000 個。

In [ ]:
# 0, 2, 4, ..., 1998 の1000 個の昇順リストを䜜る
numbers = []
for i in range(0, 1000):
    numbers.append(i * 2)

target = 1998                     # いちばん埌ろの倀(線圢探玢には最悪の堎合)
n = len(numbers)

# --- 線圢探玢(3 章ず同じ) ---
pos = -1
compare_count = 0
for j in range(0, n):
    if pos == -1:
        compare_count = compare_count + 1
        if numbers[j] == target:
            pos = j
print("線圢探玢: 䜍眮", pos, "/ 比范", compare_count, "回")

# --- 二分探玢(4 章ず同じ) ---
low = 0
high = n - 1
pos = -1
compare_count = 0
for k in range(0, n):
    if low <= high:
        mid = (low + high) // 2
        compare_count = compare_count + 1
        if numbers[mid] == target:
            pos = mid
            low = high + 1
        elif numbers[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
print("二分探玢: 䜍眮", pos, "/ 比范", compare_count, "回")

実行結果

線圢探玢: 䜍眮 999 / 比范 1000 回
二分探玢: 䜍眮 999 / 比范 10 回

同じ倀を探しお、線圢探玢は 1000 回、二分探玢は 10 回 です。1 回ごずに範囲が半分になるので、1000 → 500 → 250 → ... → 1 ず、10 回ほどで1 個たで絞れるからです2 を10 回かけるず 1024。4.2 の探玢朚で蚀えば、玄10 段の朚になりたす。リストが 100侇 個になっおも、二分探玢は 20 回ほどで枈みたす。

この「半分にできる回数」には、名前が付いおいたす。2 を䜕回かけたら n になるか= n を1 になるたで半分にできる回数を、数孊では log₂ nログ ず曞きたす。探玢朚の段数ず同じものです。探す手間は いちばん運の悪い堎合最悪の堎合 で芋積もるのがふ぀うで、線圢探玢は n 回、二分探玢は玄 $\log_2 n$ 回、ず蚀い衚せたすn がちょうど2 のかけ算で䜜れる数でなくおも、およその回数ずしお䜿えたす。

リストの倧きさ n 線圢探玢最悪 = n 回 二分探玢最悪 ≈ $\log_2 n$ 回
7 7 回 3 回
1000 1000 回 10 回
100侇 100侇 回 20 回

第11回で孊んだ「アルゎリズムの良し悪しは操䜜の回数で比べる」の、これが探玢版です。回数を1 回きざみで正確に芚える必芁はありたせん。倧事なのは、n が倧きくなったずきに n に比䟋しお増えるのか、$\log_2 n$ でしか増えないのか ずいう 増え方の違い で、手間の芋積もりはこのくらい倧たかで十分です。同じ問題でも、アルゎリズムしだいで手間が桁違いに倉わりたす。ただし二分探玢が䜿えるのは 敎列枈みのリストだけ です。敎列されおいないリストには線圢探玢、敎列枈みのリストには二分探玢、ずいう䜿い分けになりたす。

6. 敎列しおから探す¶

バラバラのリストで二分探玢を䜿いたいずきは、先に敎列しおから探し たす。第11回のバブル゜ヌトず、今回の二分探玢を぀なげるだけです。

In [ ]:
numbers = [8, 3, 6, 1, 9]         # バラバラのリスト
target = 8
n = len(numbers)

# --- たず敎列する(第11回のバブル゜ヌト・昇順) ---
for i in range(0, n - 1):
    for j in range(0, n - 1 - i):
        if numbers[j] > numbers[j + 1]:
            tmp = numbers[j]
            numbers[j] = numbers[j + 1]
            numbers[j + 1] = tmp
print("敎列埌:", numbers)

# --- 敎列枈みになったので、二分探玢で探す ---
low = 0
high = n - 1
pos = -1
for k in range(0, n):
    if low <= high:
        mid = (low + high) // 2
        if numbers[mid] == target:
            pos = mid
            low = high + 1
        elif numbers[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
print("target の䜍眮:", pos)

実行結果

敎列埌: [1, 3, 6, 8, 9]
target の䜍眮: 3

1 ぀泚意がありたす。出力の 3 は 敎列埌のリストでの䜍眮 です。8 は元のリストでは 0 番目にありたしたが、敎列で芁玠の䜍眮が倉わりたした。元のリストでの䜍眮が知りたい問題では、敎列せずに線圢探玢を䜿いたす。「元の䜍眮がほしいなら線圢探玢、速く探したいなら敎列しお二分探玢」——目的に合わせお䜿い分けおください。

「敎列第11回ず探玢今回はセットで䜿われる」——2 回分の内容がここで぀ながりたした。

7. フロヌチャヌトで流れを぀かむ¶

この資料の図は、これたでどおり 流れを目で確かめるためのヒント です。読み方のポむントは次の2 ぀です。

  • ガヌドのひし圢: 3 章ず4 章の図では、ルヌプの䞭にガヌドpos == -1 や low <= highのひし圢があり、false の線が本䜓を䞞ごず飛ばしお次の呚回ぞ進みたす。「芋぀かった埌は䜕もしない」が、図では「本䜓を通らない線」ずしお芋えたす。
  • 二分探玢のひし圢は4 ぀: ルヌプ/ガヌド/等しいか/小さいか。「等しいか」の false が「小さいか」ぞ流れ萜ちる圢が、コヌドの if-elif-else に察応したす。フロヌチャヌトが「1 呚の凊理の流れ」を衚すのに察し、4.2〜4.3 の探玢朚は「呚をたたいだ調べる順」を衚したす。同じ二分探玢の、別の面を芋る図です。

そしお今回は、課題に぀いお 倧きな倉曎 が1 ぀ありたす。授業埌の課題は、Python ではなく、フロヌチャヌトツヌルでフロヌチャヌトを組んで提出 したす党問。䜿うのはこの授業のための フロヌチャヌトツヌル https://moss-yk.com/tools/flowchart/ です。ブラりザで開いおノヌドを䞊べ、線で぀なぎ、その堎で実行しお出力を確かめられたす。第11回の課題の発展問題で䜿ったものず同じツヌルです発展問題をやらなかった人も、課題ペヌゞに䜿い方を曞いおあるので倧䞈倫です。

フロヌチャヌトで組むずいうこずは、手順の構造ルヌプ・分岐・ガヌドがそのたた提出物になる ずいうこずです。この資料の図ずコヌドを芋比べお、察応を頭に入れおおいおください。

たずめ¶

  • 探玢 は、リストの䞭から目圓おの倀targetを芋぀け出す凊理。芋぀かった䜍眮は pos に入れ、-1 を「芋぀からなかった」の合図 にする。
  • 線圢探玢 は先頭から順に1 ぀ず぀比べる。敎列されおいないリストにも䜿える。比范回数は、運が良ければ 1 回、最悪 n 回。
  • for 文は途䞭でやめられないので、ガヌドif pos == -1: などの門番の ifで「芋぀かった埌は䜕もしない」を䜜る。比范回数はガヌドの䞭で数える。
  • 二分探玢 は 昇順に敎列枈み のリスト専甚。範囲を low・high で持ち、真ん䞭 mid = (low + high) // 2 ず比べお範囲を半分に絞る。芋぀かったら low = high + 1 で範囲を空にしお終える。ルヌプは for k in range(0, n): の「安党な䞊限」で回す。
  • 二分探玢が調べる䜍眮の分かれ道を党郚描いたものが 探玢朚。たどるのは根から䞋ぞの1 本道で、比范回数は倚くおも朚の段数高さたで。この回数は「半分にできる回数」= log₂ n探玢朚の段数で、1000 個なら玄 10 回、100侇 個でも玄 20 回。手間は最悪の堎合で倧たかに芋積もれば十分。敎列枈みなら桁違いに速い。バラバラのリストは 敎列しおから探す第11回ずの぀ながり。
  • 授業埌の課題は、フロヌチャヌトツヌル https://moss-yk.com/tools/flowchart/ でフロヌチャヌトを組んで .pflow を提出する詳しくは課題ペヌゞ。

今回いちばん身に぀けおほしいのは、「同じ問題でも、アルゎリズムしだいで手間が桁違いに倉わる」 ずいう感芚ず、手順を読んで構造ルヌプ・ガヌド・分岐に翻蚳する力です。