一、解题思维总结
1. 何时使用二分查找?
| 场景 | 解法 |
|---|---|
| 有序数组中查找目标值 | 基础二分查找 |
| 有序矩阵中查找目标值 | 先定位行再二分查找 |
| 寻找满足条件的最小值/最大值 | 二分查找边界 |
| 旋转排序数组中的查找 | 先找旋转点再二分查找 |
| 基于时间戳的键值存储 | 二分查找时间戳 |
2. 复杂度分析
- 时间复杂度:O(log n)
- 空间复杂度:O(1)(递归实现可能为 O(log n))
3. 常用技巧
- 初始化指针:
let left = 0, right = nums.length - 1; - 计算中间值:
const mid = left + Math.floor((right - left) / 2);(避免溢出) - 循环条件:
left <= right(查找存在性)或left < right(查找最值) - 更新指针:根据比较结果调整 left 或 right
二、核心技术
技巧一:基础二分查找
适用场景:有序数组中查找目标值
核心要点:
- 有序数组是前提
- 循环条件
left <= right - 找到目标值立即返回
典型例题:二分查找
var search = function(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
};
技巧二:二维矩阵中的二分查找
适用场景:每行有序且每行首元素大于前一行尾元素的矩阵
核心要点:
- 先定位可能包含目标的行
- 再在该行内进行二分查找
典型例题:搜索二维矩阵
var searchMatrix = function(matrix, target) {
let rowIndex = 0, rowStart = 0, rowEnd = matrix.length - 1;
while (rowStart <= rowEnd) {
rowIndex = rowStart + Math.floor((rowEnd - rowStart) / 2);
if (matrix[rowIndex][0] <= target &&
matrix[rowIndex][matrix[rowIndex].length - 1] >= target) {
break;
}
if (matrix[rowIndex][0] < target) {
rowStart = rowIndex + 1;
continue;
}
rowEnd = rowIndex - 1;
}
if (rowStart > rowEnd) {
return false;
}
let colLeft = 0, colRight = matrix[rowIndex].length - 1;
while (colLeft <= colRight) {
const middle = colLeft + Math.floor((colRight - colLeft) / 2);
if (matrix[rowIndex][middle] === target) {
return true;
}
if (matrix[rowIndex][middle] < target) {
colLeft = middle + 1;
continue;
}
colRight = middle - 1;
}
return false;
};
技巧三:寻找满足条件的最小值
适用场景:需要找到满足条件的最小速度/值
核心要点:
- 确定左右边界(如最小速度为 1,最大速度为数组最大值)
- 循环条件
left < right - 找到满足条件的mid后,调整
right = mid以寻找更小值
典型例题:爱吃香蕉的珂珂
var minEatingSpeed = function (piles, h) {
let low = 1;
let high = Math.max(...piles);
while (low < high) {
let mid = Math.floor((low + high) / 2);
let hoursNeeded = 0;
for (let pile of piles) {
hoursNeeded += Math.ceil(pile / mid);
}
if (hoursNeeded <= h) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
};
技巧四:旋转排序数组中的最小值
适用场景:旋转后的有序数组(元素互不相同)
核心要点:
- 比较中间元素和右边界元素
- 若
nums[mid] >= nums[right],则最小值在右半部分 - 否则在左半部分(包括 mid)
典型例题:寻找旋转排序数组中的最小值
var findMin = function(nums) {
let left = 0, right = nums.length - 1;
while (left < right) {
const middle = left + Math.floor((right - left) / 2);
if (nums[middle] >= nums[right]) {
left = middle + 1;
continue;
}
right = middle;
}
return nums[left];
};
技巧五:旋转排序数组中的目标查找
适用场景:旋转后的有序数组中查找目标值
核心要点:
- 先找到旋转点(最小值位置)
- 再根据目标值与首元素的比较确定查找范围
- 在对应范围内进行二分查找
典型例题:搜索旋转排序数组
var search = function(nums, target) {
const pivot = findPivot(nums);
if (pivot === 0) {
return binarySearch(nums, 0, nums.length - 1, target);
}
if (target >= nums[0]) {
return binarySearch(nums, 0, pivot, target);
} else {
return binarySearch(nums, pivot, nums.length - 1, target);
}
};
// 查找支点
function findPivot(arr) {
let left = 0, right = arr.length - 1;
while (left < right) {
const middle = left + Math.floor((right - left) / 2);
if (arr[middle] > arr[right]) {
left = middle + 1;
continue;
}
right = middle;
}
return left;
}
// 二分查找
function binarySearch(arr, left, right, target) {
let index = -1;
while (left <= right) {
const middle = left + Math.floor((right - left) / 2);
if (arr[middle] === target) {
index = middle;
break;
}
if (arr[middle] > target) {
right = middle - 1;
continue;
}
left = middle + 1;
}
return index;
}
技巧六:基于时间的键值存储
适用场景:需要根据时间戳检索值
核心要点:
- 使用 Map 存储键对应的时间戳-值对列表
- set 操作追加到列表(时间戳严格递增)
- get 操作使用二分查找找到最大的不大于目标时间戳的值
典型例题:基于时间的键值存储
var TimeMap = function() {
this.map = new Map();
};
TimeMap.prototype.set = function(key, value, timestamp) {
if (!this.map.has(key)) {
this.map.set(key, []);
}
this.map.get(key).push([timestamp, value]);
};
TimeMap.prototype.get = function(key, timestamp) {
const arr = this.map.get(key);
if (!arr) {
return '';
}
let left = 0, right = arr.length - 1;
while (left <= right) {
const middle = left + Math.floor((right - left) / 2);
const [t, v] = arr[middle];
if (t === timestamp) {
return v;
}
if (t < timestamp) {
left = middle + 1;
continue;
}
right = middle - 1;
}
return arr[right] ? arr[right][1] : '';
};
技巧七:两个正序数组的中位数
适用场景:找到两个正序数组的中位数(要求 O(log(m+n)) 复杂度)
核心要点:
- 转化为寻找第 k 小的元素
- 递归或迭代地缩小查找范围
- 处理边界情况
典型例题:找两个正序数组的中位数
// 基础解法(O(m+n)复杂度)
var findMedianSortedArrays = function(nums1, nums2) {
const arr = [];
let i = 0, j = 0;
while (i < nums1.length || j < nums2.length) {
if (i < nums1.length && j < nums2.length) {
if (nums1[i] <= nums2[j]) {
arr.push(nums1[i]);
++i;
} else {
arr.push(nums2[j]);
++j;
}
continue;
}
if (i < nums1.length) {
arr.push(nums1[i]);
++i;
} else if (j < nums2.length) {
arr.push(nums2[j]);
++j;
}
}
let index = arr.length / 2;
if (parseInt(index + '') === index) {
return (arr[index] + arr[index - 1]) / 2;
}
index = Math.floor(index);
return arr[index];
};
三、易错点提醒
- 计算 mid 时避免溢出:使用
left + Math.floor((right - left)/2)而非(left+right)/2 - 循环条件的选择:查找存在性用
left <= right,查找最值用left < right - 更新指针时注意 +1 或 -1:避免死循环
- 旋转数组中比较的是中间元素和右边界元素:而非左边界元素
- 基于时间的键值存储中 set 操作的效率:直接 push 比创建新数组更高效
四、学习心得
二分查找的优势
- 高效:时间复杂度 O(log n),远优于线性查找
- 灵活:可应用于多种类型的问题
- 简洁:代码实现相对简单
解题思维模式
- 确定边界:明确查找范围的左右边界
- 定义条件:确定如何根据中间值调整边界
- 循环终止:明确循环结束的条件和返回结果