🔍 你真的会二分查找吗?

🔍 你真的会二分查找吗?

二分查找是一种在有序数组中查找特定元素的高效算法。它通过不断将搜索范围缩小一半来快速定位目标值,时间复杂度为 O(log n)。

🤔 常见疑惑

在学习二分查找时,你可能会遇到这些问题:

  1. 循环条件是用 <= 还是 <
  2. 边界更新是用 mid-1 还是 mid
  3. 如何处理重复元素?
  4. 左闭右闭区间 [left, right] 和左闭右开区间 [left, right) 如何选择?

让我们通过实例一步步解开这些疑惑。

📚 最基本的二分查找

💡 基本实现

javascript 复制代码
// 基本的二分查找
function search(arr, target) {
  let left = 0;
  let right = arr.length - 1; // 左闭右闭区间 [left, right]

  while (left <= right) {
    // 区间有效时继续查找
    const mid = left + Math.floor((right - left) / 2); // 防止整数溢出

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid - 1; // 目标在左侧
    } else {
      return mid; // 找到目标值
    }
  }

  return -1; // 目标值不存在
}

// 示例
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(search(arr, 7)); // 输出: 6(7在数组中的位置)

🔍 关键点解析

  1. 区间定义

    • 使用闭区间 [left, right]
    • right 初始值为 arr.length - 1
    • 循环条件为 left <= right
  2. 中间值计算

    • 使用 left + (right - left) / 2
    • 而不是 (left + right) / 2
    • 防止整数溢出
  3. 边界更新

    • 目标在右侧:left = mid + 1
    • 目标在左侧:right = mid - 1
    • 保证区间不断缩小

💫 左闭右开区间 [left, right)

🔍 关键点解析

  1. 为什么使用 left <= right

    • 因为初始化时 right = arr.length - 1
    • 区间是闭区间 [left, right],两端都可以取到
    • left == right 时,这个值也需要判断
  2. 区间特点

    • 左闭右闭区间:[left, right]
    • 每次都在一个确定的区间内查找
    • 区间在循环过程中会逐渐缩小,但始终是有效的

💫 左闭右开区间 [left, right)

javascript 复制代码
function search2(arr, target) {
  let left = 0;
  let right = arr.length; // 左闭右开区间 [left, right)

  while (left < right) {
    // 区间不为空时继续查找
    const mid = left + Math.floor((right - left) / 2);

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid; // 目标在左侧,注意不是 mid-1
    } else {
      return mid; // 找到目标值
    }
  }

  return -1; // 目标值不存在
}

🔍 关键点解析

  1. 为什么使用 left < right

    • 因为初始化时 right = arr.length
    • 区间是左闭右开 [left, right),右端点取不到
    • left == right 时,区间为空,没有值需要判断
  2. 为什么 right = mid 而不是 mid-1

    • 因为右区间是开区间,right 指向的位置不在查找范围内
    • mid 可能是目标位置,不能跳过
    • 保持区间定义的一致性:右开区间的特点
  3. 区间特点

    • 左闭右开区间:[left, right)
    • 实际查找范围:[left, right-1]
    • 每次缩小区间时都保持左闭右开的特性

🎯 练习题目

在掌握了基本原理后,建议练习以下题目:

  1. 704. 二分查找 - 基础二分查找
  2. 剑指 Offer 53 - I. 在排序数组中查找数字 I - 查找数字出现次数

🎯 进阶:查找最左侧边界

💡 问题引入

想象这样一个场景:

  • 有一个升序数组,其中包含多个相同的目标值
  • 需要找到最左侧的那个目标值的位置
  • 必须使用二分查找来实现

虽然顺序查找也能解决这个问题,但我们的目标是用二分查找来优化时间复杂度。

🔍 实现思路

我们将在基本二分查找的基础上做一些巧妙的修改。关键在于:

  • 即使找到目标值也不立即返回
  • 继续向左边搜索,寻找可能存在的更左侧的目标值
  • 使用左闭右闭区间 [left, right] 来实现
javascript 复制代码
// 查找最左侧的目标值
function searchLeft(arr, target) {
  let left = 0;
  let right = arr.length - 1; // 左闭右闭区间

  while (left <= right) {
    // 区间有效时继续查找
    const mid = left + Math.floor((right - left) / 2);

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid - 1; // 目标在左侧
    } else {
      right = mid - 1; // ⭐ 关键:收缩右边界,继续向左找
    }
  }

  // 边界条件检查
  if (left >= arr.length || arr[left] !== target) {
    return -1; // 目标值不存在
  }
  return left; // 返回最左侧的位置
}

// 示例
const arr = [1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 8, 9, 10];
console.log(searchLeft(arr, 7)); // 输出: 6(第一个7的位置)

🔍 代码要点解析

  1. 核心思路

    • 即使找到目标值也不返回
    • 通过收缩右边界,向左继续寻找
    • 最终 left 指向的就是最左侧的目标值
  2. 边界处理

    • 循环结束后,left 指向第一个大于等于 target 的位置
    • 需要检查:
      1. left 是否越界
      2. 该位置的值是否等于目标值
  3. 为什么这样做有效?

    • 当找到目标值时,不急于返回
    • 收缩右边界,继续在左半部分查找
    • 保证了找到的是最左侧的目标值

💫 左闭右开版本

javascript 复制代码
// 查找最左侧的目标值(左闭右开版本)
function searchLeft2(arr, target) {
  let left = 0;
  let right = arr.length; // 左闭右开:[left, right)

  while (left < right) {
    // 区间不为空时继续查找
    const mid = left + Math.floor((right - left) / 2);

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid; // 目标在左侧,注意不是 mid-1
    } else {
      right = mid; // ⭐ 关键:收缩右边界,继续向左找
    }
  }

  // 边界条件检查
  if (left >= arr.length || arr[left] !== target) {
    return -1; // 目标值不存在
  }
  return left; // 返回最左侧的位置
}

// 示例
const arr2 = [1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 8, 9, 10];
console.log(searchLeft2(arr2, 7)); // 输出: 6(第一个7的位置)

🔍 左闭右开版本的特点

  1. 区间定义

    • 搜索范围:[left, right)
    • right 初始值为 arr.length
    • 循环条件为 left < right
  2. 边界处理

    • arr[mid] >= target 时,使用 right = mid
    • 保持右开区间的特性
    • 最终 left 指向目标位置

🎯 进阶:查找最右侧边界

💡 问题转化

查找最右侧边界可以看作是查找最左侧边界的镜像问题:

  • 当找到目标值时,不是收缩右边界
  • 而是扩展左边界,继续向右寻找
javascript 复制代码
// 查找最右侧的目标值
function searchRight(arr, target) {
  let left = 0;
  let right = arr.length - 1; // 左闭右闭区间

  while (left <= right) {
    // 区间有效时继续查找
    const mid = left + Math.floor((right - left) / 2);

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid - 1; // 目标在左侧
    } else {
      left = mid + 1; // ⭐ 关键:扩展左边界,继续向右找
    }
  }

  // 边界条件检查
  if (right < 0 || arr[right] !== target) {
    return -1; // 目标值不存在
  }
  return right; // 返回最右侧的位置
}

// 示例
const arr = [1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 8, 9, 10];
console.log(searchRight(arr, 7)); // 输出: 10(最后一个7的位置)

🔍 代码要点解析

  1. 核心思路

    • 找到目标值时不返回
    • 通过扩展左边界,向右继续寻找
    • 最终 right 指向的就是最右侧的目标值
  2. 边界处理

    • 循环结束后,right 指向最后一个等于 target 的位置
    • 需要检查:
      1. right 是否越界(小于 0)
      2. 该位置的值是否等于目标值
  3. 与查找最左侧边界的区别

    • 找到目标值时的处理方向相反
    • 最终返回的是 right 而不是 left
    • 边界检查条件也相应调整

💫 左闭右开版本

左闭右闭版本

js 复制代码
// 查找最右侧的目标值(左闭右开版本)
function searchRight2(arr, target) {
  let left = 0;
  let right = arr.length; // 左闭右开:[left, right)

  while (left < right) {
    // 区间不为空时继续查找
    const mid = left + Math.floor((right - left) / 2);

    if (arr[mid] < target) {
      left = mid + 1; // 目标在右侧
    } else if (arr[mid] > target) {
      right = mid; // 目标在左侧,注意不是 mid-1
    } else {
      left = mid + 1; // ⭐ 关键:扩展左边界,继续向右找
    }
  }

  // 边界条件检查
  if (right <= 0 || arr[right - 1] !== target) {
    return -1; // 目标值不存在
  }
  return right - 1; // 返回最右侧的位置
}

// 示例
const arr2 = [1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 8, 9, 10];
console.log(searchRight2(arr2, 7)); // 输出: 10(最后一个7的位置)

需要注意的点是:在循环后进行判断的时候是 right-1 进行判断,因为 right 在初始赋值和循环条件赋值时都没有-1,所以最后进行边界判断和返回下标时需要-1

📊 二分查找实现对比

🔍 基本实现对比

实现细节 左闭右闭区间 [left, right] 左闭右开区间 [left, right)
初始化右边界 right = arr.length - 1 right = arr.length
循环条件 while (left <= right) while (left < right)
左边界更新 left = mid + 1 left = mid + 1
右边界更新 right = mid - 1 right = mid

🎯 边界查找对比

查找类型 左闭右闭区间 [left, right] 左闭右开区间 [left, right)
最左侧边界 right = mid - 1 right = mid
最右侧边界 left = mid + 1 left = mid + 1

💡 实践建议

  • 对于初学者,建议使用左闭右闭区间 [left, right]
    • 更直观易理解
    • 边界处理更统一
    • 不需要额外的索引调整
  • 使用左闭右开区间时需要注意:
    • 查找最右侧边界时,返回结果需要 -1 调整
    • 边界条件判断更复杂
相关推荐
中微子3 小时前
TypeScript never 类型详解
前端
2401_841495643 小时前
【计算机视觉】霍夫变换函数的参数调整
人工智能·python·算法·计算机视觉·霍夫变换·直线检测·调整策略
Strawberry_rabbit3 小时前
路由配置中的svg图标如何匹配
前端·css
用户52980797824983 小时前
Vue 为何自动加载 index.vue?
前端
北风GI3 小时前
element-plus 自定义主题 最佳实践
前端
CodeCraft Studio3 小时前
国产化PDF处理控件Spire.PDF教程:C#中轻松修改 PDF 文档内容
前端·pdf·c#·.net·spire.pdf·编辑pdf·修改pdf
晴殇i3 小时前
告别 localStorage!探索前端存储新王者 IndexedDB
前端·javascript·面试
Mintopia3 小时前
Next.js 的分布式基础思想:从 CAP 到事件风暴,一路向“可扩展”的银河系巡航
前端·javascript
Moment3 小时前
Next.js 16 Beta:性能、架构与开发体验全面升级 💯💯💯
前端·javascript·github