给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
ini
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
ini
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
ini
输入: nums = [1,3,5,6], target = 7
输出: 4
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
为 无重复元素 的 升序 排列数组-104 <= target <= 104
思路:
这道题是个简单题目。logn是典型的二分查找复杂度。直接给出实现了。
Java
class Solution {
public int searchInsert(int[] nums, int target) {
int i = 0, n = nums.length;
while (i < n) {
int mid = (i + n) >> 1;
if (i == mid) {
return target <= nums[i] ? i : i+1;
}
if (nums[mid] > target) {
n = mid;
} else if (nums[mid] < target) {
i = mid;
} else {
return mid;
}
}
return 0;
}
}
这段代码实现了正确的搜索插入位置功能,但在二分法的实现上与标准写法有所不同。以下是关键点分析:
- 区间处理 :代码使用左闭右开区间
[i, n)
,通过调整i
和n
缩小区间。例如,nums[mid] < target
时,i = mid
,相当于新的区间为[mid, n)
,这可能导致缩小区间较慢,但通过循环条件和提前返回仍能正确收敛。 - 终止条件 :在
i == mid
时(即区间长度接近1时),直接判断插入位置。这处理了剩余元素的边界情况,避免了无限循环,确保了正确性。 - 循环外返回:当数组为空时,直接返回0,符合题目要求。其他情况循环内均会返回,不会执行到此处。
虽然代码与常见的左闭右闭或左闭右开的标准二分法有所区别(如调整区间时不使用 mid ± 1
),但其逻辑正确,能够覆盖所有情况。因此,可以视为一种有效的二分法变种。
结论:该实现是正确且高效的二分法,尽管写法与某些标准形式不同,但核心思想一致。
讲讲标准的二分查找
标准的二分法实现通常有两种常见写法,以下是针对「搜索插入位置」问题的两种标准实现,并附详细解释:
标准写法 1:左闭右闭区间 [left, right]
java
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 避免整数溢出
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半部分
} else {
right = mid - 1; // 目标在左半部分
}
}
// 循环结束时,left 是第一个大于 target 的位置
return left;
}
}
关键点解释:
-
区间定义 :
使用左闭右闭区间
[left, right]
,即搜索范围始终包含left
和right
指向的元素。 -
循环条件 :
while (left <= right)
保证区间合法时继续搜索。当left > right
时,所有元素已检查完毕。 -
区间更新:
- 如果
nums[mid] < target
,说明目标在右半部分,更新left = mid + 1
。 - 如果
nums[mid] > target
,说明目标在左半部分,更新right = mid - 1
。
- 如果
-
返回值 :
循环结束时,
left
指向第一个大于target
的位置,即插入位置。若数组为空,直接返回0
。
标准写法 2:左闭右开区间 [left, right)
java
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半部分
} else {
right = mid; // 目标在左半部分(右开)
}
}
// 循环结束时,left 和 right 重合,即插入位置
return left;
}
}
关键点解释:
-
区间定义 :
使用左闭右开区间
[left, right)
,即搜索范围包含left
指向的元素,但不包含right
。 -
循环条件 :
while (left < right)
保证区间合法时继续搜索。当left == right
时,区间为空。 -
区间更新 :
• 如果
nums[mid] < target
,更新left = mid + 1
。• 如果
nums[mid] > target
,更新right = mid
(因为右开,不包含mid
)。 -
返回值 :
循环结束时,
left
和right
重合,指向插入位置。
用户代码与标准写法的对比
你的代码逻辑是正确的,但有以下特点:
-
区间更新方式 :
你使用了类似左闭右开的方式(
i
和n
的更新),但没有显式区分区间定义,而是通过提前判断i == mid
处理边界条件。 -
提前返回 :
当区间缩小到长度为 1 时(
i == mid
),直接比较target
和nums[i]
并返回结果。这避免了标准写法中循环结束后再判断的步骤。 -
代码简洁性 :
标准写法更注重统一处理所有情况,而你的代码通过提前返回简化了部分逻辑。
总结
两种标准写法都严格遵循二分法的核心思想:通过不断折半缩小搜索范围 。它们的区别仅在于区间定义和边界处理,但最终都能在 O(log n) 时间内解决问题。你的代码虽然与标准写法略有差异,但本质是等价的,且时间复杂度相同。