前端算法笔记二 —— 二分查找

62 阅读7分钟

二分查找(Binary Search)是一种基于有序数组的高效查找算法,通过不断缩小搜索范围来定位目标值。其核心原理是分治思想,每次将搜索区间减半,时间复杂度为 O(log n)。以下通过图例和步骤详细说明:


📊 核心原理图解

假设有序数组为 [1, 3, 4, 6, 7, 8, 10, 13, 14, 18, 19, 21, 24, 37, 40, 45, 71],目标值 target = 7
数组索引范围:left = 0(起点),right = 16(终点)。

步骤分解:

  1. 第1轮查找

    • 计算中点mid = (0 + 16) / 2 = 8
    • 比较arr[8] = 14 > target=7 → 目标在左半区间
    • 更新右边界right = mid - 1 = 7
    • 新区间[1, 3, 4, 6, 7, 8, 10, 13](索引 0~7)
  2. 第2轮查找

    • 计算中点mid = (0 + 7) / 2 = 3
    • 比较arr[3] = 6 < target=7 → 目标在右半区间
    • 更新左边界left = mid + 1 = 4
    • 新区间[7, 8, 10, 13](索引 4~7)
  3. 第3轮查找

    • 计算中点mid = (4 + 7) / 2 = 5
    • 比较arr[5] = 8 > target=7 → 目标在左半区间
    • 更新右边界right = mid - 1 = 4
    • 新区间[7](仅剩索引4)
  4. 第4轮查找

    • 计算中点mid = (4 + 4) / 2 = 4
    • 比较arr[4] = 7 == target=7找到目标,返回索引 4

⚙️ 关键步骤总结

步骤操作区间变化
初始化left=0, right=16[0, 16]
第1轮:mid=8arr[8]=14 > 7right=7[0, 7]
第2轮:mid=3arr[3]=6 < 7left=4[4, 7]
第3轮:mid=5arr[5]=8 > 7right=4[4, 4]
第4轮:mid=4arr[4]=7 == 7返回索引4结束

⚠️ 注意事项

  1. 有序性前提:数组必须有序(升序或降序),否则算法失效。
  2. 边界更新
    • arr[mid] < targetleft = mid + 1
    • arr[mid] > targetright = mid - 1
  3. 终止条件:当 left > right 时,目标不存在,返回 -1
  4. 避免整数溢出:中点计算应为 mid = left + (right - left) / 2,而非 (left + right) / 2

💻 代码示例(JavaScript)

function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2); // 避免整数溢出
    if (arr[mid] === target) return mid;
    else if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

⏱️ 性能分析

  • 时间复杂度O(log n)。每次迭代区间减半,最坏情况需 log₂(n) 次比较(如1000个元素仅需10次)。
  • 空间复杂度O(1)(迭代实现)。
  • 适用场景:静态有序数据集(如数据库索引、字典搜索),不适用于频繁插入/删除的动态数据。

通过分治策略,二分查找将大规模问题转化为小规模子问题,是算法设计中效率与简洁性的典范。


在有序数组中查找目标值的首次和末次出现位置(如 LeetCode 34 题),其核心是通过两次二分查找分别定位左右边界,时间复杂度保持 O(log n)。以下结合原理、图例和步骤详细说明:

⚙️ 核心原理

  1. 左边界(首次出现)查找

    • nums[mid] == target 时,不立即返回,而是将右指针 right 移到 mid - 1,继续向左搜索更早的 target
    • 循环结束时,left 指向第一个等于或大于 target 的位置,需验证 nums[left] == target
  2. 右边界(末次出现)查找

    • nums[mid] == target 时,将左指针 left 移到 mid + 1,继续向右搜索更晚的 target
    • 循环结束时,right 指向最后一个等于或小于 target 的位置,需验证 nums[right] == target

💡 关键区别

  • 左边界查找中,nums[mid] >= target 时收缩右边界;
  • 右边界查找中,nums[mid] <= target 时收缩左边界。

📊 图例与步骤示例

数组[5, 7, 7, 8, 8, 10]目标值target = 8
目标输出:首次位置 3,末次位置 4(索引从 0 开始)。

1. 查找左边界(首次出现)

  • 初始化left = 0, right = 5

  • 循环过程

    步骤leftrightmidnums[mid]操作(因 nums[mid]target
    10527 < 8left = mid + 1 = 3
    23548 = 8right = mid - 1 = 3(继续向左搜索)
    33338 = 8right = mid - 1 = 2(循环结束)
  • 结果left = 3
    → 验证 nums[3] = 8 = target ✅,首次位置为 3

2. 查找右边界(末次出现)

  • 初始化left = 0, right = 5

  • 循环过程

    步骤leftrightmidnums[mid]操作(因 nums[mid]target
    10527 < 8left = mid + 1 = 3
    23548 = 8left = mid + 1 = 5(继续向右搜索)
    355510 > 8right = mid - 1 = 4(循环结束)
  • 结果right = 4
    → 验证 nums[4] = 8 = target ✅,末次位置为 4


⚠️ 边界与异常处理

  1. 目标值不存在

    • 左边界查找后若 nums[left] != target,返回 [-1, -1]
      示例nums = [5, 7, 7, 8, 8, 10], target = 6 → 左边界查找结束 left = 2,但 nums[2] = 7 ≠ 6,返回 [-1, -1]
  2. 全相同元素

    • nums = [8, 8, 8, 8], target = 8
      • 左边界:left 结束于 0
      • 右边界:right 结束于 3
  3. 目标值超出数组范围

    • target < nums[0]target > nums[-1],直接返回 [-1, -1]

💻 代码实现(JavaScript)

function searchRange(nums, target) {
    if (nums.length === 0) return [-1, -1];
    
    // 查找左边界(首次出现位置)
    const findLeft = () => {
        let left = 0, right = nums.length - 1;
        while (left <= right) {
            const mid = left + Math.floor((right - left) / 2);
            if (nums[mid] >= target) {
                right = mid - 1; // 向左收缩
            } else {
                left = mid + 1;
            }
        }
        return (nums[left] === target) ? left : -1;
    };

    // 查找右边界(末次出现位置)
    const findRight = () => {
        let left = 0, right = nums.length - 1;
        while (left <= right) {
            const mid = left + Math.floor((right - left) / 2);
            if (nums[mid] <= target) {
                left = mid + 1; // 向右收缩
            } else {
                right = mid - 1;
            }
        }
        return (nums[right] === target) ? right : -1;
    };

    const leftBound = findLeft();
    if (leftBound === -1) return [-1, -1]; // 目标不存在
    const rightBound = findRight();
    return [leftBound, rightBound];
}

// 测试用例
console.log(searchRange([5,7,7,8,8,10], 8)); // [3,4]
console.log(searchRange([5,7,7,8,8,10], 6)); // [-1,-1]
console.log(searchRange([], 0));              // [-1,-1]

💡 常见错误与避免

  1. 死循环

    • 右边界查找时若用 mid = (left + right) // 2,当只剩两个元素(如 left=3, right=4)可能陷入死循环。
      解决:改用 mid = left + (right - left + 1) // 2
  2. 越界访问

    • 循环结束后需检查 leftright 是否在有效索引范围内。
  3. 混淆边界条件

    • 左边界结束时 left 指向首个 ≥ target 的位置,右边界结束时 right 指向首个 ≤ target 的位置,需二次验证是否等于 target

面试提示:若被问“为何不用线性扫描?”,强调有序数组的特性使二分查找将时间复杂度从 O(n) 优化到 O(log n),适合大规模数据。


在有序数组中查找目标值的插入位置是二分查找的经典应用场景,其核心原理是通过不断缩小搜索区间定位第一个大于或等于目标值的位置。以下我将从原理、图例步骤到代码实现详细说明。

⚙️ 核心原理

  1. 有序数组特性:数组升序排列时,插入位置 pos 满足:
    • 所有 [0, pos-1] 的元素均 < target
    • 所有 [pos, n-1] 的元素均 target
  2. 二分查找终止条件
    • 若找到 target,直接返回其索引。
    • 若未找到,循环结束时 left 指向首个 ≥ target 的位置,即插入位置 。
  3. 指针移动逻辑
    • nums[mid] < target → 目标在右侧 → left = mid + 1
    • nums[mid] ≥ target → 目标在左侧或等于 → right = mid - 1

📊 图例与步骤说明

数组[1, 3, 5, 6]目标值target = 2(需插入到索引 1
初始状态left = 0, right = 3

步骤leftrightmidnums[mid]比较结果操作
103133 > 2right = mid-1 = 0
200011 < 2left = mid+1 = 1
结束left=1(插入位置)

结果

  • 循环结束时 left = 1, right = 0left > right
  • 插入位置为 left = 1(首个 ≥ 2 的位置)

💻 JavaScript 实现

function searchInsert(nums, target) {
  let left = 0;
  let right = nums.length - 1;
  
  while (left <= right) {
    const mid = Math.floor(left + (right - left) / 2); // 防溢出
    if (nums[mid] === target) {
      return mid; // 找到目标值,直接返回索引
    } else if (nums[mid] < target) {
      left = mid + 1; // 目标在右半部分
    } else {
      right = mid - 1; // 目标在左半部分
    }
  }
  return left; // 插入位置为 left
}

// 测试用例
console.log(searchInsert([1, 3, 5, 6], 2)); // 输出: 1
console.log(searchInsert([1, 3, 5, 6], 7)); // 输出: 4(插入末尾)
console.log(searchInsert([], 5));          // 输出: 0(空数组)

⚠️ 关键点解析

  1. 终止条件 left <= right
    • 确保单元素数组(如 [5], target=5)能被正确处理 。
  2. 插入位置为 left 的推导
    • 循环结束时,left 指向 第一个 ≥ target 的元素位置。若 target 超过最大值,left = nums.length
  3. 边界情况处理
    • 空数组:直接返回 0
    • 目标值小于最小值left 保持为 0
    • 目标值大于最大值left 递增至 nums.length

⏱️ 复杂度与适用场景

  • 时间复杂度O(log n),每次迭代范围减半。
  • 空间复杂度O(1),仅需常数级变量。
  • 适用场景:静态有序数组的插入位置搜索(如数据库索引维护、日志时间戳插入)。

此算法通过二分查找的指针终止特性,将插入位置与目标查找统一处理,兼顾高效性与代码简洁性。