二分查找(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轮查找
- 计算中点:
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轮查找
- 计算中点:
mid = (0 + 7) / 2 = 3 - 比较:
arr[3] = 6<target=7→ 目标在右半区间 - 更新左边界:
left = mid + 1 = 4 - 新区间:
[7, 8, 10, 13](索引 4~7)
- 计算中点:
-
第3轮查找
- 计算中点:
mid = (4 + 7) / 2 = 5 - 比较:
arr[5] = 8>target=7→ 目标在左半区间 - 更新右边界:
right = mid - 1 = 4 - 新区间:
[7](仅剩索引4)
- 计算中点:
-
第4轮查找
- 计算中点:
mid = (4 + 4) / 2 = 4 - 比较:
arr[4] = 7==target=7→ 找到目标,返回索引4
- 计算中点:
⚙️ 关键步骤总结
| 步骤 | 操作 | 区间变化 |
|---|---|---|
| 初始化 | left=0, right=16 | [0, 16] |
| 第1轮:mid=8 | arr[8]=14 > 7 → right=7 | [0, 7] |
| 第2轮:mid=3 | arr[3]=6 < 7 → left=4 | [4, 7] |
| 第3轮:mid=5 | arr[5]=8 > 7 → right=4 | [4, 4] |
| 第4轮:mid=4 | arr[4]=7 == 7 → 返回索引4 | 结束 |
⚠️ 注意事项
- 有序性前提:数组必须有序(升序或降序),否则算法失效。
- 边界更新:
- 若
arr[mid] < target→left = mid + 1 - 若
arr[mid] > target→right = mid - 1
- 若
- 终止条件:当
left > right时,目标不存在,返回-1。 - 避免整数溢出:中点计算应为
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)。以下结合原理、图例和步骤详细说明:
⚙️ 核心原理
-
左边界(首次出现)查找:
- 当
nums[mid] == target时,不立即返回,而是将右指针right移到mid - 1,继续向左搜索更早的target。 - 循环结束时,
left指向第一个等于或大于target的位置,需验证nums[left] == target。
- 当
-
右边界(末次出现)查找:
- 当
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)1 0 5 2 7 < 8 left = mid + 1 = 32 3 5 4 8 = 8 right = mid - 1 = 3(继续向左搜索)3 3 3 3 8 = 8 right = mid - 1 = 2(循环结束) -
结果:
left = 3
→ 验证nums[3] = 8 = target✅,首次位置为3。
2. 查找右边界(末次出现)
-
初始化:
left = 0,right = 5 -
循环过程:
步骤 leftrightmidnums[mid]操作(因 nums[mid]≤target)1 0 5 2 7 < 8 left = mid + 1 = 32 3 5 4 8 = 8 left = mid + 1 = 5(继续向右搜索)3 5 5 5 10 > 8 right = mid - 1 = 4(循环结束) -
结果:
right = 4
→ 验证nums[4] = 8 = target✅,末次位置为4。
⚠️ 边界与异常处理
-
目标值不存在:
- 左边界查找后若
nums[left] != target,返回[-1, -1]。
示例:nums = [5, 7, 7, 8, 8, 10],target = 6→ 左边界查找结束left = 2,但nums[2] = 7 ≠ 6,返回[-1, -1]。
- 左边界查找后若
-
全相同元素:
nums = [8, 8, 8, 8],target = 8:- 左边界:
left结束于0; - 右边界:
right结束于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]
💡 常见错误与避免
-
死循环:
- 右边界查找时若用
mid = (left + right) // 2,当只剩两个元素(如left=3, right=4)可能陷入死循环。
→ 解决:改用mid = left + (right - left + 1) // 2。
- 右边界查找时若用
-
越界访问:
- 循环结束后需检查
left或right是否在有效索引范围内。
- 循环结束后需检查
-
混淆边界条件:
- 左边界结束时
left指向首个 ≥target的位置,右边界结束时right指向首个 ≤target的位置,需二次验证是否等于target。
- 左边界结束时
⚡ 面试提示:若被问“为何不用线性扫描?”,强调有序数组的特性使二分查找将时间复杂度从 O(n) 优化到 O(log n),适合大规模数据。
在有序数组中查找目标值的插入位置是二分查找的经典应用场景,其核心原理是通过不断缩小搜索区间定位第一个大于或等于目标值的位置。以下我将从原理、图例步骤到代码实现详细说明。
⚙️ 核心原理
- 有序数组特性:数组升序排列时,插入位置
pos满足:- 所有
[0, pos-1]的元素均 <target - 所有
[pos, n-1]的元素均 ≥target
- 所有
- 二分查找终止条件:
- 若找到
target,直接返回其索引。 - 若未找到,循环结束时
left指向首个 ≥target的位置,即插入位置 。
- 若找到
- 指针移动逻辑:
nums[mid] < target→ 目标在右侧 →left = mid + 1nums[mid] ≥ target→ 目标在左侧或等于 →right = mid - 1
📊 图例与步骤说明
数组:[1, 3, 5, 6],目标值:target = 2(需插入到索引 1)
初始状态:left = 0, right = 3
| 步骤 | left | right | mid | nums[mid] | 比较结果 | 操作 |
|---|---|---|---|---|---|---|
| 1 | 0 | 3 | 1 | 3 | 3 > 2 | right = mid-1 = 0 |
| 2 | 0 | 0 | 0 | 1 | 1 < 2 | left = mid+1 = 1 |
| 结束 | left=1(插入位置) |
结果:
- 循环结束时
left = 1,right = 0(left > 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(空数组)
⚠️ 关键点解析
- 终止条件
left <= right:- 确保单元素数组(如
[5],target=5)能被正确处理 。
- 确保单元素数组(如
- 插入位置为
left的推导:- 循环结束时,
left指向 第一个 ≥target的元素位置。若target超过最大值,left = nums.length。
- 循环结束时,
- 边界情况处理:
- 空数组:直接返回
0。 - 目标值小于最小值:
left保持为0。 - 目标值大于最大值:
left递增至nums.length。
- 空数组:直接返回
⏱️ 复杂度与适用场景
- 时间复杂度:O(log n),每次迭代范围减半。
- 空间复杂度:O(1),仅需常数级变量。
- 适用场景:静态有序数组的插入位置搜索(如数据库索引维护、日志时间戳插入)。
此算法通过二分查找的指针终止特性,将插入位置与目标查找统一处理,兼顾高效性与代码简洁性。