33.搜索旋转排序数组

在力扣的算法题库中,"搜索旋转排序数组"是一道非常经典的二分查找变体题。很多同学在遇到"旋转"这个条件时,往往会觉得无法直接使用二分查找。今天我们就来详细拆解这道题,带你掌握如何在看似无序的数组中,依然能够运用二分思想实现 O(log n) 的高效查找。
1. 题目回顾
假设有一个严格升序排列的数组,在某个未知的下标处进行了旋转(例如 [0,1,2,4,5,6,7] 旋转后变为 [4,5,6,7,0,1,2])。现在给定这个旋转后的数组 nums 和一个目标值 target,要求返回 target 的下标,如果不存在则返回 -1。
核心要求:算法的时间复杂度必须控制在 O(log n)。
2. 核心思路:寻找"局部有序"
面对 O(log n) 的时间复杂度要求,我们首先想到的就是二分查找。虽然数组整体被旋转打乱了顺序,但它有一个极其重要的特性:
无论我们从哪个位置将数组一分为二,其中至少有一半是严格有序的。
比如 [4,5,6,7,0,1,2],如果我们从中间切开:
- 左半部分
[4,5,6]是有序的,右半部分[7,0,1,2]是无序的; - 或者换个切法,右半部分有序,左半部分无序。
解题的关键就在于:在二分的过程中,判断哪一半是有序的,并进一步判断目标值是否在这个有序区间内,从而决定下一步的搜索方向。
3. 算法详细步骤
- 初始化指针 :设置
left = 0,right = nums.length - 1。 - 进入二分循环 :当
left <= right时持续执行。 - 计算中点 :
mid = left + (right - left) / 2(防止整型溢出)。 - 命中检查 :如果
nums[mid] == target,直接返回mid。 - 判断有序区间并收缩边界 :
- 情况一:左半部分有序 (即
nums[left] <= nums[mid])- 如果
target刚好落在左侧有序区间内(nums[left] <= target < nums[mid]),说明目标在左边,收缩右边界right = mid - 1。 - 否则,目标在右边,收缩左边界
left = mid + 1。
- 如果
- 情况二:右半部分有序 (即
nums[left] > nums[mid])- 如果
target刚好落在右侧有序区间内(nums[mid] < target <= nums[right]),说明目标在右边,收缩左边界left = mid + 1。 - 否则,目标在左边,收缩右边界
right = mid - 1。
- 如果
- 情况一:左半部分有序 (即
4. 代码实现(Java & Kotlin)
Java版本
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
// 计算中间下标,防止 (left + right) 溢出
int mid = left + (right - left) / 2;
// 1. 命中目标值,直接返回
if (nums[mid] == target) {
return mid;
}
// 2. 判断左半部分 [left, mid] 是否有序
if (nums[left] <= nums[mid]) {
// 左半部分有序,判断 target 是否在左侧区间内
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1; // 在左侧,收缩右边界
} else {
left = mid + 1; // 不在左侧,去右侧找
}
}
// 3. 否则,右半部分 [mid, right] 一定有序
else {
// 右半部分有序,判断 target 是否在右侧区间内
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1; // 在右侧,收缩左边界
} else {
right = mid - 1; // 不在右侧,去左侧找
}
}
}
// 循环结束未找到,返回 -1
return -1;
}
}
Kotlin版本
class Solution {
fun search(nums: IntArray, target: Int): Int {
var left = 0
var right = nums.size - 1
while (left <= right) {
val mid = left + (right - left) / 2
if (nums[mid] == target) {
return mid
}
// 判断左半部分是否有序
if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1
} else {
left = mid + 1
}
} else {
// 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
}
5. 复杂度分析
- 时间复杂度:O(log n)。每次循环都将搜索区间缩小一半,符合二分查找的标准复杂度。
- 空间复杂度 :O(1)。只使用了
left、right、mid等常数级别的额外空间。
92.翻转链表Ⅱ

相比于完全反转整个链表,这道题要求我们只反转链表中的指定区间
[left, right],这非常考验对链表指针操作的细节把控。
1.题目回顾
给你单链表的头指针 head 和两个整数 left 和 right(其中 left <= right)。请你反转从位置 left 到位置 right 的链表节点,并返回反转后的链表。
示例 :
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
2.核心思路:穿针引线法
这道题的核心在于将链表分为三个部分:未反转的前半段 、需要反转的区间 、未反转的后半段。我们只需要把中间那段反转,再重新把这三段"缝合"起来即可。
具体步骤如下:
- 引入虚拟头节点(Dummy Node) :
因为left有可能是1(即从头节点开始反转),为了避免对头节点进行复杂的特判,我们创建一个虚拟头节点dummy,让dummy.next = head。 - 定位前驱节点
p0:
我们需要找到反转区间的前一个节点(即left - 1位置的节点),记为p0。它将是连接"前半段"和"反转后区间"的关键锚点。 - 局部反转区间 :
从p0.next开始,对区间内的right - left + 1个节点进行标准的链表反转操作。 - 重新连接链表 :
反转结束后,我们需要把断开的链表接回去:- 原反转区间的第一个节点 (反转后变成了区间的最后一个节点 ),它的
next应该指向后半段的第一个节点。 - 前驱节点
p0的next应该指向反转后区间的新头节点。
- 原反转区间的第一个节点 (反转后变成了区间的最后一个节点 ),它的
3.算法详细步骤与图解
假设链表为 1 -> 2 -> 3 -> 4 -> 5,left = 2, right = 4。
- 准备阶段 :
- 创建
dummy(0) -> 1 -> 2 -> 3 -> 4 -> 5。 - 移动
p0到left - 1的位置,此时p0指向节点1。
- 创建
- 反转准备 :
cur指向反转区间的第一个节点(节点2)。pre初始化为null,用于反转过程中的前驱记录。
- 执行局部反转(循环
right - left + 1次) :- 使用标准的"三指针法"反转
2 -> 3 -> 4这段链表。 - 反转结束后:
pre指向节点4(新区间的头),cur指向节点5(后半段的头),而原来的p0.next(节点2)变成了新区间的尾。
- 使用标准的"三指针法"反转
- 缝合链表 :
p0.next.next = cur:让节点2指向节点5。p0.next = pre:让节点1指向节点4。- 最终得到:
1 -> 4 -> 3 -> 2 -> 5。
4.代码实现(Java && Kotlin)
Java版本
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 1. 创建虚拟头节点,简化 left=1 时的边界情况处理
ListNode dummy = new ListNode(0, head);
// 2. 找到反转区间的前驱节点 p0 (即 left - 1 位置的节点)
ListNode p0 = dummy;
for (int i = 0; i < left - 1; i++) {
p0 = p0.next;
}
// 3. 开始反转区间 [left, right] 内的节点
// pre 用于记录反转后的前驱,cur 指向当前要反转的节点(初始为 left 位置的节点)
ListNode pre = null;
ListNode cur = p0.next;
// 需要反转的节点数量为 right - left + 1
for (int i = 0; i < right - left + 1; i++) {
ListNode nxt = cur.next; // 暂存 cur 的后继节点,防止断链
cur.next = pre; // 反转当前节点的指针
pre = cur; // pre 指针后移
cur = nxt; // cur 指针后移
}
// 4. 重新连接链表
// 此时:
// - pre 指向反转区间的新头节点(原 right 位置的节点)
// - cur 指向反转区间后面的第一个节点(即后半段的头)
// - p0.next 指向原反转区间的第一个节点(反转后变成了区间的尾节点)
// 让原反转区间的第一个节点(现尾部)指向后半段的头节点
p0.next.next = cur;
// 让前驱节点 p0 指向反转后的新头节点
p0.next = pre;
// 5. 返回新链表的头节点
return dummy.next;
}
}
Kotlin版本
/**
* Definition for singly-linked list.
* class ListNode(var `val`: Int) {
* var next: ListNode? = null
* }
*/
class Solution {
fun reverseBetween(head: ListNode?, left: Int, right: Int): ListNode? {
// 1. 创建虚拟头节点,指向原链表头,简化 left=1 时的边界处理
val dummy = ListNode(0)
dummy.next = head
// 2. 找到反转区间的前驱节点 p0 (即 left - 1 位置的节点)
var p0: ListNode? = dummy
for (i in 0 until left - 1) {
p0 = p0?.next
}
// 3. 开始局部反转区间 [left, right] 内的节点
// pre 用于记录反转后的前驱,cur 指向当前要反转的节点(初始为 left 位置的节点)
var pre: ListNode? = null
var cur: ListNode? = p0?.next
// 需要反转的节点数量为 right - left + 1
for (i in 0 until right - left + 1) {
val nxt = cur?.next // 暂存 cur 的后继节点,防止断链
cur?.next = pre // 反转当前节点的指针
pre = cur // pre 指针后移
cur = nxt // cur 指针后移
}
// 4. 重新连接链表(缝合操作)
// 此时:
// - pre 指向反转区间的新头节点(原 right 位置的节点)
// - cur 指向反转区间后面的第一个节点(即后半段的头)
// - p0?.next 指向原反转区间的第一个节点(反转后变成了区间的尾节点)
// 让原反转区间的第一个节点(现尾部)指向后半段的头节点
p0?.next?.next = cur
// 让前驱节点 p0 指向反转后的新头节点
p0?.next = pre
// 5. 返回新链表的头节点(跳过虚拟头节点)
return dummy.next
}
}
5.复杂度分析
- 时间复杂度 :O(n)。其中 n 是链表的长度。我们只需要遍历一次链表(先走到
left前驱,再反转指定区间),整体是线性的。 - 空间复杂度 :O(1)。我们只使用了
dummy,p0,pre,cur等固定数量的指针变量,没有使用额外的数组或递归栈空间。