力扣实训 _ [33].搜索旋转排序数组 _ [92].翻转链表Ⅱ

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. 算法详细步骤

  1. 初始化指针 :设置 left = 0,right = nums.length - 1。
  2. 进入二分循环 :当 left <= right 时持续执行。
  3. 计算中点 :mid = left + (right - left) / 2(防止整型溢出)。
  4. 命中检查 :如果 nums[mid] == target,直接返回 mid。
  5. 判断有序区间并收缩边界 :
    • 情况一:左半部分有序 (即 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.核心思路:穿针引线法

这道题的核心在于将链表分为三个部分:未反转的前半段 、需要反转的区间 、未反转的后半段。我们只需要把中间那段反转,再重新把这三段"缝合"起来即可。

具体步骤如下:

  1. 引入虚拟头节点(Dummy Node) :
    因为 left 有可能是 1(即从头节点开始反转),为了避免对头节点进行复杂的特判,我们创建一个虚拟头节点 dummy,让 dummy.next = head。
  2. 定位前驱节点 p0 :
    我们需要找到反转区间的前一个节点(即 left - 1 位置的节点),记为 p0。它将是连接"前半段"和"反转后区间"的关键锚点。
  3. 局部反转区间 :
    从 p0.next 开始,对区间内的 right - left + 1 个节点进行标准的链表反转操作。
  4. 重新连接链表 :
    反转结束后,我们需要把断开的链表接回去:
    • 原反转区间的第一个节点 (反转后变成了区间的最后一个节点 ),它的 next 应该指向后半段的第一个节点。
    • 前驱节点 p0 的 next 应该指向反转后区间的新头节点。

3.算法详细步骤与图解

假设链表为 1 -> 2 -> 3 -> 4 -> 5,left = 2, right = 4。

  1. 准备阶段 :
    • 创建 dummy(0) -> 1 -> 2 -> 3 -> 4 -> 5。
    • 移动 p0 到 left - 1 的位置,此时 p0 指向节点 1。
  2. 反转准备 :
    • cur 指向反转区间的第一个节点(节点 2)。
    • pre 初始化为 null,用于反转过程中的前驱记录。
  3. 执行局部反转(循环 right - left + 1 次) :
    • 使用标准的"三指针法"反转 2 -> 3 -> 4 这段链表。
    • 反转结束后:pre 指向节点 4(新区间的头),cur 指向节点 5(后半段的头),而原来的 p0.next(节点 2)变成了新区间的尾。
  4. 缝合链表 :
    • 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 等固定数量的指针变量,没有使用额外的数组或递归栈空间。
相关推荐
春弦_4 小时前
自用算法整理二
数据结构·算法·图论
缓慢更新4 小时前
审批的加签与会签:多人意见怎么合
android·java·数据库·低代码·rxjava
hetao17338375 小时前
2026-10-22~23 hetao1733837 的刷题记录
c++·算法
yuniko-n5 小时前
【JUC】volatile 非原子性深入解析
java
java_nnnn5 小时前
LeetCode刷题滑动窗口(串联所有单词的子串+最小覆盖的子串)Java
算法·leetcode·职场和发展
sun_weitao5 小时前
极简版 MCP 工具实现方案
java·linux·服务器
Navigator_Z6 小时前
LeetCode //C - 1301. Number of Paths with Max Score
c语言·算法·leetcode
金銀銅鐵7 小时前
[Java] 展示增删元素时 LinkedList 的内部变化
java·后端·ai编程
卡卡~~7 小时前
“101计划”核心课程实践教材《数据实验结构指导-C++语言版》题目集
开发语言·c++·学习·算法
企业数字化笔记7 小时前
AI视频增强后画质怎么验收?插值基线、伪细节与帧间闪烁的检查方法
人工智能·算法·音视频