力扣实训 _ [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 = 0right = 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)。只使用了 leftrightmid 等常数级别的额外空间。

92.翻转链表Ⅱ


相比于完全反转整个链表,这道题要求我们只反转链表中的指定区间 [left, right],这非常考验对链表指针操作的细节把控。

1.题目回顾

给你单链表的头指针 head 和两个整数 leftright(其中 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 应该指向后半段的第一个节点。
    • 前驱节点 p0next 应该指向反转后区间的新头节点

3.算法详细步骤与图解

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

  1. 准备阶段
    • 创建 dummy(0) -> 1 -> 2 -> 3 -> 4 -> 5
    • 移动 p0left - 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 等固定数量的指针变量,没有使用额外的数组或递归栈空间。
相关推荐
MrZhao4001 小时前
多 Agent 协作与通信:MessageBus 最小实现
算法
Zhang~Ling1 小时前
二叉搜索树(BST)详解:插入、删除、查找与 Key/Value 实战场景
数据结构·c++·算法
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第86题】【Mysql篇】第16题:MySQL 中锁的种类与行锁实现原理?
java·开发语言·数据库·mysql·面试
日月云棠1 小时前
12 Enum —— 枚举类型的底层实现
java·后端
8Qi81 小时前
LeetCode 76. 最小覆盖子串(Minimum Window Substring)
数据结构·算法·leetcode·滑动窗口·哈希表
轻刀快马1 小时前
从繁琐到极简,从幻象到本质:Spring AOP 架构演进与实战避坑指南
java·spring·架构
weixin_BYSJ19871 小时前
springboot旅游管理系统04470(附源码+开发文档+部署教程)
java·spring boot·python·算法·django·flask·旅游
Bingorl1 小时前
机器学习之朴素贝叶斯算法
人工智能·算法·机器学习
8Qi81 小时前
LeetCode 209. 长度最小的子数组(Minimum Size Subarray Sum)
java·算法·leetcode·双指针·滑动窗口