Skip to content
Playground

7.探索アルゴリズム

学習目標

  • 「配列からある値を探す」問題に対して、線形探索を自力で書ける
  • 二分探索の仕組みを理解し、トレースできる
  • 線形探索と二分探索の探索回数の違いを説明できる

「100人分のテストの点数が入った配列から、90点の人が何番目にいるかを調べたい」——このように、データの中から目的の値を見つける処理を 探索(サーチ)と呼びます。

探索はプログラミングで最も基本的なアルゴリズムの1つです。代表的な探索アルゴリズムに線形探索と二分探索があります。

線形探索 は、最もシンプルな探索方法です。配列の先頭から順に1つずつ調べて、目的の値が見つかったらその位置を返します。

たとえば、以下の配列から 88 を探す場合、先頭の 34 から順に比較していき、4番目(インデックス 3)で一致します。

┌──┬──┬──┬──┬──┐
│34│72│15│88│43│
└──┴──┴──┴──┴──┘

配列に存在しない値を探す場合は、全要素を調べて「見つからなかった」と判定します。

図で確認した手順をプログラムにするために、1つずつ考えていきます。

  1. 先頭から1つずつ調べる

    for ループで配列の要素にインデックス 0 から末尾まで順にアクセスすることで表現できます。

    対応するコード
    for (let i = 0; i < arr.length; i++) {
  2. 目的の値と比較する

    各要素を調べるたびに、arr[i] === target で比較します。一致したら、その時点のインデックス ireturn で返します。return が実行されるとループも関数も終了するので、残りの要素は調べません。

    対応するコード
    if (arr[i] === target) {
    return i;
    }
  3. 見つからなければ -1 を返す

    ループが最後まで回っても一致する要素がなかった場合は、ループの後で return -1 を返します。

    対応するコード
    return -1;
search.js
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}
const data = [34, 72, 15, 88, 43];
console.log(linearSearch(data, 88));
console.log(linearSearch(data, 43));
console.log(linearSearch(data, 99));
3
4
-1

linearSearch(data, 88) をトレースします。

iarr[i]arr[i] === target操作
034false
172false
215false
388truereturn 3

i = 3 で一致したので、ループの残り(i = 4)には到達しません。

線形探索は「一致する値を探す」だけでなく、条件を変えてさまざまな探索に応用できます。たとえば、最初の偶数を探す関数は次のように書けます。

search.js
function findFirstEven(arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2 === 0) {
return i;
}
}
return -1;
}
console.log(findFirstEven([7, 3, 8, 1, 4]));
console.log(findFirstEven([1, 3, 5]));
2
-1

構造は linearSearch と同じです。arr[i] === target の部分が arr[i] % 2 === 0 に変わっただけです。「先頭から順に調べて、条件を満たす要素が見つかったらそのインデックスを返す」というパターンは共通しています。

線形探索は先頭から1つずつ調べるので、最悪の場合(目的の値が末尾にある、または存在しない)は配列の全要素を調べます。

要素数が 10 なら最大 10 回、要素数が 1000 なら最大 1000 回です。要素数に比例して探索回数が増えます。

要素数が 100 万の配列から値を探すとします。線形探索では最悪 100 万回の比較が必要で、データが大きくなるほど時間がかかります。

ただし、配列がソート(昇順に並べ替え)済みであれば、もっと少ない回数で探す方法があります。

「1 から 100 の中で正解の数を当てるゲーム」を考えます。答えるたびに「もっと大きい / 小さい / 正解」と教えてもらえます。

最初は真ん中の 50 を答えるのが最も効率的です。違ったら次の範囲(1〜4951〜100)の真ん中をまた答えます。このように「探す範囲を半分に絞る」ことを繰り返せば、100 個の数でも最大 7 回で正解にたどり着けます。

ソート済みの配列に対して同じ考え方を使うのが 二分探索 です。配列から 30 を探す例で見てみます。

┌──┬──┬──┬──┬──┬──┬──┬──┬──┐
│10│20│30│40│50│60│70│80│90│
└──┴──┴──┴──┴──┴──┴──┴──┴──┘

真ん中の要素と目的の値を比較して、目的の値がある側に範囲を絞ります。

  1. 配列全体の真ん中は 5030 < 50 なので左半分に範囲を絞る
  2. 残った範囲の真ん中は 2030 > 20 なので右半分に範囲を絞る
  3. 残った範囲の真ん中は 30。一致したので位置 2 を返す

9 個の要素に対して、たった 3 回の比較で見つかりました。

二分探索の手順をプログラムにするために、何が必要かを1つずつ考えていきます。

  1. 探索範囲を変数で管理する

    「今どこからどこまでを探しているか」を記録する必要があります。探索範囲の左端のインデックスを low、右端のインデックスを high とします。最初は配列全体が対象なので low = 0high = 8(要素数 - 1)です。

    対応するコード
    let low = 0;
    let high = arr.length - 1;
  2. 真ん中の位置を求める

    lowhigh の中間を Math.floor((low + high) / 2) で計算します。この値を mid とします。

    対応するコード
    const mid = Math.floor((low + high) / 2);
  3. 真ん中の要素と比較して範囲を絞る

    arr[mid]target を比較すると、3つの場合に分かれます。

    • 一致した → mid を返して終了
    • target のほうが大きい → 目的の値は右半分にある → low = mid + 1
    • target のほうが小さい → 目的の値は左半分にある → high = mid - 1

    mid + 1mid - 1 にするのは、mid の位置は既に調べたので、次の探索範囲に含める必要がないためです。

    対応するコード
    if (arr[mid] === target) {
    return mid;
    } else if (arr[mid] < target) {
    low = mid + 1;
    } else {
    high = mid - 1;
    }
  4. ループの終了条件を決める

    範囲を絞り続けると、いずれ lowhigh を超えます。low > high になったら探索範囲が空になったことを意味するので、「見つからなかった」と判定します。

    対応するコード
    while (low <= high) {
    // (ステップ 2, 3 の処理)
    }
    return -1;
search.js
function binarySearch(arr, target) {
let low = 0;
let high = arr.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
const data = [10, 20, 30, 40, 50, 60, 70, 80, 90];
console.log(binarySearch(data, 30));
console.log(binarySearch(data, 80));
console.log(binarySearch(data, 55));
2
7
-1

配列 [10, 20, 30, 40, 50, 60, 70, 80, 90] を使って、二分探索の動きをトレースします。

まず、値が見つかる場合として binarySearch(data, 80) を見てみます。

ステップlowhighmidarr[mid]比較操作
10845080 > 50low = 5
25867080 > 70low = 7
37878080 === 80return 7

3 回の比較で見つかりました。

次に、値が見つからない場合として binarySearch(data, 55) を見てみます。

ステップlowhighmidarr[mid]比較操作
10845055 > 50low = 5
25867055 < 70high = 5
35556055 < 60high = 4
454low > highreturn -1

4 回目はループに入る前に low > high になっているので、mid は計算されません(表の )。探索範囲が空になり、「見つからなかった」と判定されます。

線形探索は要素数に比例して探索回数が増えます。二分探索は毎回範囲を半分にするので、探索回数は大幅に少なくなります。

要素数線形探索(最大)二分探索(最大)
1010 回4 回
100100 回7 回
1,0001,000 回10 回
1,000,0001,000,000 回20 回

二分探索の最大探索回数は、要素数を何回 2 で割れるか(= 2 の何乗か)に対応します。1,000,000 は約 2^20 なので、最大 20 回です。