在LeetCode中等难度题目中,「搜索旋转排序数组」是一道经典的二分查找变形题。它的核心考点的是对“旋转数组”特性的理解,以及如何在非完全升序的数组中,依然保持二分查找O(log n)的时间复杂度。今天就来一步步拆解这道题,从题目分析到代码实现,再到细节注意点,帮你彻底搞懂它。
一、题目解读:什么是旋转排序数组?
题目给出的前提很明确:
-
原数组是升序排列的,且所有元素互不相同(这一点很关键,避免了重复元素带来的判断干扰);
-
数组在某个未知下标k处「向左旋转」,旋转后数组变成 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]];
-
给定旋转后的数组nums和目标值target,要求找到target的下标,找不到则返回-1,且必须满足O(log n)时间复杂度。
举个例子:原数组 [0,1,2,4,5,6,7],在k=3处向左旋转后,得到 [4,5,6,7,0,1,2]。如果target=0,返回下标4;如果target=3,返回-1。
这里的核心矛盾是:数组不再是完全升序,但又保留了“部分升序”的特性——旋转后数组会被分成两个升序子数组(比如例子中的 [4,5,6,7] 和 [0,1,2])。而二分查找的核心是“通过中间值缩小查找范围”,所以我们的思路就是利用这种“部分升序”,判断target落在哪个升序区间,进而调整双指针。
二、核心思路:二分查找的变形应用
常规二分查找适用于完全升序数组,通过mid值与target的大小对比,直接调整左右指针。但旋转数组有两个升序区间,所以我们需要先判断「mid所在的区间是否是升序区间」,再判断target是否在该区间内,从而确定指针移动方向。
具体步骤拆解:
-
初始化双指针l=0(左边界)、r=n-1(右边界),n为数组长度;
-
循环条件:l ≤ r(当l超过r时,说明查找范围为空,未找到target);
-
计算mid = Math.floor((l + r) / 2)(注意TS/JS中除法会返回浮点数,需手动取整);
-
若nums[mid] === target,直接返回mid(找到目标值);
-
判断mid所在的区间是否为升序:
-
情况1:nums[0] ≤ nums[mid] → mid左侧(l到mid)是升序区间;
-
情况2:nums[0] > nums[mid] → mid右侧(mid到r)是升序区间;
-
-
根据升序区间,判断target是否在该区间内,调整指针:
-
情况1(左侧升序):若target在[nums[0], nums[mid])之间 → 缩小范围到左侧(r=mid-1);否则缩小到右侧(l=mid+1);
-
情况2(右侧升序):若target在(nums[mid], nums[n-1]]之间 → 缩小范围到右侧(l=mid+1);否则缩小到左侧(r=mid-1);
-
-
循环结束后仍未找到,返回-1。
三、完整代码实现(TypeScript)
结合上述思路,给出完整的TypeScript代码,关键步骤已添加注释,方便理解:
function search(nums: number[], target: number): number {
const n = nums.length;
// 边界处理:数组为空,直接返回-1
if (n === 0) {
return -1;
}
// 边界处理:数组只有一个元素,直接判断是否等于target
if (n === 1) {
return nums[0] === target ? 0 : -1;
}
// 初始化双指针:左指针l,右指针r
let l = 0, r = n - 1;
// 二分查找循环:当左指针不大于右指针时,继续查找
while (l <= r) {
// 计算中间下标mid,手动取整避免浮点数
const mid = Math.floor((l + r) / 2);
// 找到目标值,直接返回下标
if (nums[mid] === target) {
return mid;
}
// 情况1:mid左侧是升序区间(nums[0] <= nums[mid])
if (nums[0] <= nums[mid]) {
// 判断target是否在左侧升序区间内(nums[0] <= target < nums[mid])
if (nums[0] <= target && target < nums[mid]) {
// 缩小范围到左侧,右指针左移
r = mid - 1;
} else {
// 缩小范围到右侧,左指针右移
l = mid + 1;
}
} else {
// 情况2:mid右侧是升序区间(nums[0] > nums[mid])
// 判断target是否在右侧升序区间内(nums[mid] < target <= nums[n-1])
if (nums[mid] < target && target <= nums[n - 1]) {
// 缩小范围到右侧,左指针右移
l = mid + 1;
} else {
// 缩小范围到左侧,右指针左移
r = mid - 1;
}
}
}
// 循环结束,未找到目标值,返回-1
return -1;
}
四、关键细节与易错点
这道题的代码不算复杂,但细节处理不到位很容易出错,尤其是以下3个点:
1. 边界条件处理
必须先处理数组为空(n=0)和数组只有一个元素(n=1)的情况。如果忽略这两个边界,当数组长度为0时会出现指针异常,长度为1时会进入循环做无用功,影响效率。
2. mid的计算方式
在TypeScript/JavaScript中,(l + r) / 2 会返回浮点数(比如l=0、r=1时,结果是0.5),所以必须用Math.floor()取整,否则mid会是小数,导致数组下标报错。
补充:也可以用 (l + r) >> 1 进行位运算取整(效果等同于Math.floor((l + r)/2)),但要注意避免溢出(本题中数组长度不会过大,两种方式均可)。
3. 区间判断的等号问题
这是最容易出错的地方,比如:
-
左侧升序区间判断时,用 nums[0] ≤ nums[mid](包含等于,因为mid可能就是0下标,此时左侧只有一个元素,也是升序);
-
target在左侧区间的判断的是 nums[0] ≤ target && target < nums[mid](target不能等于nums[mid],因为前面已经判断过nums[mid] !== target);
-
右侧升序区间判断时,target的范围是 nums[mid] < target && target ≤ nums[n-1](同理,target不能等于nums[mid])。
如果等号位置写错,会导致指针调整错误,进而错过目标值或者进入死循环。
五、复杂度分析与题目延伸
1. 时间复杂度
整个算法采用二分查找,每次循环都会将查找范围缩小一半,所以时间复杂度是 O(log n),完全满足题目要求。
2. 空间复杂度
算法只使用了常数个变量(l、r、mid、n),没有使用额外的空间,空间复杂度是 O(1)。
3. 题目延伸
这道题的变形题是「搜索旋转排序数组II」,区别在于数组元素可以重复。此时,nums[0] ≤ nums[mid] 无法直接判断左侧是升序区间(比如 [1,0,1,1,1]),需要先处理重复元素(比如当nums[l] === nums[mid]时,l++),感兴趣的可以后续深入研究。
六、总结
「搜索旋转排序数组」的核心是“利用旋转数组的部分升序特性,改造二分查找”。解题的关键在于:
-
判断mid所在的升序区间;
-
根据target是否在该升序区间,调整双指针;
-
注意边界条件和等号的处理。
这道题虽然是中等难度,但只要掌握了二分查找的核心思想,再结合旋转数组的特性,就能轻松破解。建议大家多动手调试代码,尝试不同的测试用例(比如旋转点在开头、结尾、中间的情况),加深对算法的理解。