当前位置: 首页 > news >正文

LeetCode 25:K 个一组翻转链表

LeetCode 25:K 个一组翻转链表

在这里插入图片描述

问题定义与核心挑战

给定链表头节点 head 和整数 k,要求 k 个节点一组翻转链表,不足 k 个的节点保持原序。例如:

  • 输入:head = [1,2,3,4,5], k=2 → 输出:[2,1,4,3,5]
  • 输入:head = [1,2,3,4,5], k=3 → 输出:[3,2,1,4,5]

核心挑战:

  1. 分组与边界判断:如何确定每组的范围(找到尾节点),并处理不足 k 个节点的情况。
  2. 链表反转:如何高效反转每组内的节点,并连接前后部分。
  3. 递归与迭代的平衡:递归思路更直观,但需理解栈的调用逻辑。

核心思路:递归 + 局部反转

  1. 分组定位:找到当前组的尾节点(从 head 出发走 k-1 步),若不足 k 个节点则直接返回。
  2. 局部反转:反转当前组内的节点(从 head 到尾节点)。
  3. 递归连接:将反转后的组与剩余链表(尾节点的下一个节点开始)递归处理的结果连接。

算法步骤详解

步骤 1:定位当前组的尾节点
  • head 出发,移动 k-1 次,找到当前组的尾节点 tail
  • 若移动过程中 tail 变为 null,说明剩余节点不足 k 个,直接返回 head(无需翻转)。
ListNode tail = head;
for (int i = 0; i < k - 1; i++) {if (tail == null) return head; // 不足k个,直接返回tail = tail.next;
}
if (tail == null) return head; // 双重保险(罕见情况)
步骤 2:保存剩余链表并反转当前组
  • 剩余链表的头节点为 tail.next(记为 rest)。
  • 反转当前组(headtail),得到新的头节点 newHead(原 tail)。
ListNode rest = tail.next; // 剩余链表的头
ListNode newHead = reverse(head, tail); // 反转当前组
步骤 3:递归处理剩余链表并连接
  • 递归处理剩余链表 rest,得到处理后的头节点 restHead
  • 将当前组的尾节点(原 head)连接到 restHead
ListNode restHead = reverseKGroup(rest, k); // 递归处理剩余部分
head.next = restHead; // 连接反转后的尾(原head)与剩余部分
return newHead; // 返回当前组反转后的头(原tail)
步骤 4:实现局部反转函数 reverse

反转从 headtail 的链表,返回新的头节点(原 tail):

  • 用三指针法(prev, curr, next)逐步反转节点。
  • 最后处理尾节点 tail,使其指向反转后的前一个节点。
private ListNode reverse(ListNode head, ListNode tail) {ListNode prev = null;ListNode curr = head;while (curr != tail) { // 遍历到尾节点前一个ListNode next = curr.next;curr.next = prev; // 反转指针prev = curr;curr = next;}curr.next = prev; // 处理尾节点return curr; // 新头是原tail
}

完整代码(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 reverseKGroup(ListNode head, int k) {// 边界处理:空链表或k=1(无需翻转)if (head == null || k == 1) {return head;}// 步骤1:定位当前组的尾节点ListNode tail = head;for (int i = 0; i < k - 1; i++) {if (tail == null) { // 不足k个节点,直接返回return head;}tail = tail.next;}if (tail == null) { // 双重检查(理论上不会触发)return head;}// 步骤2:保存剩余链表,反转当前组ListNode rest = tail.next; // 剩余链表的头ListNode newHead = reverse(head, tail); // 反转当前组(head->tail)// 步骤3:递归处理剩余链表,连接结果ListNode restHead = reverseKGroup(rest, k); // 递归处理剩余部分head.next = restHead; // 原head现在是反转后的尾,连接剩余部分return newHead; // 返回当前组反转后的头(原tail)}// 辅助函数:反转从head到tail的链表,返回新的头(原tail)private ListNode reverse(ListNode head, ListNode tail) {ListNode prev = null;ListNode curr = head;while (curr != tail) { // 遍历到尾节点前一个ListNode next = curr.next;curr.next = prev; // 反转指针prev = curr;curr = next;}curr.next = prev; // 处理尾节点,使其指向反转后的前一个节点return curr; // 新头是原tail}
}

关键逻辑解析

1. 分组定位的细节
  • 循环 k-1 次找尾节点:确保当前组恰好有 k 个节点(headtailk 个)。
  • 若中途 tailnull,说明剩余节点不足 k 个,直接返回(避免无效反转)。
2. 局部反转的三指针法
  • prev 初始为 nullcurrhead 开始,逐步将 curr.next 指向 prev,实现反转。
  • 循环结束时,curr 指向 tail,需单独处理 tailnext(指向 prev,即反转后的前一个节点)。
3. 递归的作用
  • 递归处理剩余链表 rest,天然实现“分组处理”的逻辑,代码更简洁。
  • 递归栈的深度为 O(n/k)(每组递归一次),空间复杂度可接受(n 为链表长度)。
4. 链表连接的关键
  • 反转后,原 head 变为当前组的尾节点,需将其 next 指向剩余链表的处理结果(restHead)。

示例验证(以示例 1 为例)

输入head = [1,2,3,4,5], k=2

  1. 第一次递归

    • 定位 tail = 2head=1 移动 1 次),rest = 3
    • 反转 1->2 得到 2->1newHead=2
    • 递归处理 rest=3,进入第二次递归。
  2. 第二次递归

    • 定位 tail = 4head=3 移动 1 次),rest = 5
    • 反转 3->4 得到 4->3newHead=4
    • 递归处理 rest=5,进入第三次递归。
  3. 第三次递归

    • 定位 tail 时,head=5 移动 1 次后 tail=null,返回 5
    • restHead=5,原 head=3(反转后的尾)的 next=53->5
    • 返回 4->3->5
  4. 回溯连接

    • 第二次递归中,原 head=3next=4->3->51->4->3->5
    • 第一次递归中,原 head=1next=4->3->52->1->4->3->5

复杂度分析

  • 时间复杂度O(n)。每个节点被访问两次(一次定位尾节点,一次反转)。
  • 空间复杂度O(n/k)。递归栈的深度为分组数(最坏情况 n/k 层,如 k=1 时退化为 O(n),但题目中 k≥1k≤n)。

该方法通过 递归分组 + 局部反转,巧妙地将复杂的链表操作分解为子问题,代码简洁且逻辑清晰,是链表类问题的经典解法。

http://www.dtcms.com/a/306525.html

相关文章:

  • MCU中的CAN总线是什么?
  • WebRTC核心组件技术解析:架构、作用与协同机制
  • 一文掌握最新版本Monocle3单细胞轨迹(拟时序)分析
  • 如何将JPG、PNG、GIF图像转换成PDF、SVG、EPS矢量图像
  • Rust基础[part9]_返回值和错误处理、模块化
  • [特殊字符] 征服CPU的艺术:Rust多进程编程实战指南
  • Cortex-M处理器的优势?
  • STM32CubeIDE新建项目过程记录备忘(二)
  • FFmpeg:因码流采集与封装不同步导致录制出来的MP4文件会出现黑屏、绿屏的问题
  • Zynq SoC 中断控制系统设计与实现:基于 GPIO 的中断驱动开发
  • LocalDateTime vs Instant vs ZonedDateTime:到底该用哪个?
  • .net6的webapi项目统一封装返回值
  • 剧本杀系统 App 开发:科技赋能,重塑剧本杀游戏体验
  • 光伏气象监测系统:当阳光遇见科技
  • Javascript 基础总结
  • 做题笔记:某大讯飞真题28道
  • 浅拷贝和深拷贝
  • uni-app,uni.navigateTo
  • 【LangChain4j 详解】Java生态大语言模型框架设计哲学与架构原理
  • Node.js以及异步编程
  • vue模块化导入
  • 网络安全学习第16集(cdn知识点)
  • Android调用python库和方法的实现
  • 开源项目:排序算法的多种实现方式
  • DAY15-指针(3)
  • 解决:React Native 中常见的 状态栏遮挡内容
  • python 中 TypeError: self类型对象传入错误解决办法
  • 在职申硕,怎么选适合自己的学科专业呢?
  • 计算机网络1-3:三种交换方式
  • sed编程入门