Chaba Content Site

二分探索 (Binary Search)

2025-08-17

二分探索の学習ノート

「半分ずつ探せばいいじゃん」って簡単に思ってたけど、実装でめちゃくちゃバグを出した。

最初に書いた間違いだらけのコード

// 初心者の頃の間違い
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length;  // ← arr.length - 1 じゃないとダメ
    
    while (left < right) {   // ← left <= right が正しい
        const mid = (left + right) / 2;  // ← Math.floor忘れた
        
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid;      // ← mid + 1 じゃないと無限ループ
        } else {
            right = mid;     // ← mid - 1 にすべき
        }
    }
    
    return -1;
}

正しい実装(やっと理解した)

function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;  // 最後のインデックス
    
    while (left <= right) {      // イコールが重要!
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;       // midは違うから+1
        } else {
            right = mid - 1;      // midは違うから-1
        }
    }
    
    return -1;
}

なぜ left <= right なのか

これ、めっちゃ悩んだ。図を描いてやっと理解した:

配列: [1, 3, 5, 7, 9]
target: 5

1回目: left=0, right=4, mid=2, arr[2]=5 ✓ 見つかった!

でも、もしtarget=7だったら:
1回目: left=0, right=4, mid=2, arr[2]=5 < 7
2回目: left=3, right=4, mid=3, arr[3]=7 ✓

最後の瞬間:left=right=3 の時も確認が必要!

オーバーフロー問題(知らなかった)

// 実は危険な書き方
const mid = Math.floor((left + right) / 2);

// より安全な書き方(大きな数値でオーバーフローしない)
const mid = Math.floor(left + (right - left) / 2);

// JavaやC++では重要だけど、JavaScriptではそこまで気にしなくていいかも

二分探索の変形パターン

1. 最初に現れる位置を探す

function findFirst(arr, target) {
    let left = 0, right = arr.length - 1;
    let result = -1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            result = mid;
            right = mid - 1;  // 左側も探す!
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return result;
}

2. 挿入位置を探す(めっちゃ使う)

function searchInsert(arr, target) {
    let left = 0, right = arr.length - 1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return left;  // leftが挿入位置!
}

実践で使える場面

  1. ソート済み配列での検索 - 当たり前だけど基本
  2. 回転ソート配列 - ちょっと工夫が必要
  3. 平方根の計算 - 実数でも二分探索できる
  4. 最小値の最大化問題 - 競プロでよく見る

デバッグのコツ(散々苦労した)

// デバッグ用のログを入れる
while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    console.log(`left=${left}, right=${right}, mid=${mid}, arr[mid]=${arr[mid]}`);
    // ...
}

小さい配列(3-5要素)で手計算して、ログと比較すると間違いが見つかりやすい。

よくある間違い(全部やった)

  1. 無限ループ: left = midright = mid にして進まない
  2. 配列外アクセス: right = arr.length にしてしまう
  3. 見逃し: while (left < right) で要素を見逃す
  4. 整数除算忘れ: JavaScriptでMath.floor忘れる

まとめ:二分探索マスターへの道

次回はソートアルゴリズムについてまとめる予定

Tags: 探索 効率化