编码母题 / P0 / 经典算法 / 03
二分查找左右边界
重要程度必练难度中等预计完成25–35 分钟
题目是什么
输入非降序数字数组 nums 和目标值 target,返回 target 首次与最后一次出现的索引 [first,last];不存在时返回 [-1,-1]。要求 O(log n) 时间;空数组也应返回 [-1,-1]。
输入输出示例
输入 [5,7,7,8,8,10] 和 8,输出 [3,4];输入 [] 和 1,输出 [-1,-1]。
练习起手式与验证
/*
* 题目:二分查找左右边界
* 要求:输入非降序数字数组 nums 和目标值 target,返回 target 首次与最后一次出现的索引 [first,last];不存在时返回 [-1,-1]。要求 O(log n) 时间;空数组也应返回 [-1,-1]。
* 示例:输入 [5,7,7,8,8,10] 和 8,输出 [3,4];输入 [] 和 1,输出 [-1,-1]。
*/
function searchRange(nums, target) {}
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([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]
/*
* 题目:二分查找左右边界
* 要求:输入非降序数字数组 nums 和目标值 target,返回 target 首次与最后一次出现的索引 [first,last];不存在时返回 [-1,-1]。要求 O(log n) 时间;空数组也应返回 [-1,-1]。
* 示例:输入 [5,7,7,8,8,10] 和 8,输出 [3,4];输入 [] 和 1,输出 [-1,-1]。
*/
function searchRange(nums, target) {}
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([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]解题思路与实现
展开解题思路
对同一个数组做两次二分。找到 target 时先记下索引;找首次位置就继续查左半边,找最后位置就继续查右半边。循环结束后返回两次记录的结果。
function searchRange(nums, target) {
const main = (nums, target, type) => {
let left = 0;
let right = nums.length - 1;
let result = -1;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (nums[middle] === target) {
result = middle;
// 命中后仍要继续收缩,才能找到重复值的最左或最右索引。
if (type === "first") {
right = middle - 1;
} else {
left = middle + 1;
}
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return result;
};
return [main(nums, target, "first"), main(nums, target, "last")];
}
// 三组以上可直接运行的验证。
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([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]
function searchRange(nums, target) {
const main = (nums, target, type) => {
let left = 0;
let right = nums.length - 1;
let result = -1;
while (left <= right) {
const middle = Math.floor((left + right) / 2);
if (nums[middle] === target) {
result = middle;
// 命中后仍要继续收缩,才能找到重复值的最左或最右索引。
if (type === "first") {
right = middle - 1;
} else {
left = middle + 1;
}
} else if (nums[middle] > target) {
right = middle - 1;
} else {
left = middle + 1;
}
}
return result;
};
return [main(nums, target, "first"), main(nums, target, "last")];
}
// 三组以上可直接运行的验证。
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([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]
时间复杂度:O(log n)。首次和末次位置各做一次二分查找,常数倍不改变复杂度。
空间复杂度:O(1)。只维护边界、当前索引与结果,没有随数组长度增长的辅助结构。